mtoohey.com

Control Flow Monad

posted: 
tags: [MonadOcaml]

This article is going to assume you already have some understanding of what a monad is. If you haven’t heard of monads before, you may want to do some research before reading. That said, you don’t have to understand them perfectly: you might find this example useful if you’re still trying to wrap your head around them.

Recently, while I was working on an interpreter written in OCaml, I came across an interesting application of monads. (I am likely not the first person to use monads in this way, but found it cool so I thought it was worth a small article.) The interpreter is for a procedural language which allows you to break or continue within a loop, and return within a function body. Originally, I dealt with this by defining something like the following simple data type:

type 'a control =
  | Break
  | Continue
  | Return of value option
  | None of 'a

Evaluating an expression would produce a value wrapped in this type, which could then be matched on to handle any potential control flow. However, I found myself often writing lots of nested code to handle cases where one of the aforementioned control flow constructs could cause evaluation to end immediately. I also realized that the handling was almost always the same:

match control with
| Break -> Break
| Continue -> Continue
| Return value -> Return value
| None a -> (* Do something with a here... and eventually return `None b` *)

(All the cases that just seem to return the same thing were often necessary when converting from a control of one type to a control of another, for example going from a int control to a string control.)

Then I realized that this looked kinda like a monad. Here’s bind, which looks pretty similar to the structure above, and return:

let ( >>= ) control f =
  match control with
  | Break -> Break
  | Continue -> Continue
  | Return value -> Return value
  | None a -> f a

let return a = None a

let ( let* ) = ( >>= )

The final definition uses OCaml’s let extensions feature which makes bind very convenient to use. Here’s an example of using this to evaluate a function call expression, which I would’ve done before using three levels of nested matching:

let* callee = exec_expr callee scopes in
match callee with
| Procedure f ->
    let* args = exec_prod args scopes in
    return (f args)
| invalid -> raise InvalidCallee

Hope you found this interesting, thanks for reading!