What is Phantom type in OCaml

Info

The left side of = represents type, while the right side represents the value.

To use phantom type in OCaml, just add type parameters (such as 'a) on the left side of = when defining a variant type, but do not use them on the right side. For example

ocaml

type 'a t = some_type

The essence here: We do not use 'a on the right side of =, which means we can replace 'a with any type we want when defining values

The phantom type refers to such type parameters ('a in this example). Because we won’t use them, that’s why it is called phantom 👻

Since 'a isn’t being used, it’s better to replace it with _, which is a more idiomatic approach.

ocaml

type _ t = some_type

Now let’s see how to use the phantom type.

Info

In my opinion, the advantage of using phantom type is that we are allowed to attach “state” into types. So we can differentiate different types in a more type-safe way.

I will just borrow the currency example from this Haskell MOOC1. It’s intuitive that we can represent any currency type with float value (the float type in OCaml). Let’s use the phantom type to define a currency type.

ocaml

utop # type _ currency = Currency of float;;

Then we can define two phantom types for two different kinds of currency: the US dollar and the Chinese yuan.

ocaml

utop # type dollar;;
utop # type yuan;;

Now, let’s write a function for adding currencies. Assuming that we only want to add US dollars, we can write code like this.

ocaml

utop # let dollar_add (Currency x : dollar currency) (Currency y : dollar currency) :
    dollar currency =
  Currency (x +. y);;

val dollar_add : dollar currency -> dollar currency -> dollar currency = <fun>

By the function signature, we can see that this dollar_add function can only work for US dollars. Let’s verify this.

ocaml

utop # let three_dollar : dollar currency = Currency 3.0;;
utop # let four_dollar : dollar currency = Currency 4.0;;

utop # let res = dollar_add three_dollar four_dollar;;
val res : dollar currency = Currency 7.

From the output, we find that adding two US dollars is totally fine. Now let’s replace the function inputs with two Chinese yuans.

ocaml

utop # let yuan_sum = dollar_add three_yuan four_yuan;;
Error: This expression has type yuan currency
       but an expression was expected of type dollar currency
       Type yuan is not compatible with type dollar

The OCaml compiler catches this type of error at compile-time😺

What if we want to write a general function that can handle any currency type? We can add 'a currency type declaration in the function signature. For example

ocaml

utop # let scale (Currency x : 'a currency) factor : 'a currency =
    Currency (x *. factor);;

val scale : 'a currency -> float -> 'a currency = <fun>

We can also omit the type declaration here.

ocaml

utop # let scale (Currency x) factor = Currency (x *. factor);;

val scale : 'a currency -> float -> 'b currency = <fun>

There is a subtle difference here in that the return type is 'b currency rather than 'a currency, for that the OCaml always infers the most general compatible type for us. If this is not the expected behavior in your case, you can choose to add type declarations yourself.

Compared to writing variant type without phantom type, the benefits we gain are that.

  1. Extensibility: every time we want to add a new case, we just need to define one more phantom type (type ...)
  2. Type safety: By adding the type declaration, we can control the function’s behavior and get fine-grained control. The OCaml compiler would help us track “states” and catch type errors in compile-time.

It seems that the code becomes more verbose after using phantom type. However, usually in OCaml, we would put the type declarations in a module and put it into a .mli file, and then give implementations in the corresponding .ml file, which makes code more concise. For simplicity, I do not use this design here. You can find how it works in this blog post written by the Jane Street