Do Not Open

Christian Lindig
4 min readSep 7, 2015

In OCaml, access to a non-local value like the printf function requires using the dot notation: Printf.printf. Below is a simple hello.ml that uses functions from the List, Printf, and Array modules of the standard library in this way:

<<hello.ml>>=<<join function>>
let main () =
let argv = Array.to_list Sys.argv in
let args = List.tl argv in
match args with
| [] -> Printf.printf "Hello, world!\n"
| names -> Printf.printf "Hello, %s!\n" (join names)
let () = main ()

The join function (of type string list -> string) turns a list of strings into one and is omitted here — you can find it below. Running hello yields the familiar results:

$ ./hello.native Ernie Bert
Hello, Ernie and Bert!

Sweet Poison Open

Since qualified access using the dot notation can be tedious to write, programmers are tempted to open at least some modules and to use non-local values directly:

<<hello_open.ml>>=
open Printf
open List
open Sys
<<join function>>
let main () =
let argv = Array.to_list argv in
let args = tl argv in
match args with
| [] -> printf "Hello, world!\n"
| names -> printf "Hello, %s!\n" (join names)
let () = main ()

Now, printf, argv and tl don’t need qualified access anymore. But don’t use open like this. While the compiler and tools still know where to find each value, readers of your code probably don’t. And chances are, after a while you don’t either. The problem is not so much opening modules from the standard library, that programmers are familiar with, but your own modules that readers are unfamiliar with.

Each open introduces all values from the opened module into the local name space — not just the one that you need. The three open modules introduce 90 values into the name space. With each open the chances of one module shadowing values (like to_list) from another one increase, as does the helplessness of your readers.

Saying No to Open

Instead of simply opening one or many modules, use the module system to your advantage:

  • bind a frequently used module to a local module with a short name, or
  • bind a frequently used value to a local name

Below is hello.ml again but with non-local values and modules bound to short names:

<<hello_again.ml>>=
module L = List
module A = Array
let printf = Printf.printf<<join function>>
let main () =
let argv = A.to_list Sys.argv in
let args = L.tl argv in
match args with
| [] -> printf "Hello, world!\n"
| names -> printf "Hello, %s!\n" (join names)
let () = main ()

Local Open And Local Modules

You can even bind a module locally. Below, Printf is bound to P just for the body of the main function. (In a real program I would introduce a local value printf instead of binding the module.)

let main () =
let module P = Printf in (* local module *)
let argv = Array.to_list Sys.argv in
let args = List.tl argv in
match args with
| [] -> P.printf "Hello, world!\n"
| names -> P.printf "Hello, %s!\n" (join names)

For completeness, I’d like to add that open can also be used locally and thus at least confine its problems:

let main () =
let module P = Printf in (* local module *)
let open List in (* local open *)
let argv = Array.to_list Sys.argv in
let args = tl argv in (* uses List.tl *)
match args with
| [] -> P.printf "Hello, world!\n"
| names -> P.printf "Hello, %s!\n" (join names)

What Others are Saying

In Real World OCaml, Jason Hickey, Anil Madhavapeddy and Yaron Minsky agree about the perils of opening modules:

Opening modules at the toplevel of a module should be done quite sparingly, and generally only with modules that have been specifically designed to be opened, like Core.Std or Option.Monad_infix.

If you do need to do an open, it’s better to do a local open.

However, they don’t like to bind modules to short names as a remedy and liken it to be a mistake:

Rebinding modules to very short names at the top level of your module is usually a mistake.

When values from a module are used frequently, I don’t see how opening or binding it again and again locally is the better solution — so we disagree here.

Prof. Norman Ramsey, who works on programming languages, advises (page 10) his PhD students firmly never to use open in either Standard ML or OCaml. I collaborated with Norman in the past and cite him here to highlight the range of opinions about open that exist among well-informed users of the language.

Since there is still a lot of agreement, why does not using open globally need this article? Because I still see it used far too much on GitHub projects when I try to understand the code.

Resources

The join function that was omitted above. It is only suited for short lists as it is not tail recursive and uses inefficient string concatenation.

<<join function>>=
let rec join = function
| [] -> ""
| [x] -> x
| [x;y] -> x ^ " and " ^ y
| x::xs -> x ^ ", " ^ join xs

--

--