The power of OCaml's signatures

A student in my lab is currently taking a programming languages course that teaches OCaml; he's a C guy and doesn't really like OCaml. A few weeks ago, we were talking about OCaml's module system and how we can use it to hide its underlying implementation.

module type MY_TYPE = sig type t val empty : t val do_something : t -> t (* ... *) end module MyType : MY_TYPE = struct type t = int let empty = 0 let do_something n = n + 88 end

The MyType module implements the MY_TYPE signature, and if we look at a MyType.t in the OCaml REPL we see the rather unhelpful <abstr> . This student was trying to finish an assignment, and he was annoyed at this opacity which made debugging harder. I told him that in practice, it can often be extremely helpful, but had no actual example from my own experience to offer. Yesterday, I was fortunate to have such an experience.

I am currently writing a small and simple compiler for a subset of the Go language, in preparation of a compiler class this winter. As I started working on the type checker, I needed a symbol table, and thus defined the following signature (inside a .mli file):

type 'a t val empty : 'a t val add : 'a t -> Ast.symbol -> 'a -> 'a t val mem : 'a t -> Ast.symbol -> bool val find_exn : 'a t -> Ast.symbol -> 'a val print : 'a t -> ('a -> string) -> unit

And the actual implementation used a Map, and that seemed to work fine. During the implementation of the type checker, I found that I had a small problem: in Go, two variables cannot be declared in the same lexical scope, i.e.:

// Illegal var x int = 3 var x int = 4 // Okay var x int = 3 { var x int = 4 }

To permit variable shadowing, I needed a way to determine if a symbol was declared locally or in a parent scope. I modified the signature of my symbol table with functions that you would find in any good compiler textbook:

type 'a t val empty : 'a t val add : 'a t -> Ast.symbol -> 'a -> 'a t val mem : 'a t -> Ast.symbol -> bool val find_exn : 'a t -> Ast.symbol -> 'a val print : 'a t -> ('a -> string) -> unit val open_scope : 'a t -> 'a t val exit_scope : 'a t -> 'a t val defined_locally : 'a t -> Ast.symbol -> bool

I modified the implementation to use a list of Maps instead of a single one, where open_scope adds an empty Map to the list and close_scope removes the head of the list. After recompiling the program, everything worked as it did before, and I was able to detect symbols defined multiple times. This did not involve the modification of any code in the actual type checking procedures.

This is a small example of the power of abstract data types and how OCaml enforces them: by not allowing the user to peek into a abstract type and play with its underlying data structures, we can apply important changes to our core while keeping user code unaltered and unaffected.