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 =
of name * type_args option
| Hint of tok
| HintArray of tok * hint_type
| HintQuestion of hint_type comma_list paren | HintTuple
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 =
of name * type_args option
| Name Array of tok
| of tok * t
| Question of hint_type comma_list paren
| Tuple 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]
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)
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. ☰
@misc{Keleshev:2015-1,
title="Namespacing Variants in ML",
author="Vladimir Keleshev",
year=2015,
howpublished=
"\url{https://keleshev.com/namespacing-variants-in-ml}",
}
Did you like this blog post? If so, check out my new book: Compiling to Assembly from Scratch. It teaches you enough assembly programming and compiler fundamentals to implement a compiler for a small programming language.