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
- Free and Freer Monads: Putting Monads Back into Closet
- Why Free Monads Matter
- Deriving the Free Monad
- Free Monads in the Wild - OCaml Edition
- What does Free buy us?
- Jeremy Mikkola's Cheatsheet: Free Monad
- Free monad in 7 steps
Some subtleties around the sensationalist hype about Free Monads:
6.1. Examples in OCaml
- Nick Vanderweit's Free monads in OCaml