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

  • 969