\( \DeclareMathOperator{\tr}{tr} \newcommand\D{\mathrm{d}} \newcommand\E{\mathrm{e}} \newcommand\I{\mathrm{i}} \newcommand\bigOh{\mathcal{O}} \newcommand{\cat}[1]{\mathbf{#1}} \newcommand\curl{\vec{\nabla}\times} \newcommand{\CC}{\mathbb{C}} \newcommand{\NN}{\mathbb{N}} \newcommand{\QQ}{\mathbb{Q}} \newcommand{\RR}{\mathbb{R}} \newcommand{\ZZ}{\mathbb{Z}} % For +---- metric \newcommand{\BDpos}{} \newcommand{\BDneg}{-} \newcommand{\BDposs}{\phantom{-}} \newcommand{\BDnegg}{-} \newcommand{\BDplus}{+} \newcommand{\BDminus}{-} % For -+++ metric \newcommand{\BDpos}{-} \newcommand{\BDposs}{-} \newcommand{\BDneg}{} \newcommand{\BDnegg}{\phantom{-}} \newcommand{\BDplus}{-} \newcommand{\BDminus}{+} \)
UP | HOME

Free Monads - SML

Table of Contents

1. Overview

The basic idea underlying free monads is to start with an endofunctor \(f\) and then turn it into a monad, just as the free group starts with a set then turns it into a group.

Free monads are useful when it comes into describing side-effects monadically.

Haskell takes this idea and runs with it.

We will see how this works in Standard ML.

2. Functors in SML

A Functor (not to be confused with the builtin functor keyword in the module language) is a structure adhering to the following signature:

signature FUNCTOR = sig
  type 'a t;
  val map : ('a -> 'b) -> 'a t -> 'b t;
end;

That's all we need. There are two functor laws, which are inspired by category theory. If F is a functor, then…

  • unit law: F.map id == id
  • preserves composition: F.map (g o f) = (F.map g) o (F.map f)

Ta-duh!

(See also the dedicated discussion about functors on its own page.)

3. Derivation Using the Free Monoid

A monad is a monoid in the category of endofunctors, and a free monad is just the free monoid in that category. We can leverage this information to deduce the structure of the join function for a free monad.

The free monoid is precisely the list. (When the free monoid is generated by a singleton, it's isomorphic to the natural numbers.) The analogy with free monads should be:

datatype 'a list =          nil | cons of 'a * 'a list;
(* structure F : FUNCTOR *)
datatype 'a Free = Return of 'a | Join of ('a Free) F.t;

The intuition is that we have Join apply F iteratively. Zero iterates would be the Return data constructor.

Free monoids have an append function (which, for lists, is…the append function). The analogous function for monads would be the join : 'a F.t F.t -> 'a F.t. Similarly, monoids have an empty constant, which corresponds to the return function for monads in "programmer argot".

The free monoid of iterates of F would then correspond to a type of the form

datatype 'a Free = Zero of 'a | Succ of 'a Free F.t;

The empty "constant" would be empty : 'a -> 'a Free and it's given precisely by the Zero constructor.

The append operation would have its type signature be append : 'a Free Free -> 'a Free. This would send append (Zero x) = x since x : 'a Free, and then the only question is how it would behave on Succ x?

Well, if we considered Succ x : 'a Free Free, then x : 'a Free Free F.t. We see that F.map append : 'a Free Free F.t -> 'a Free F.t, and so it's obvious applicable to x. However the resulting type is not 'a Free. But it is the correct type to be fed into the Succ data constructor. This amounts to "flattening" the "leafs" of x. This suggests append (Succ x) = Succ (F.map append x) is the correct definition.

This append function is the join function for the free monad:

fun append (Zero x) = x
  | append (Succ x) = Succ (F.map append x);

val join = append;

From the join function (and the functor gives us a map function), we can infer the bind function immediately.

4. Free Monads

4.1. Monad signature

Just to review, we have the following signature for monads in Standard ML:

signature MONAD = sig
  include FUNCTOR;
  val return : 'a -> 'a t;
  val bind : 'a t -> ('a -> 'b t) -> 'b t;
  val join : 'a t t -> 'a t;
end;

We may or may not need the exec function to perform the computation abstracted away by the monad.

4.2. Free Monad Signature

A free monad will take a Functor, and turn it into a monad. The Functor is kept as a field of the structure.

Intuitively, a free monad is generated by a "join" operation (or "multiplication") operation. It suffices to describe this using a datatype with Return of 'a and Join of 'a t F.t, so the operations "stack" as we'd expect from a free gadget.

We then have the following signature:

signature FREE_MONAD = sig
  structure F : FUNCTOR;
  (* probably should rename the constructors to the more
  intuitive RETURN of 'a | JOIN of ('a t) F.t
  since $\mu$ corresponds to the `join` function for a monad.
  This `JOIN` constructor wraps the "outer" functor as a
  placeholder. *)
  datatype 'a t = Return of 'a
                | Join of ('a t) F.t;
  val return : 'a -> 'a t;
  val bind : 'a t -> ('a -> 'b t) -> 'b t;
  val join : 'a t t -> 'a t;
  val map : ('a -> 'b) -> 'a t -> 'b t;
  val lift : 'a F.t -> 'a t;
  (* lift is analogous to (fn x => [x]) *)
  (* fun lift x = Join (F.fmap Return x); *)

  (* val fold : ('a F.t -> 'a) -> 'a m -> 'a; *)
(* fun fold _ (Return a) = a
     | fold f (Join x) = f (F.fmap (fold f) x);
*)
end;

Note that we need to "hack" the SML module system to make this a functor. The type alias type 'a t = 'a m accomplishes this. See Bob Harper's Programming in SML, Section 18.1, for details. (We could have written something like include MONAD; include FUNCTOR where type 'a t = 'a m in the signature, but it would not change the implementation as a structure or functor).

Here Return and Join are [data] constructors for the free monad generated by the Functor F.

The bind function (which tells us everything we'd want to know about a monad) is the "default implementation" in terms of Join and F.map with some subtlety. Normally we would have x >>= f = join (F.map f x) but we need to "propagate" the (>>= f) down; i.e., we would have join (F.map (fn m => bind m f) x) be the bind function.

The bind implementation amounts to more of a map than anything else. (In fact, we need to patch this up later on if we want to use Free(F) as a monad; we can redefine fun bind m f = join (map f m) at the end of the functor structure's body to avoid this.)

Another way to approach this is to first implement the map function, then the join function, and then the bind function follows as the default implementation as bind m f = join (map f m) (see, e.g., Bartosz Milewski's Category Theory for Programmers, section 20.2).

Also observe we include the where type... clause to make public the underlying functor's type:

functor Free(F : FUNCTOR) :> FREE_MONAD
                               where type 'a F.t = 'a F.t
                                 = struct
  structure F = F;
  datatype 'a t = Return of 'a
                | Join of ('a t) F.t;
  (* return : 'a -> 'a t *)
  fun return x = Return x;

  fun map f (Return a) = Return (f a)
    | map f (Join x) = Join (F.map (map f) x);

  fun join (Return x) = x
    | join (Join x) = Join (F.map join x);

  fun bind m f = join (map f m);
  
  (* lift : 'a F.t -> 'a t *)
  fun lift f = Join (F.map return f);
end;

This is actually a bit more explicit than the Haskell type signatures, since Haskell does not distinguish which instance each fmap comes from.

4.3. Example: IO

We can use the free monad to describe a simple IO monad. Where to start?

First we need to construct a functor with the various operations we'd like to perform. For now, simply printing a string to the screen and reading a string from stdIn suffices.

We encode this as a datatype by making each operation with a signature like Foo : A -> B -> C -> Z IO.t corresponds to a data constructor FooOp of A * B * C * (Z -> 'a). This is the idiom for using free monads in functional programming: we have a functor describing the syntax tree for an embedded DSL, and we translate the type signature for each operation in the DSL into a type for the data constructor's payload as we have just sketched.

One caveat: if Z = unit (as would happen with print_line), then we can replace Z -> 'a with just 'a.

There is some difficulty with IORef operations. The operations would be:

  • newIORef : 'a -> ('a IORef) IO.t
  • readIORef : 'a IORef -> 'a IO.t
  • writeIORef : 'a IORef -> 'a -> unit IO.t

The type variable 'a appearing in these operations are not quantified, and that causes problems. This requires working with multiple bound type variables datatype ('a,'b) t = ..., which then will not match the expected signature for a functor.

(* A functor describing the operations with side-effects in IO

In general, an operation `Foo : A -> B -> C -> Z IO.t` would
be encoded by a data constructor `FooOp of A * B * C * (Z -> 'a)`.

We also treat `unit -> 'a` as interchangeable with `'a`.
 *)
structure IO_Op = struct
  datatype 'a t = Print of (string * 'a)
                | Read of (string -> 'a);
  fun map f (Print (s,k)) = Print (s, f k)
    | map f (Read k) = Read (fn s => f (k s));
end;

We will also have a read_line function which we need to implement (it's straightforward):

fun read_line () =
  case TextIO.inputLine TextIO.stdIn of
      SOME s => s
    | NONE => "";

Now we can put all these pieces together to form the free monad describing IO side effects we prescribed:

local
  structure FreeIO = Free(IO_Op);
in
  structure Job : sig
      datatype t = datatype FreeIO.t
      val bind: 'a t -> ('a -> 'b t) -> 'b t
      val join: 'a t t -> 'a t
      val map: ('a -> 'b) -> 'a t -> 'b t
      val return: 'a -> 'a t
      val unsafePerform: 'a t -> 'a
  end = struct
    open FreeIO;
    (* datatype replication to make the constructors public *)
    datatype t = datatype FreeIO.t;
    (* execute the monad *)
    fun unsafePerform (Return x) = x
      | unsafePerform (Join f) =
        case f of
            (IO_Op.Print (s, k)) => (print s;
                                     unsafePerform k)
          | (IO_Op.Read k) => unsafePerform (k (read_line()));
  end
end;

4.4. Example usage of IO Free Monad

We can now use the previous machinations in a toy example:

infix >> >>=
fun ((m : 'a Job.t) >>= (n : 'a -> 'b Job.t)) = Job.bind m n;
fun (m >> n) = Job.bind m (fn _ => n);

fun print_string msg =
  Job.Join (IO_Op.Print (msg, Job.Return ()));
val read_string =
  Job.Join (IO_Op.Read (fn msg => Job.Return msg));
fun main () =
  (print_string "What's your name? ") >>
    (read_string >>= (fn name => 
                       (print_string "Hello, ") >>
                         (print_string name)));

Job.unsafePerform (main ());

This isn't terribly different than the IO Monad implemented in Standard ML as found in Andrew Gordon's PhD dissertation.

5. Concluding Remarks

The Free monad is used to decompose an embedded DSL, but in Haskell it comes at the cost of losing a lot of compiler optimizations. So instead it's preferable to use the MTL monad transformer library.

The other use for Free monads is to containerize side-effects (or other effects).

6. References

6.1. Examples in OCaml

Last Updated 2025-02-27 Thu 07:18.