Free Monads in the Wild - OCaml Edition

OCaml programmers don’t seem to resort to free monads much. I like to imagine that this is the case because we’re a practical bunch. But it could simply be that this technique like other monads is a bit heavyweight syntactically, let alone the performance implications it might have.

Regardless, the pattern does seem to occur from time to time. It’s worth pointing it out when it happens, to raise awareness for it. I know I still fail to see opportunities when it is useful. At least I managed to recognize this pattern when I encountered it in opam’s source code.

OPAM’s Job Module

OPAM has a convenient process management module called OpamProcess. The workhorse in it is the Job.Op sub module. Where it defines the following primitive for running jobs:

type 'a job =
  | Done of 'a
  | Run of command * (result -> 'a job)

where command, and result are both just mundane records that include details such as the actual command to run, arguments, the environment, cwd, status code returned, stdout, stderr, etc. OpamProcess even includes a val run : command -> result function that lets you go from one type to the other by just executing the command.

The module also contains a few handy combinators for manipulating these jobs:

val (@@>): command -> (result -> 'a job) -> 'a job
val (@@+): 'a job -> ('a -> 'b job) -> 'b job
val (@@|): 'a job -> ('a -> 'b) -> 'b job

OPAM’s source contains lengthy documentation about these functions. But an experienced reader will notice that this is just map, bind, and a smart constructor for Run.

As an example, let’s use this module to write a simple job that will recursively find all ml files in your cwd, and put them in mls.tar. A bit contrived, but bear with me.

open OpamProcess

let tar_mls =
  let open Job.Op in
  (* passing dir is required for stdout to be saved. This will create
     temp files that opam will clean on its own *)
  let command cmd args = command ~verbose:false ~dir:"." ~name:cmd cmd args in
  let all_mls =
    (command "find" ["." ; "-iname" ; "*.ml"])
    @@> (fun r -> Done r.r_stdout) in
  all_mls @@+ (fun mls ->
      (command "tar" (["-cf" ; "mls.tar"] @ mls))
      @@> (fun r -> Done r.r_code)
    )

Kind of ugly, although I blame that mostly on opam’s selection of combinator names. A more standard set of combinators, say >>= and >>| for bind and map respectively would make this code shed a lot of indentation and useless names. A few more seemingly absent functions are these staples from Control.Monad:

val sequence : 'a job list -> 'a list job
val mapM : ('a -> 'b job) -> 'a list -> 'b list job
val foldM : ('b -> 'a -> 'b job) -> 'b -> 'a list -> 'b job

The functions would improve usability quite a lot, but OpamProcess isn’t a part of any standard lib. So it’s understandable why it wants to just cater to its own internal use cases.

Anyhow, let’s run this (at your own risk!) to convince ourselves this does something. Note that running the job requires calling val run : 'a Op.Job -> 'a like this:

let () = ignore (Job.run tar_mls)

What’s the point of this ceremony? We could have just as easily written this directly by running commands and inspecting their output. What we’re gaining here is the separation of the computational content from the effects that it produces. Put another way, this is the essence of the interpreter pattern. We build up a pure computation, and use an interpreter (in this case run) to force the effects. One of the main benefits is the ability to implement different interpreters for the same computation.

Consider:

val dry_run : 'a Op.job -> 'a

This will execute our computation without actually running any commands but will instead pass a dummy result to subsequent commands.

The possibilities are endless here, you can have interpreters that log commands and results, inject environment variables, run every command with nice, etc. Testability is another strong feature of this technique. It should be easy to “mock” run every command and return some dummy output in anticipation of a particular result.

Generalizing to Free Monads

First, let me put up a disclaimer that I’m definitely the wrong person to teach the theory and fundamentals of free monads. I have neither the pedagogical skill nor the deep understanding required for this. But I do hope that this post will whet one’s appetite to look further into this subject.

Let’s quickly recall what a free monad is. A free monad is a monad construction out of a functor. Where functor refers to the haskell functor type class or the following module signature - not be confused with a parameterized module. I will try to clarify this unfortunate overlap of terms throughout this post.

module type Functor = sig
  type 'a t
  val map : ('a -> 'b) -> 'a t -> 'b t
end

to construct a free monad out of a functor, we can use an OCaml functor. A free monad is of course a monad so we’ll define the appropriate bind (>>=) and return operations. Although I will construct a val join : 'a t t -> 'a t and a val map : ('a -> 'b) -> 'a t -> 'b t instead since in my opinion, the definition is simpler.

module Free (F : Functor) = struct
  type 'a t =
    | Return of 'a
    | Free of ('a t) F.t

  let return a = Return a

  let rec map f = function
    | Return a -> Return (f a)
    | Free a -> Free (F.map (map f) a)

  let rec join = function
    | Return a -> a
    | Free f -> Free (F.map join f)

  let lift f = Free (F.map return f)

  (* The following definition is standard for any monad: *)
  let (>>=) t f = join (map t f)
  (* Infix map in the style of core *)
  let (>>|) t f = map f t
end

Here’s the associated functor (the type class or module signature) instance:

module JobF = struct
  type 'a t = Run of command * (result -> 'a)
  let map f (Run (c, k)) = Run (c, fun res -> f (k res))
end

You can convince yourself with simple substitution that Free(JobF).t is equivalent to 'a Job.Op.t. We’ll write this OCaml functor application as:

module J = struct
  include Free(JobF)
  let run c = lift (JobF.Run (c, fun i -> i))
end

The module is called J to avoid conflicts with Job and the run constructor will be very useful for us.

let _ =
  let tar_mls =
    let open J in
    run (command "find" ["." ; "-iname" ; "*.ml"])
    >>| (fun r -> r.r_stdout) >>= fun mls ->
    run (command "tar" (["-cf" ; "mls.tar"] @ mls))
  in
  Lwt_main.run (run_lwt tar_mls)

For completeness, let’s write our own interpreter for JobF. This is mostly just a demonstration to show how it’s done. But we’ll also make our own interpreter execute the commands with Lwt. Decoupling your programs from a particular implementation is a worthy goal in OCaml. So it’s worth to advertise as many techniques as possible for accomplishing this.

Here’s my hastily written implementation. (An Async implementation is an exercise for the reader)

let rec run_lwt : 'a J.t -> 'a Lwt.t = function
  | J.Return a -> Lwt.return a
  | J.Free (JobF.Run (cmd, k)) ->
    let (cmd, args) =
      (* Gross but necessary because command is abstract *)
      match Str.split (Str.regexp "[ \t]+") (string_of_command cmd) with
      | c::args -> (c, Array.of_list (c::args))
      | _ -> failwith "invalid command" in
    let p = Lwt_process.open_process_full (cmd, args) in
    let code_signal =
      p # status >|= function
      | Unix.WEXITED c -> (c, None)
      | Unix.WSTOPPED c
      | Unix.WSIGNALED c -> (256, Some c) in
    let read_stream s = Lwt_stream.to_list (Lwt_io.read_lines s) in
    let stdout = read_stream (p # stdout) in
    let stderr = read_stream (p # stderr) in
    let result =
      stdout >>= fun r_stdout ->
      stderr >>= fun r_stderr ->
      code_signal >|= fun (r_code, r_signal) ->
      { r_code
      ; r_signal
      ; r_duration = 0.0 (* TODO for the reader *)
      ; r_info = []
      ; r_stdout
      ; r_stderr
      ; r_cleanup = [] } in
    Lwt_io.close p # stdin >>= fun () ->
    Lwt.map k result >>= run_lwt

This should be enough to show one of the advantages of free monads. The ability to write your program in a way that separates effects from your computation gives us the opportunity to reinterpret these effects in different ways. This isn’t the only benefit of using free monads, and the example cited here is one of the simplest functors you can imagine, but it’s still nice to see how even this is useful in the context of real software.

Where to Go from Here?

Gabriel Gonzales’ posts about Free Monads are the best introduction to the subject:

http://www.haskellforall.com/2012/07/purify-code-using-free-monads.html

http://www.haskellforall.com/2012/06/you-could-have-invented-free-monads.html

John A De Goe’s post is an excellent resource to learn how to use free monads in practice:

http://degoes.net/articles/modern-fp

ocaml-free from NicolasT is a small but pretty complete library for Free monads. It also includes support for Free Applicatives a bonus.

https://github.com/NicolasT/ocaml-free

Comments

comments powered by Disqus