Deriving the Free Monad

The free monad is defined by this structure1:

sealed trait Free[F[_], A]
final case class Return[F[_], A](a: A) extends Free[F, A]
final case class Suspend[F[_], A](s: F[Free[F, A]]) extends Free[F, A]

We can use the free monad without understanding its implementation, but to really understand it we need to know why it is defined this way.

It certainly wasn’t obvious to me why this is the correct definition, and reading the literature quickly devolved into “doughnoids in the category of pretzelmorphisms” land. Here I want to present an explanation aimed at programmers that doesn’t involve abstract alphabet-soup.

Preliminaries

The free monad represents the minimal possible structure to implement a monad and nothing else. This is achieved by separating the structure of monadic computations from the process that gives them meaning. The free monad gives us the means to construct a monad from any type (that is also a functor)2 by wrapping it in the free monad.

Let me give a simple example of this separation of structure and meaning that doesn’t involve any monads. Consider the expression

1 + 2 + 3

When we write this expression we bundle the structure of the computation (two additions) with the meaning given to that computation (Int addition).

We could separate structure and meaning by representing the structure of the computation as data, perhaps as3

Add(1, Add(2, 3))

Now we can write a simple interpreter to give meaning to this structure. Having separated the abstract syntax tree from the interpreter we can choose different interpretations for a given tree, such as computing with dual numbers to automatically compute derivatives, or running the code on a GPU for performance.

The free monad is just an abstract syntax tree representation of a monad. It has the advantage that we can define custom interpreters for the computations represented in the free monad, and with some further tricks compose monads and interpreters4.

We should be able to derive the free monad from the operations required to define a monad, as it is just a representation of those operations as a data structure. Before we dive into the free monad, however, I want to return to our example of addition and derive the free monoid. This serves as a useful warmup before we tackle the free monad.

The Free Monoid

Our goal with implementing the free monoid is to represent computations like

1 + 2 + 3

in a generic way without giving them any particular meaning.

The free monoid will wrap an arbitrary type and must itself be a monoid.

A monoid for some type A is defined by:

  1. an operation append with type (A, A) => A; and
  2. an element zero of type `A

The following laws must also hold:

  1. append is associative, meaning append(x, append(y, z)) == append(append(x, y), z) for all x, y, and z, in A.
  2. zero is an identity of append, meaning append(a, zero) == append(zero, a) == a for any a in A.

The monoid operations (append and zero) suggest we want a structure something like

sealed trait FreeMonoid[+A]
final case object Zero extends FreeMonoid[Nothing]
final case class Append[A](l: A, r: A) extends FreeMonoid[A]

but this doesn’t work – we can’t write, for instance, Append(Zero, Zero) because the types don’t line up.

We can use a structure like

sealed trait FreeMonoid[+A]
final case object Zero extends FreeMonoid[Nothing]
final case class Value[A](a: A) extends FreeMonoid[A]
final case class Append[A](l: FreeMonoid[A], r: FreeMonoid[A]) extends FreeMonoid[A]

Now we can represent 1 + 2 + 3 as

Append(Value(1), Append(Value(2), Value(3)))

This is not the simplest representation we can use. With a bit of algebraic manipulation, justified by the monoid laws, we can normalize any monoid expression into a form that allows for a simpler representation. Let’s illustrate this via algebraic manipulation on 1 + 2 + 3.

The identity law means we can insert the addition of zero in any part of the computation without changing the result, and likewise we can remove any zeros (unless the entire expression consists of just zero). We’re going to decree that any normalized expression must have a single zero at the end of the expression like so:

1 + 2 + 3 + 0

The associativity law means we can place brackets wherever we want. We’re going to decide to bracket expressions so traversing the expression from left to right goes from outermost to innermost bracket, like so:

(1 + (2 + (3 + 0)))

With these changes – which by the monoid laws make no difference to the meaning of the expression – we can construct the following abstract syntax tree.

sealed trait FreeMonoid[+A]
final case object Zero extends FreeMonoid[Nothing]
final case class Append[A](l: A, r: FreeMonoid[A]) extends FreeMonoid[A]

We can represent 1 + 2 + 3 (normalized to (1 + (2 + (3 + 0)))) as

Append(1, Append(2, Append(3, Zero)))

The final step is to recognise that this structure is isomorphic (in the real, not the Javascript, sense) to List. So we could just as easily write

1 :: 2 :: 3 :: Nil

or

List(1, 2, 3)

Our final step is to make sure that List itself a monoid. It is. The monoid operations on List are:

  • append is ++, list concatentation;
  • zero is Nil, the empty list; and
  • we can “lift” any type into the free monoid using List.apply

High fives all around – we’ve derived the free monoid from first principles.

The Free Monad

We are now ready to tackle the free monad. We can take the same approach starting with the monad operations point and flatMap, but our task will be easier if we reformulate monads in terms of point, map, and join. Under this formulation a monad for a type F[_] has:

  • an operation point with type A => F[A];
  • an operation join with type F[F[A]] => F[A]; and
  • an operation map with type (F[A], A => B) => F[B].

From this list of operations we can start to create an abstract syntax tree. We start with the definition of Free.

sealed trait Free[F[_], A]

We can directly convert point into a case Return (following the names I introduced in the introduction).

final case class Return[F[_], A](a: A) extends Free[F, A]

We are going to convert join into a case Suspend. What is the type of the value we store inside Suspend? We might think it should store a value of type F[F[A]] but if we did this we wouldn’t be able to store, say, a Return inside the outer F. We can break it down like this:

  • The inner F[A] will be represented by an instance of the free monad, and thus has type Free[F, A].
  • The outer F[_] will be wrapped in the Suspend we’re creating.

Therefore the value we should store has type F[Free[F, A]] giving us

final case class Suspend[F[_], A](f: F[Free[F, A]]) extends Free[F, A]

Finally we have map. This suggests a case like5

final case class Map[F[_], A, B](fa: Free[F, A], f: A => B) extends Free[F, B]

This looks a little problematic. We have three type parameters while Free only has two. In fact we can do away with this case! We inherit map from monad being a functor. A map represents a pure, not an effectful, computation. We don’t need to represent Map in the free monad abstract syntax tree so long as we can implement the map operation in our free monad.

Our final free monad data type looks like

sealed trait Free[F[_], A]
final case class Return[F[_], A](a: A) extends Free[F, A]
final case class Suspend[F[_], A](s: F[Free[F, A]]) extends Free[F, A]

This is what we saw in the introduction. But does it really work? To show it does, let’s implement the monad operations on this data type. We’ll use the more familiar flatMap and point formulation, which is better suited to Scala, than the point, join, and map formulation above.

We can knock out point easily enough.

object Free {
  def point[F[_]](a: A): Free[F, A] = Return[F, A](a)
}

Things get a bit trickier with flatMap, however. Since we know Free in an algebraic data type we can easily get the structural recursion skeleton.

sealed trait Free[F[_], A] {
  def flatMap[B](f: A => Free[F, B]): Free[F, B] =
    this match {
      case Return(a)  => ???
      case Suspend(s) => ???
    }
}

The case for Return just requires us to follow the types.

sealed trait Free[F[_], A] {
  def flatMap[B](f: A => Free[F, B]): Free[F, B] =
    this match {
      case Return(a)  => f(a)
      case Suspend(s) => ???
    }
}

The case for Suspend is a bit trickier. The value s has type F[Free[F, A]]. The only operation we (currently) have available is f, which accepts an A. We could flatMap f over the Free[F, A] wrapped in F, but we haven’t yet required any operations on F. If we require F is a functor we can then map over it. Concretely, we can use this code snippet:

s map (free => free flatMap f)

A bit of algebra shows the result has type F[Free[F, B]], and we can wrap that in a Suspend to get a result of type Free[F, B]. Our final implementation is thus

sealed trait Free[F[_], A] {
  def flatMap[B](f: A => Free[F, B])(implicit functor: Functor[F]): Free[F, B] =
    this match {
      case Return(a)  => f(a)
      case Suspend(s) => Suspend(s map (_ flatMap f))
    }
}

We can write map in terms of flatMap

def map[B](f: A => B)(implicit functor: Functor[F]): Free[F, B] =
  flatMap(a => Return(f(a)))

It’s left as an exercise to the reader to prove the monad laws hold.

Conclusions

In this blog post I’ve tried to explain how the free monad comes to be without invoking category theory. Hopefully this sheds a bit more light on the construction, and shows it’s a natural consequence of the monad operations.

There is a lot more to the free monad than just constructing it – using it is rather important too. I’ve linked to a few more ideas in the footnotes, but I have another blog post the describes why the free monad is interesting. Finally, our book Essential Interpreters has complete coverage of the free monad (at least it will, when we’ve written it!)

  1. There are other ways of defining the free monad, but this is the most common in my reading.

  2. The free monad requires that we wrap it around a functor. There is another trick, called the Coyoneda, that allows us to turn any type into a functor. This allows us to wrap any type with the free monad (by first constructing a Coyoneda functor for it). In this discussion we’re not going to cover the Coyoneda so for our purposes the free monad can only be wrapped around a functor.

  3. This data structure can’t actually be implemented. The right-hand element of Add is an Add in one case and an Int in another. We’ll see how to actually implement this in the next section.

  4. This extension is described in Data Types a la Carte and eventually will be described in Essential Interpreters.

  5. If you look at the Scalaz implementation of free monads you see a case very much like this called GoSub. This actually represents flatMap (read the types) but it isn’t strictly necessary if we’re not also implementing trampolining as Scalaz’s implementation does.


Like what you're reading?

Join our newsletter

Looking for a Scala job?

Job Listings


Comments

Please review our Community Guidelines before posting a comment. We encourage discussion in good faith, but do not allow combabitive, exclusionary, or harassing behaviour. If you have any questions, contact us!