April 30, 2019

Readable Constrained Wrapper Types

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)

© 2008-2023 Stéphane Lavergne — Please ask permission before copying parts of this site.

Powered by Hugo & Kiss.