This folder will explain the basic idea of
purescript-run and then solve the paper's version of the Expression Problem using it.
purescript-run? Why would we use that over
Free? There are three reasons.
First, let's look at the type of
newtype Run r a = Run (Free (VariantF r) a)
We can see that
Run is a compile-time-only type that specifies the
Functor type of
Free to the open
Let's compare the same idea encoded in both forms (note:
Run will use naming conventions that will be explained below):
free :: Free (VariantF (add :: FProxy Add, subtract :: FProxy Subtract)) a -- is the same as run :: Run (ADD + SUBTRACT) a
Run draws attention to the effects used and eliminates other distracting "noise" that occurs due to a lot of types.
Second, this library exposes helper functions that add a
MonadRec type class constraint to guarantee that stack overflows won't occur. Due to the recursive nature by which one "interprets" a
Free-based computations can sometimes result in stack overflows. These helper functions make it trivial to insure stack-safety. See the "Stack-Safety" section at the bottom of the project's ReadMe for more info.
Third, this library already defines types and functions for using and working with different effects (e.g.
WriterT, etc. but for the
Free monad). One does not need to re-implement these types for each project, so that the code works every time. (These are also covered more below)
Let's look at a few core functions (the following block of code is licensed under the MIT license:
newtype Run r a = Run (Free (VariantF r) a) -- `Run`'s version of `Free`'s `liftF` lift ∷ ∀ sym r1 r2 f a . Row.Cons sym (FProxy f) r1 r2 ⇒ IsSymbol sym ⇒ Functor f ⇒ Proxy sym → f a → Run r2 a -- Run (Free ( VariantF (row :: type)) output) lift symbol dataType = Run <<< liftF <<< inj symbol dataType -- This function will appear later in this folder's code -- | Extracts the value from a purely interpreted program. extract ∷ ∀ a. Run () a → a -- `Run`'s version of `Free`'s `resume` peel :: forall a r. Run r a -> Either (VariantF r (Run r a)) a
Let's look at some of the type aliases it provides:
type EFFECT = FProxy Effect type AFF = FProxy Aff
Rather than typing
(fieldName :: FProxy Functor), we use an all-caps type alias:
(fieldName :: FUNCTOR). This improves code readability, so we will follow suit.
purescript-run has a few other type aliases that will look familiar.
newtype Reader e a = Reader (e → a) type READER e = FProxy (Reader e) data State s a = State (s → s) (s → a) type STATE s = FProxy (State s) data Writer w a = Writer w a type WRITER w = FProxy (Writer w) newtype Except e a = Except e type EXCEPT e = FProxy (Except e) type FAIL = EXCEPT Unit
The takeaways here:
- As stated above,
purescript-runalready defines and properly handles the types that make the same effects we saw in the
MTLfolder work out-of-box.
ain each type is the output type, so it is excluded.
FAILindicates an error whose type we don't care about.
If we look at some of the functions that each of the above MTL-like types provide, we'll notice another pattern. Each type (e.g.
Reader) seems to define its own
_reader :: Proxy "reader") for the corresponding type in
VariantF's row type (e.g.
However, if one wanted to use a custom
Symbol name for their usage of an MTL-like type (e.g.
Reader), they can append
at to the function and get the same thing. In other words:
liftReader readerObj = liftReaderAt _reader readerObj liftReaderAt symbol readerObj = -- implementation ask = askAt _reader askAt symbol = -- implementation
In short, one can use a
Run-based monad to do two different state computations in the same function, something which the unmodified
MTL approach via
MonadState cannot do.
- See this project's
Hello World/Projects/src/Simplest Programfolder for an example of what a very simple program with a
Run-based architecture looks like.
- A simple program using multiple effects
- A short explanation from Free to Run that also covers
VariantFand whose code can be found in the project's test directory: