What is Phantom type in OCaml
Syntax
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
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.
type _ t = some_type
Now let’s see how to use the phantom type.
Use case
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.
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.
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.
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.
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.
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
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.
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.
Wrap-up
Compared to writing variant type without phantom type, the benefits we gain are that.
- Extensibility: every time we want to add a new case, we just need to define one more phantom type (
type ...
) - 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