Namespacing Variants in ML

2015-04-12

When reading code written in OCaml or Standard ML, I keep seeing variant constructors having ad-hoc prefixes or suffixes used for namespacing.

Here’s an example from Modern Compiler Implementation in ML (page 98):

type operator = PlusOp | MinusOp | TimesOp | DivideOp

And here’s just one of many examples from Facebook pfff tool:

type hint_type =
  | Hint of name * type_args option
  | HintArray of tok
  | HintQuestion of tok * hint_type
  | HintTuple of hint_type comma_list paren

What you can do instead is to drop the prefix/suffix and use a small module as a namespace instead:

module Operator = struct
  type t = Plus | Minus | Times | Divide
end
module Hint = struct
  type t =
    | Name of name * type_args option
    | Array of tok
    | Question of tok * t
    | Tuple of hint_type comma_list paren
end
Now, at the use site you can select the most readable option depending on the context. You can spell it all out if the variants are only used briefly:
let operators = [Operator.Plus; Operator.Minus]

Or you can create a module alias if you use the variants a lot:

module Op = Operator

let operators = [Op.Plus; Op.Minus]

Or you can locally open the module if you need to use them intensely in a particular scope:

let operators =
  let open Operator in
  [Plus; Minus; Times; Divide]

(* or *)

let operators =
  Operator.([Plus; Minus; Times; Divide])

Or you can just open the module at the top of your file if that’s your thing:

open Operators

let operators = [Plus; Minus; Times; Divide]

Modular Programming

Now that you have a module like Operator you suddenly realize that other definitions probably also belong to it.

You might have functions with names such as parse_operator or action_of_operator:

type operator = PlusOp | MinusOp | TimesOp | DivideOp

let parse_operator = function
  | "+" -> Some PlusOp
  | "-" -> Some MinusOp
  | "*" -> Some TimesOp
  | "/" -> Some DivideOp
  | _   -> None

let action_of_operator = function
  | PlusOp -> (+)
  | MinusOp -> (-)
  | TimesOp -> ( * )
  | Divide -> (/)

Now you can group them all in the namespace module and give them more appropriate names (for module context):

module Operator = struct
  type t = Plus | Minus | Times | Divide

  let of_string = function
    | "+" -> Some Plus
    | "-" -> Some Minus
    | "*" -> Some Times
    | "/" -> Some Divide
    | _   -> None

  let to_action = function
    | Plus   -> (+)
    | Minus  -> (-)
    | Times  -> ( * )
    | Divide -> (/)
end
Namespacing functions this way has same benefits to namespacing your original type. The new flexibility affords you better readability or shorter expressions at the call site. For example, you can write something like this now:
let action =
  Operator.(source |> of_string |> to_action)

Another advantage is that such module visually groups related type and functions in your source file.

Yet another advantage: you can use your new module in functor context, assuming it implements the required signature:

module OperatorSet = Set.Make (Operator)

Conclusion

One of my fist complains coming from object-oriented to functional programming was that so much functional code looks like big bags of functions and types with little structure. Well, ML structures to the rescue!

Nowadays whenever I have a type and a bunch of related functions (sounds familiar?), I’m more inclined than not to group them in a namespace module.

Comment on Reddit
Comment on Hacker News
Follow me on Twitter