Explaining Run

This folder will explain the basic idea of purescript-run and then solve the paper's version of the Expression Problem using it.

What is Run?

If you recall, xgrommx mentions purescript-run in a comment in ADT8.purs. (The ReadMe of this library provides an overview of the ideas we've explained here.)

What is purescript-run? Why would we use that over Free? There are three reasons.

First, let's look at the type of Run :

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 CoProduct type: VariantF.

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

In short, 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 monad, 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. StateT, ReaderT, 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)

Comparing Run to Free and MTL

Free and Run: Some Core Functions Compared

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`
  ∷ ∀ 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

Naming Conventions for Effects

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.

Similarities to MTL

Type Aliases

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-run already defines and properly handles the types that make the same effects we saw in the MTL folder work out-of-box.
  • The a in each type is the output type, so it is excluded.
  • FAIL indicates an error whose type we don't care about.

MTL-Like Functions

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 Symbol (e.g. _reader :: Proxy "reader") for the corresponding type in VariantF's row type (e.g. READER).

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.

Examples of MTL-Like Run-Based Code