Everything you always wanted to know about
Free Monads ...
but were afraid to ask
Marcin Rzeźnicki
mrzeznicki@iterato.rs
... we use this stuff
What's Free?
A way to pack a sequential computation into a data structure, so you can inspect that data structure and “interpret” it later
It’s called “free” because you get a monad for free for any higher-kinded * -> * type
What's Free?
Free [F[_], A]
Description of a program
Operational algebra (DSL)
Type of value produced by program
What's Free?
In this interpretation - F[A] is the set of operations the program can be reduced to (algebra) and A is the value that will be produced by the program (unless it halts or runs forever).
Free monads let us model programs as a sequence of algebraic operations that describe the semantics of our program.
Such descriptions of programs can be introspected (one step at a time), interpreted, and transformed.
Implementation (cats)
"pack a sequential computation into a data structure"
sealed abstract class Free[S[_], A]
/**
* Return from the computation with the given value.
*/
private final case class Pure[S[_], A](a: A) extends Free[S, A]
/** Suspend the computation with the given suspension. */
private final case class Suspend[S[_], A](a: S[A]) extends Free[S, A]
/** Call a subroutine and continue with the given function. */
private final case class Gosub[S[_], B, C](c: Free[S, C], f: C => Free[S, B])
extends Free[S, B]
Example
Algebra
sealed trait KVStoreA[A]
case class Put[T](key: String, value: T) extends KVStoreA[Unit]
case class Get[T](key: String) extends KVStoreA[Option[T]]
case class Delete(key: String) extends KVStoreA[Unit]
type KVStore[A] = Free[KVStoreA, A]
Example
Algebra
def get[T](key: String): KVStore[Option[T]] =
Free.pure(None)
def get[T](key: String): KVStore[Option[T]] =
Free.liftF(Get[T](key))
// Update composes get and set, and returns nothing.
def update[T](key: String, f: T => T): KVStore[Unit] =
for {
vMaybe <- get[T](key)
_ <- vMaybe.map(v => put[T](key, f(v))).getOrElse(Free.pure(()))
} yield ()
Pure
Suspend
Gosub(Get[T](key), ...)
Implementation (cats)
"programs can be introspected (one step at a time)"
@tailrec
final def step: Free[S, A] = this match {
case Gosub(Gosub(c, f), g) => c.flatMap(cc => f(cc).flatMap(g)).step
case Gosub(Pure(a), f) => f(a).step
case x => x
}
"programs can be interpreted"
final def foldMap[M[_]](f: NaturalTransformation[S,M])(implicit M: Monad[M]): M[A] =
step match {
case Pure(a) => M.pure(a)
case Suspend(s) => f(s)
case Gosub(c, g) => M.flatMap(c.foldMap(f))(cc => g(cc).foldMap(f))
}
Implementation (cats)
"programs can be transformed"
final def mapSuspension[T[_]](f: NaturalTransformation[S,T]): Free[T, A] =
foldMap[Free[T, ?]] {
new NaturalTransformation[S, Free[T, ?]] {
def apply[B](fa: S[B]): Free[T, B] = Suspend(f(fa))
}
}(Free.freeMonad)
Composition
- if you have algebras f and g, you can compose them into a composite algebra Coproduct[F, G]
final case class Coproduct[F[_], G[_], A](run: F[A] Xor G[A])
def inject[F[_], G[_]](fa: F[A])(implicit I : Inject[F, G]): Free[G, A] = Free.liftF(I.inj(fa))
Inject can always be constructed for Coproduct
Composition
- if you can interpret f and g separately into some h, you can interpret the coproduct of their algebras into h (vertical composition)
trait NaturalTransformation[F[_], G[_]] {
def or[H[_]](h: NaturalTransformation[H,G]): NaturalTransformation[Coproduct[F, H, ?],G] =
new (NaturalTransformation[Coproduct[F, H, ?],G]) {
def apply[A](fa: Coproduct[F, H, A]): G[A] = fa.run match {
case Xor.Left(ff) => self(ff)
case Xor.Right(gg) => h(gg)
}
}
}
Composition
- if you can interpret f into g, and g into h, then you can interpret f into h (horizontal composition)
trait NaturalTransformation[F[_], G[_]] {
def apply[A](fa: F[A]): G[A]
def compose[E[_]](f: NaturalTransformation[E, F]): NaturalTransformation[E, G] =
new NaturalTransformation[E, G] {
def apply[A](fa: E[A]): G[A] = self.apply(f(fa))
}
def andThen[H[_]](f: NaturalTransformation[G, H]): NaturalTransformation[F, H] =
f.compose(self)
}
Free as a monad
Because Free is a monad
/**
* `Free[S, ?]` has a monad for any type constructor `S[_]`.
*/
implicit def freeMonad[S[_]]: Monad[Free[S, ?]] =
new Monad[Free[S, ?]] {
def pure[A](a: A): Free[S, A] = Free.pure(a)
override def map[A, B](fa: Free[S, A])(f: A => B): Free[S, B] = fa.map(f)
def flatMap[A, B](a: Free[S, A])(f: A => Free[S, B]): Free[S, B] = a.flatMap(f)
}
Free as a monad
... you can teach programs to do tricks for free (via monad transformers)
type DSL[_]
type Program[A] = Free[DSL, A]
type ProgramEx[A, Ex] = XorT[Program, Ex, A]
type LoggingProgram[A] = WriterT[Program, Log, A]
type ProgramWithEnv[A] = ReaderT[Program, Env, A]
Throw "exceptions"
Logging
Reading environment
Free - summary
In modern FP, we shouldn’t write programs — we should write descriptions of programs, which we can then introspect, transform, and interpret at will.
This leads to a style of programming where a large program is deconstructed into layers. These layers represent different levels of abstraction and different concerns.
Ultimately, all these “layers” are compiled into something
Free in applications
1. Start with business logic and turn these into algebra that you will later write interpreters for
2. In parallel to building a business-level DSL maintain "internal" DSL for dealing with Free as a metaphore of "program"
3. HTTP resource receives a request and determies which `Free` program to call.
4. Wire HTTP layer with appropriate interpreters for each program it may need to execute (service layer)
5. After the program has been interpreted, resource inspects the result and turns it into HTTP response.
Free in applications
Services (interpreters)
DB repositories etc
HTTP layer
Programs
determines program to call
sends program to be interpreted
uses during interpretation
Free in applications
Pls send me the codez!
freem
By Marcin Rzeźnicki
freem
- 1,070