Early in ‘09, I put up a post asking Private type abbreviations, what are they good for?. I got a lot of good answers to that question, but I thought I would mention one more: using private types for encoding subtyping relationships in phantom types. Below is a simple example, which is based on an example from a previous post. This example proposes three kinds of ref’s, to be separated by phantom types:

  • readwrite: a ref that can be both read from and written to.
  • readonly: a ref that can only be read from (but someone else might be able to modify)
  • immutable: a ref that can not be modified under any circumstances.

Obviously, the last case, immutable, is rather silly for a ref. But for a more complex datastructure (an array, for example), it makes perfect sense.

There is a natural sub-typing relationship here. Both immutable and readwrite refs can be used anywhere where one needs a readonly ref. In the following, we represent this subtyping relationship using private type abbreviations.

type readonly
type readwrite = private readonly
type immutable = private readonly

module Ref : sig
  type +'a t
  val create : int -> readwrite t
  val create_imm : int -> immutable t
  val set : readwrite t -> int -> unit
  val get : 'a t -> int
end
 =
struct
  type 'a t = int ref
  let create x = ref x
  let create_imm x = ref x
  let set x v = x := v
  let get x = !x
end

Note that we need define no explicit coercion functions in the interface. One can simply use the :> syntax to do whatever coercions are required. i.e., one can write:

let x = Ref.create 3
let y = (x :> readonly Ref.t)

Note that it’s important to declare the phantom parameter as covariant (which is what the + in the type definition is for), since otherwise you won’t be able to cast the phantom parameter.

I’ve come to think of private type abbreviations as one of the better ways of designing a phantom type. My general design preference goes something like this:

  • If you can, use uninhabited types. They’re the simplest thing, because there are no type equalities, and all coercions are explicit in the interface.
  • If subtyping really helps make the interface more usable, use private type abbreviations on top of uninhabited types.
  • Finally, and only if sorely pressed, should you use polymorphic variants or object types. These are harder to understand, but also the most expressive choice.