david wong

Hey! I'm David, cofounder of zkSecurity and the author of the Real-World Cryptography book. I was previously a crypto architect at O(1) Labs (working on the Mina cryptocurrency), before that I was the security lead for Diem (formerly Libra) at Novi (Facebook), and a security consultant for the Cryptography Services of NCC Group. This is my blog about cryptography and security and other related topics that I find interesting.

Simple introduction to monads in OCaml posted November 2022

I'm not a language theorist, and even less of a functional programming person. I was very confused trying to understand monads, as they are often explained in complicated terms and also mostly using haskell syntax (which I'm not interested in learning). So if you're somewhat like me, this might be the good short post to introduce you to monads in OCaml.

I was surprised to learn that monads are really just two functions: return and bind:

module type Monads = sig
    type 'a t
    val return : 'a -> 'a t
    val bind : 'a t -> ('a -> 'b t) -> 'b t
end

Before I explain exactly what these does, let's look at something that's most likely to be familiar: the option type.

An option is a variant (or enum) that either represents nothing, or something. It's convenient to avoid using real values to represent emptiness, which is often at the source of many bugs. For example, in languages like C, where 0 is often used to terminate strings, or Golang, where a nil pointer is often used to represent nothing.

An option has the following signature in OCaml:

type 'a option = None | Some of 'a

Sometimes, we want to chain operations on an option. That is, we want to operate on the value it might contain, or do nothing if it doesn't contain a value. For example:

let x = Some 5 in
let y = None
match x with
  | None -> None
  | Some v1 -> (match y with
    | None -> None
    | Some v2 -> v1 + v2)

the following returns nothing if one of x or y is None, and something if both values are set.

Writing these nested match statements can be really tedious, especially the more there are, so there's a bind function in OCaml to simplify this:

let x = Some 5 in
let y = None in
Option.bind x (fun v1 -> 
  Option.bind y (fun v2 ->
    Some (v1 + v2)
  )
)

This is debatably less tedious.

This is where two things happened in OCaml if I understand correctly:

Let's explain the syntax introduced by OCaml first.

To do this, I'll define our monad by extending the OCaml Option type:

module Monad = struct
  type 'a t = 'a option

  let return x = Some x
  let bind = Option.bind
  let ( let* ) = bind
end

The syntax introduced by Ocaml is the let* which we can define ourselves. (There's also let+ if we want to use that.)

We can now rewrite the previous example with it:

open Monad

let print_res res =
  match res with
  | None -> print_endline "none"
  | Some x -> Format.printf "some: %d\n" x

let () =
  (* example chaining Option.bind, similar to the previous example *)
  let res : int t =
    bind (Some 5) (fun a -> bind (Some 6) (fun b -> Some (a + b)))
  in
  print_res res;

  (* same example but using the let* syntax now *)
  let res =
    let* a = Some 5 in
    let* b = Some 6 in
    Some (a + b)
  in
  print_res res

Or I guess you can use the return keyword to write something a bit more idiomatic:

  let res =
    let* a = Some 5 in
    let* b = Some 6 in
    return (a + b)
  in
  print_res res

Even though this is much cleaner, the new syntax should melt your brain if you don't understand exactly what it is doing underneath the surface.

But essentially, this is what monads are. A container (e.g. option) and a bind function to chain operations on that container.

Bonus: this is how the jane street's ppx_let syntax works (only defined on result, a similar type to option, in their library):

open Base
open Result.Let_syntax

let print_res res =
  match res with
  | Error e -> Stdio.printf "error: %s\n" e
  | Ok x -> Stdio.printf "ok: %d\n" x

let () =
  (* example chaining Result.bind *)
  let res =
    Result.bind (Ok 5) ~f:(fun a ->
        Result.bind (Error "lol") ~f:(fun b -> Ok (a + b)))
  in
  print_res res;

  (* same example using the ppx_let syntax *)
  let res =
    let%bind a = Ok 5 in
    let%bind b = Error "lol" in
    Ok (a + b)
  in
  print_res res

You will need to following dune file if you want to run it (assuming your file is called monads.ml):

(executable
 (name monads)
 (modules monads)
 (libraries base stdio)
 (preprocess
  (pps ppx_let)))

And run it with dune exec ./monads.exe

Well done! You've reached the end of my post. Now you can leave a comment or read something else.

Comments

leave a comment...