This article describes the Free monad in Scala — how it works, what it’s good for and why we need it in the first place.

This piece assumes you’re already very comfortable with Scala. If you know a bit of Cats (which we teach in the Cats course), that’s a bonus, but not required for this article, because we’re going to lay all the groundwork for what we need here.

This post was written for Scala 3, but the concepts work almost identically for Scala 2 if you replace givens with implicits.

1. Introduction

In order to describe the Free monad, we need to understand what a Monad is. We’ve already written several approaches to it:

I encourage you to read at least the first two approaches. Monads are very powerful and form the building blocks for every useful piece of purely functional code. We describe monads as a type class, which takes a generic type argument which is itself generic (a higher-kinded type):

``````trait Monad[M[_]] {
def pure[A](a: A): M[A]
def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B]
}
``````

Monads describe, in essence, two fundamental operations:

• a way to wrap a plain value into a “wrapper” type
• a way to obtain a wrapper type from another, through a specific kind of transformation function

Just from these two operations, we can describe an incredible variety of computations:

• sequential collections, e.g. lists
• potentially absent values, i.e. Options
• computations that might fail, i.e. Try
• potentially failed computations with an error type of our choosing, e.g. Either
• computations that might perform side effects, i.e. IO from Cats Effect
• combinations thereof, e.g. ZIO, which take dependencies, can fail, and can perform side effects in the same computation

The Free monad describes a similar “sequential” capability for a wrapper type and for a well-defined value type.

The Free monad in Scala can be described by a similar type signature, taking a type argument `M[_]` which is the container exhibiting monadic capabilities, and a type argument `A` which is the value type with which that wrapper is defined. In other words, a Free monad trait can be written as

``````trait Free[M[_], A]
``````

Because it’s a monad, we can also write the `pure` and `flatMap` methods, with the exact same meaning. However, since the Free data type has a slightly different signature, the `pure` and `flatMap` will also look a little different:

``````trait Free[M[_], A] {
def pure(a: A): Free[M, A]
def flatMap[B](f: A => Free[M, B]): Free[M, B]
}
``````

A difference from the “regular” monad is that in this case, we shouldn’t really need an instance of Free to build a new instance of Free with the `pure` method. So we’ll move the `pure` method to a companion object, while keeping the `pure` concept intact:

``````trait Free[M[_], A] {
def flatMap[B](f: A => Free[M, B]): Free[M, B]
}

object Free {
def pure[M[_], A](a: A): Free[M, A] = ???
}
``````

We’ll leave the `pure` method unimplemented for now. The important bit is that we have the same `pure` + `flatMap` concepts here as well, even though the methods themselves are moved to different places. Now, because we have `pure` and `flatMap`, we can implement the `map` method for free — as we teach in the Scala with Cats course, the Functor’s fundamental method can be expressed in terms of `pure` and `flatMap`:

``````trait Free[M[_], A] {
// ... the rest of the code
import Free._
def map[B](f: A => B): Free[M, B] = flatMap(a => pure(f(a)))
}
``````

This `map` method will come in handy later.

Besides `pure`, `flatMap` and `map` (which derives from the other two), we also have a function that turns a regular `M[A]` into a Free instance. This function is called “lift”, either named `liftM` or `liftM` in real libraries, and looks like this:

``````object Free {
// ... the rest of the code
def liftM[M, A](ma: M[A]): Free[M, A]
}
``````

That’s it! This is the Free monad.

But… why?

Before we implement an actual Free monad, we’ll take a detour to explain why the Free monad is a useful concept in the first place. We’ll go through an example to understand how the Free monad would be actually used.

3. Why We Need The Free monad

3.1. Writing Programs with The Free Monad

Let’s imagine we’re writing a small tool to interact with a custom database we have at work. For the sake of simplicity, we’ll assume the fundamental operations of the database to be the regular CRUD:

``````trait DBOps[A]
case class Create[A](key: String, value: A) extends DBOps[Unit]
case class Read[A](key: String) extends DBOps[A]
case class Update[A](key: String, value: A) extends DBOps[A]
case class Delete(key: String) extends DBOps[Unit]
``````

This suite of operations (some of which can wrap others in real libraries) bears the fancy name of “algebra”, for the reason that any expression composed out of these fundamental types through some operators (like nesting or regular methods) belong to this group as well.

It’s easy to imagine an example of an interaction with such a database:

• read something from a key, like the name of a person
• change it, e.g. run a `toUppercase` on it
• associate this new value to another key
• delete the old key

This is a sequential suite of operations. Because it’s sequential, a monadic data type to describe all of these operations can prove very useful. However, instead of writing a type class instance for `Monad[DBOps]` and following the tagless-final approach, we’re going to use a Free monad:

``````type DBMonad[A] = Free[DBOps, A]
``````

Because we’re using a Free monad, instead of describing combinators of the data types above, we’re going to “lift” them to Free through smart constructors:

``````def create[A](key: String, value: A): DBMonad[Unit] =
Free.liftM[DBOps, Unit](Create(key, value))

def update[A](key: String, value: A): DBMonad[A] =
Free.liftM[DBOps, A](Update[A](key, value))

Free.liftM(Delete(key))
``````

and with these smart constructors in place, we can immediately imagine an abstract program that does what we said above — read, change, create new entry, delete old entry:

``````  def myLittleProgram: DBMonad[Unit] = for {
_ <- create[String]("123-456", "Daniel")
name <- get[String]("123-456")
_ <- create[String]("567", name.toUpperCase())
_ <- delete("123-456")
} yield ()
``````

This program can be completely described in terms of the Free monad, regardless of what wrapper type we use. The only problem is that it’s a description of a computation; it does not perform any meaningful work in the world, like, you know, interacting with an actual database.

We need something to interpret this program.

What does “interpretation” even mean? Interpreting a program means transforming this abstract program written in terms of the Free monad into another data type that actually performs the computations when evaluated. A good candidate for such a data structure is, for example, the Cats Effect IO. However, we can pick another data type of our choosing, with the meaning of

• potentially absent values: Option
• potentially failed computations: Try
• potentially failed computations with an error type of our choosing: Either
• computations that can perform side effects: IO
• combinations of the above: ZIO
• aggregating values: List

This is why the Free monad has another operation that can “evaluate” an instance of Free to one of these data types. The operation is called `foldMap` and looks like this:

``````trait Free[M[_], A] {
// ... existing code
def foldMap[G[_]: Monad](natTrans: M ~> G): G[A]
}
``````

This one is a bit more complicated. Let’s take each piece in turn.

First of all, what’s a `natTrans` and the `~>` symbol? The concept of “natural transformation” is a higher-kinded Function1 type that looks like this:

``````  trait ~>[F[_], G[_]] {
def apply[A](fa: F[A]): G[A]
}
``````

So instead of a regular Function1 taking value types as type parameters, now we operate at a higher kind. We used this concept in our demonstration for why monads are monoids in the category of endofunctors. Examples of natural transformations in real life include:

• `Try[A].toOption`: this is an example of an implementation of a natural transformation between `Try` and `Option`
• `List[A].headOption` which returns the head of the list, if it exists: an example of an implementation of a natural transformation between `List` and `Option`
• `Option[A].toList`: the reverse

We can abstract away this concept of natural transformation by the `~>` symbol, which looks like a function type at a higher kind.

Back to `foldMap`. Notice that the return value of `foldMap` is `G[A]`, so a different monadic type than `M[_]`, assuming that G “is” a Monad, by the context bound `G[_]: Monad`. This is important, because the evaluation of an instance of Free can only happen if the wrapper type which we’re evaluating to is also a monad, i.e. exhibits monadic behavior.

3.3. Natural Transformations

For our little program, we can interpret it with `foldMap`, if we can create a natural transformation from our type `DBOps` to some other type that will actually perform the actions described by the program. Let’s write a simple IO data type in the style of Cats Effect:

``````case class IO[A](unsafeRun: () => A)
object IO {
def create[A](a: => A): IO[A] = IO(() => a)
}
``````

The IO type encapsulates computations that evaluate to `A`, with potential side effects. The IO data type is a monad, meaning that we can create a Monad instance for it:

``````given ioMonad: Monad[IO] with {
override def pure[A](a: A) =
IO(() => a)
override def flatMap[A, B](ma: IO[A])(f: A => IO[B]) =
IO(() => f(ma.unsafeRun()).unsafeRun())
}
``````

Being a monad, the IO data type is a suitable “evaluator” for our abstract program. The only piece that we need is a natural transformation from `DBOps` to IO. This natural transformation is the logic of evaluating the abstract CRUD operations (for now just data structures) into actual effects in the real world. For that, we’ll need an actual database to store and retrieve data. For this simple example, we’ll assume a simple mutable map, but this can easily be replaced in practice by a real database.

``````val myDB: mutable.Map[String, String] = mutable.Map()
``````

For our particular database, in order to read/write values of type `A`, we’ll also provide some awesome serialization/deserialization API to/from String:

``````def serialize[A](a: A): String = a.toString
def deserialize[A](value: String): A = value.asInstanceOf[A]
``````

Of course, with your own database, the communication protocol will be different, but for this example it’ll be sufficient.

The natural transformation from `DBOps` to our IO data type will need to take the “database” and the “protocol” into account. All we need to do is convert all cases of `DBOps` into proper `IO`s that will perform effects on the database when evaluated, so we can resort to simple pattern matching for this:

``````val dbOps2IO: DBOps ~> IO = new (DBOps ~> IO) {
override def apply[A](fa: DBOps[A]): IO[A] = fa match {
case Create(key, value) => IO.create {
// database insert query - here, just printing
println(s"insert into people(id, name) values (\$key, \$value)")
myDB += (key -> serialize(value))
()
}
println(s"select * from people where id=\$key limit 1")
deserialize(myDB(key))
}
case Update(key, value) => IO.create {
println(s"update people(name=\$value) where id=\$key")
val oldValue = myDB(key)
myDB += (key -> serialize(value))
deserialize(oldValue)
}
case Delete(key) => IO.create {
println(s"delete from people where id=\$key")
()
}
}
}
``````

3.4. Evaluating the Program

With all pieces in place, we can now run the interpreter of our abstract program with `foldMap` and with the natural transformation we’ve just implemented:

``````val ioProgram: IO[Unit] = myLittleProgram.foldMap(dbOps2IO)
``````

This is a single `IO[Unit]` effect which can be passed around, reused, or “run” in main:

``````def main(args: Array[String]): Unit = {
ioProgram.unsafeRun() // PERFORMS THE ACTUAL WORK
}
``````

This will execute the IO effect, which in turn will evaluate our abstract program.

Why have we gone all this way just to insert a bunch of data into a database?

The Free monad is a pattern which allows us to separate

• the description of fundamental operations
• the business logic of our application
• the evaluation of that business logic

In other words, our abstract program is what matters for our application/business logic. We can keep that fixed, and give it different interpreters depending on how our requirements change — e.g. perhaps we want the program to be evaluated asynchronously — or we can do the reverse. This makes it very easy to maintain, because we can work independently on either

• the business logic, while keeping interpreters fixed
• the interpreters, while keeping the business logic fixed
• the fundamental operations and the interpreter(s), while keeping the business logic fixed

So notice the flexibility we get by choosing to work on a piece of the system without affecting the others. Another benefit of this approach is testability, because we can always supply a “testing” monad to evaluate the program, make assertions and ensure the business logic is correct, while the interpreter itself can be independently tested.

You might have noticed that our current code is incomplete. We took a detour to understand the reasons why Free monads are useful and how they help with code decoupling. Let’s move back to the Free monad itself, because our Free type currently looks like this:

``````trait Free[M[_], A] {
def flatMap[B](f: A => Free[M, B]): Free[M, B]
def map[B](f: A => B): Free[M, B] = flatMap(a => pure(f(a)))
def foldMap[G[_]: Monad](natTrans: M ~> G): G[A]
}

object Free {
def pure[M[_], A](a: A): Free[M, A] = ???
def liftM[M[_], A](ma: M[A]): Free[M, A] = ???
}
``````

We’re going to write actual instances of Free as case classes for all fundamental operations:

• pure
• flatMap
• liftM
``````object Free {
// ... existing code
case class Pure[M[_], A](a: A) extends Free[M, A]
case class FlatMap[M[_],A,B](fa: Free[M, A], f: A => Free[M, B]) extends Free[M, B]
case class Suspend[M[_], A](ma: M[A]) extends Free[M, A]
}
``````

The `Pure` type simply wraps a single value of type `A`; we’ll make use of this later when we evaluate `foldMap`. The `FlatMap` case class follows the signature of the `flatMap` method in the Free trait, keeping the Free instance to be transformed and a function to turn an `A` into another instance of `Free[M, B]`. The final case class is `Suspend` which corresponds to the `liftM` method.

With these case classes in place, we can evaluate `pure`, `flatMap` and `liftM` immediately:

``````trait Free[M[_], A] {
import Free.*
def flatMap[B](f: A => Free[M, B]): Free[M, B] = FlatMap(this, f) // added now
def map[B](f: A => B): Free[M, B] = flatMap(a => pure(f(a)))
def foldMap[G[_]: Monad](natTrans: M ~> G): G[A]
}

object Free {
def pure[M[_], A](a: A): Free[M, A] = Pure(a) // added now
def liftM[M[_], A](ma: M[A]): Free[M, A] = Suspend(ma) // added now

case class Pure[M[_], A](a: A) extends Free[M, A]
case class FlatMap[M[_],A,B](fa: Free[M, A], f: A => Free[M, B]) extends Free[M, B]
case class Suspend[M[_], A](ma: M[A]) extends Free[M, A]
}
``````

So the final piece that we need to implement is `foldMap`. The `foldMap` method will now need to evaluate this Free instance, depending on which type this instance belongs to:

• If this instance is a `Pure`, then return a “pure” `G[A]`. Because `G` is a monad, we can do that easily.
• If this instance is a `Suspend`, then take the `M[A]` wrapped inside and turn that into a `G[A]` because we have the natural transformation handy.
• If this instance is a `FlatMap`, then this instance has two fields: another Free instance and a transformation function.
• Run a recursive `foldMap` on the wrapped Free instance, returning a `G[A]`.
• Then `flatMap` that because `G` is a monad, so we can use the Monad instance to do it.

Notice how the presence of a given/implicit `Monad[G]` in scope is crucial here. Our code for `foldMap` is as follows:

``````def foldMap[G[_]](natTrans: M ~> G)(using monadG: Monad[G]): G[A] = this match {
case Suspend(ma) => natTrans.apply(ma)
case FlatMap(fa, f) =>
}
``````

Here, we’ve slightly changed the signature of `foldMap` so that we can make use of the given `Monad[G]`, but we can also keep the old signature and add a summoning method for Monad:

``````// alternative
}

trait Free[M[_], A] {
// ... existing code

def foldMap[G[_]: Monad](natTrans: M ~> G): G[A] = this match {
case Suspend(ma) => natTrans.apply(ma)
case FlatMap(fa, f) => // need a G[B]
}
}
``````

5. Final Code

The full program with all pieces put together looks like this:

``````object FreeMonad {
def pure[A](a: A): M[A]
def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B]
}

}

trait ~>[F[_], G[_]] {
def apply[A](fa: F[A]): G[A]
}

trait Free[M[_], A] {
import Free.*
def flatMap[B](f: A => Free[M, B]): Free[M, B] = FlatMap(this, f)
def map[B](f: A => B): Free[M, B] = flatMap(a => pure(f(a)))
def foldMap[G[_]: Monad](natTrans: M ~> G): G[A] = this match {
case Suspend(ma) => natTrans.apply(ma)
case FlatMap(fa, f) => // need a G[B]
}
}

object Free {
def pure[M[_], A](a: A): Free[M, A] = Pure(a)
def liftM[M[_], A](ma: M[A]): Free[M, A] = Suspend(ma)

case class Pure[M[_], A](a: A) extends Free[M, A]
case class FlatMap[M[_],A,B](fa: Free[M, A], f: A => Free[M, B]) extends Free[M, B]
case class Suspend[M[_], A](ma: M[A]) extends Free[M, A]
}

// sequence computations as data structures, THEN attach the monadic type at the end
// "algebra"
trait DBOps[A]
case class Create[A](key: String, value: A) extends DBOps[Unit]
case class Read[A](key: String) extends DBOps[A]
case class Update[A](key: String, value: A) extends DBOps[A]
case class Delete(key: String) extends DBOps[Unit]

// definitions - fancier algebra

// "smart" constructors
def create[A](key: String, value: A): DBMonad[Unit] =
Free.liftM[DBOps, Unit](Create(key, value))

def update[A](key: String, value: A): DBMonad[A] =
Free.liftM[DBOps, A](Update[A](key, value))

Free.liftM(Delete(key))

_ <- create[String]("123-456", "Daniel")
name <- get[String]("123-456")
_ <- create[String]("567", name.toUpperCase())
_ <- delete("123-456")
} yield () // description of a computation

// evaluate the program - interpreter/"compiler"
// IO
case class IO[A](unsafeRun: () => A)
object IO {
def create[A](a: => A): IO[A] = IO(() => a)
}

override def pure[A](a: A) = IO(() => a)
override def flatMap[A, B](ma: IO[A])(f: A => IO[B]) =
IO(() => f(ma.unsafeRun()).unsafeRun())
}

val myDB: mutable.Map[String, String] = mutable.Map()
// TODO replace these with some real serialization
def serialize[A](a: A): String = a.toString
def deserialize[A](value: String): A = value.asInstanceOf[A]

// nat trans DBOps -> IO
val dbOps2IO: DBOps ~> IO = new (DBOps ~> IO) {
override def apply[A](fa: DBOps[A]): IO[A] = fa match {
case Create(key, value) => IO.create { // actual code that uses the database
println(s"insert into people(id, name) values (\$key, \$value)")
myDB += (key -> serialize(value))
()
}
println(s"select * from people where id=\$key limit 1")
deserialize(myDB(key))
}
case Update(key, value) => IO.create {
println(s"update people(name=\$value) where id=\$key")
val oldValue = myDB(key)
myDB += (key -> serialize(value))
deserialize(oldValue)
}
case Delete(key) => IO.create {
println(s"delete from people where id=\$key")
()
}
}
}

val ioProgram: IO[Unit] = myLittleProgram.foldMap(dbOps2IO)

def main(args: Array[String]): Unit = {
ioProgram.unsafeRun() // PERFORMS THE ACTUAL WORK
}
}
``````

6. Conclusion

In this article, we did a comprehensive overview of what a Free monad is, why it’s useful, how it helps us decouple our code, how it adds flexibility and testability in our programs, and we implemented our own simple version of a Free monad along with an example abstract program and an interpreter for it in the style of Cats Effect.

Use it well!

Tags:

Updated: