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.