One key element of making impossible states impossible in OCaml is to wrap broad types into narrower ones. Here’s a strategy I use to achieve this with minimal boilerplate.
Let’s say I wanted a non-empty string with a maximum length. I might do:
(* string255.mli *)
type t = private string
val of_string : string -> t option
(* string255.ml *)
type t = string
let of_string s =
let len = String.length s in
if len > 0 && len <= 255 then Some s else None
;;
This gives us the compiler’s guarantee that any instance of String255.t
was necessarily created with String255.of_string
and is thus between 1 and 255 characters long. Any module in our project may use String255.t
instead of plain string
in its types to get this guarantee.
Crucially, OCaml’s private
keyword keeps the type definition public, but not writable. So for such a primitive type we can use a succinct type coercion instead of a more verbose to_string
function:
match String255.of_string "Hello, world!" with
| None -> 0
| Some s -> String.length (s :> string)
The explicit coersion required by OCaml is a bit unsightly, however the advantage of using private type abbreviations vs fully abstract types becomes obvious in complex scenarios. For example, a String255.t list
can be coerced into a string list
for free at compile time, whereas an equivalent abstract type would require running a List.map
of its to_string
at run-time, which incurs significant time and memory costs.
Reducing Boilerplate with Functors
What if we want 255, 64 and 16 character strings? What if we want to allow creating new modules for arbitrary lengths? Functors to the rescue!
module type Shortstring = sig
type t = private string
val of_string : string -> t option
end
module Make_shortstring (I : sig
val max_length : int
end) : Shortstring = struct
type t = string
let of_string s =
let len = String.length s in
if len > 0 && len <= I.max_length then Some s else None
;;
end
module String255 = Make_shortstring (struct let max_length = 255 end)
module String64 = Make_shortstring (struct let max_length = 64 end)
module String16 = Make_shortstring (struct let max_length = 16 end)
In my private library, for namespacing reasons I ended up creating distinct files for each type, which is equally easy:
(* string255.ml *)
include Make_shortstring (struct let max_length = 255 end)