👻Reason 👻

Phantom Types in ReasonML

Kennet Postigo
ReasonTraining
Published in
5 min readJul 20, 2018

--

The classic definition that I’ve seen referenced and tossed around in discussion comes from Haskell:

A phantom type is a parametrized type whose parameters do not all appear on the right-hand side of its definition

This definition leaves a bit to be desired and doesn’t communicate well why the parameterized type matters at a first glance. Hopefully throughout this post we’ll dig into this definition and try to make phantom types less scary 👻 through examples and text. Aight, lets dig in!

Why should I care about Phantom Types?

Phantom Types for the most part won’t enter your day to day work life as a programmer, and not knowing them will probably not hinder your ability to code effective and reliable programs. They are for the most part a bonus feature that is supported in our AWESOME types system! You might see them brought up in discussion or used in code and be like “What Jedi Type System Tricks are goin on here?”, and that just might make you curious enough to try and find out. So here goes my attempt to try to provide a simple and easy read on the topic.

Phantom types are useful when you want to deal with specialization of a more general type, and in effect creating subtypes that can be treated differently when they are passed to functions or constructor. They have the same data representation but cannot be proven to be the same according to the type system. This means we deliberately separate the type from the underlying data structure, so as to make values of the same shape have distinct types.

👶 steps into 👻 Types

Lets start off by showing you the most basic example of a phantom type according to the definition in the Haskell Wiki:

type t('a) = string;

Here we have a parameterized type t, where its parameter isn’t used on the right hand side of its definition. Meaning type t, will always be a string no matter what the ‘a is. Alone this isn’t too intriguing, so lets bring some dogs and cats into the mix. Lets write some code that allows us to create dogs and cats and determine whether they should be mated together. However, both of the cats and dogs data representation must be the same, but the type system MUST be able to determine they are unique from one another, and in doing so verifies that only 2 dogs and 2 cats can mate. We’re going to start off by creating a module type/interface for a Animal module. We create a module type first to define the bits and pieces we want the outside world to know about our module.

Our module type Animal has 3 abstract types, types that have only been given a name but their contents are left unknown. Because their contents are left abstract in the module type, the outside world cannot access and manipulate them directly, you have to use the modules functions in order create or manipulate them. We create types for the functions makeDog and makeCat in order to create values of t(dog) and t(cat). Finally, we have a mate function that will return a string. Our mate is interesting because both have to be a type t(_), but we go a step further and make both be ‘a as well. This means that both must be the same type of t. Lets go ahead and implement this module type:

Off the jump, there are somethings that should standout to you. In the module definition, we define the contents of type t, in doing so only the code in our module knows what the type of t is, but the outside world does not, because of the module type definition(interface) didn’t reveal it. Because we define the contents of t in the module the compiler now knows that no matter what ‘a type is, t will always be a string(t could have been int, float, bool, etc. it doesn’t matter that it is a string, but that we know in the module definition what it exactly it is). The dog and cat are left abstract in the module, so that the compiler can’t unify them as having the same type representation.

Afterwards are the make functions for dog and cat, these are essentially identity functions because we now know t is of type string, but the module type/interface doesn’t specify this, so the compiler will take a string and If output a t(dog) or t(cat) to the outside world. This is the beauty of phantom types! Our data representation stays the same, only our type representation changes superficially! Lets make some dogs and cats and see if they can mate

https://bit.ly/2IUSbXw

So we create 2 dogs and two cats, and we call mate with them as arguments. It goes smoothly ensuring that 2 dogs or 2 cats can mate. However if we mix dogs and cats, we get a compile time error:

Because we are using phantom types we are guaranteed that our animal “sub types” can’t intermingle with each other! Despite them just really being strings(or any specific one type for that matter)! This is where phantom types bring value to the table. Imagine using them for encoding and decoding data of different formats, validating form data, etc.

🐕 Catch all interMating 🐈

So we know how to restrict mating a dog and cat, but what if we wanted to allow a dog or cat to mate for some bizarre reason? Lets update our module type(interface for Animal):

You might be surprised at how easy it was to allow interMating, we simply changed the type parameter of t of each argument of interMate in order to let the compiler know, these types are may not be the same.

Lets add our implementation of interMate in the module definition:

As you can see, mate and interMate look frighteningly similar to one another, with the only change being the value of the string. This is awesome from a developer standpoint! Instead of adding logic to prevent inter mating in mate we simply encode this in the types. Now lets call these functions:

This is nice, we can call interMate with a dog and cat now, and if you notice, we can also call interMate with 2 dogs or 2 cats. If we wanted to enforce 1 dog or 1 cat, we can simply encode this in the type parameters of the arguments in the type definition of interMate:

Summary

Phantom types are a powerful type system feature that can remove impossible states in your code, a mechanism for sub typing, and polymorphism. It might not be the tool you reach for often, but understanding it will add a tool to your belt and give you the option to reach for it! I hope I was able to get the point across successfully, thanks for reading!

Until Next time!

If you want to receive updates about when ReasonTraining is going to be fully launched and notifications when we release new blog posts, please go ahead and drop your email here and follow us on twitter.

👋

--

--