Variants are sold as one of Reason's more powerful features, often demonstrated to show polymorphic pattern matching; however, they have another interesting use case by boxing datatypes as singleton variants to create something similar to a Value Class or newtype.

From working with Scala I am used to creating Value Classes, which can be compared to Haskell's newtype. These constructs allow the developer to express greater levels of type information and abstraction in their code, with little or no runtime performance penalty. It's possible to achieve the same effect in ReasonML with singleton variants.

What is a Value Class?

A value class is a simple wrapper around a primitive type that gives you more control over the input and output of functions. This has a number of benefits, such as restricting construction to validated values or simply aiding with passing around many parameters into a function.

These are very easy to construct in Scala by extending AnyVal

case class Name(value: String) extends AnyVal

Whilst it looks like there is an additional overhead here; after all, the String has been boxed inside a class which you would expect would need to be instantiated each time – in the JVM the wrapping class is removed after compilation. So there should not be a performance cost when wrapping types in this manner. There is just one minor issue, if you wish to access the underlying String then you have to manually access it:

val name = Name("Cat")

println("The name is: " + name.value)
I almost used ++ as if I were using Reason

You can achieve something similar in ReasonML by boxing the type and I'll demonstrate it later.

Why would you want to do this?

Essentially to make your code more descriptive and to prevent mistakes. This is probably best illustrated with examples. So let us imagine you have a function type signature for a simple function to create a person:

let makePerson: (string, string, string, int) => unit;
Back to Reason

As simple as the definition is, it may leave you wondering about a number of things: how you know distinguish between the meaning of these fields? Which holds the first name and which the surname? Just what exactly is that integer? Why are there three string parameters?

Sure, you could probably work those questions out by looking at the output type, and yes, I deliberately left it as unit to make life hard. Still, this function could be storing its output in a database or mutable dictionary somewhere and unit could be an acceptable output type.

So in order to answer that question, you may wish to use named parameters instead. And that's a reasonable solution:

let makePerson: (
  ~firstName: string,
  ~surname: string, 
  ~hometown: string, 
  ~age: int
) => unit 
You'd have to actually implement this to get it to compile

Now at least you can identify what goes where and it would be acceptable to finish here. Still, this has some minor issues that can be addressed. For example, you could accidentally pass in a name into the hometown field.

Another alternative would be to use type aliases for the fields, which would make the method more descriptive without the overhead of typing the labels each time:

type firstName = string;
type surname = string;
type hometown = string;
type age = int;

let makePerson: (
  firstName,
  surname, 
  hometown, 
  age) => unit
This reads rather nicely

Whilst very readable, this code is no safer than the original implementation. Aliases don't provide any protection and you can pass any string as any of the function's parameters.

In both solutions, the string type is still being used for three different things; however, in Scala is possible to abstract the string away by using Value Classes. Let's quickly demonstrate that:

case class FirstName(value: String) extends AnyVal
case class Surname(value: String) extends AnyVal
case class Hometown(value: String) extends AnyVal
case class Age(value: String) extends AnyVal

abstract def makePerson(
  firstName: FirstName,
  surname: Surname, 
  hometown: Hometown,
  age: Age): Person
  
// Or if you simply wanted to use a constructor
case class Person(
  firstName: FirstName,
  surname: Surname, 
  hometown: Hometown,
  age: Age)
Mmm... Scala

So in the above example, unlike simple type aliases, you cannot pass a FirstName into say the Hometown field. Each of those types is independent of the primitive type it wraps.

So how do we do this in Reason?

So how do we do this in Reason? Well, we can box the primitive types within single-argument variants.

type firstName = FirstName(string);
type surname = Surname(string);
type hometown = Hometown(string);
type age = Age(int);

let makePerson: (
  firstName,
  surname, 
  hometown, 
  age) => unit = (a, b, c, d) => ();
Aha! Single-argument variants

Now it isn't possible to accidentally pass in a hometown as a surname, any such mistake would cause the program to not compile. Whilst this is only a simple example, this becomes more useful the bigger your solution gets. Anywhere else in the codebase it would no longer be possible to mistake a surname for a string or an age for an int.

A common situation for this in a larger application is for id fields. You may end up with int being used for a user id, post id, account id, payment id, group id, and so on. If these types are abstracted within singleton variants, then we can differentiate between the types.

Now, at some point you will need to unbox the values from these singleton variants. You could use a switch, but that's a little long-winded. Instead, try using the handy fun shortcut.

let name = FirstName("Dave");

let nameString = name |> fun | FirstName(str) => str;

That's nice, but there's yet another, easier way to unbox the value:

let FirstName(nameString) = name;

/* Now you can use `nameString` 
Js.log("The person's name is: " ++ nameString");
Yeah! This is even better.

Isn't there a performance cost?

Unlike Scala, the above example does come with a penalty. There is a small cost as Reason will construct the variant as a single argument array. Accessing the value in the code above is like accessing an array using myArray[0]. For example the above name construction compiles to:

var name = /* FirstName */["Dave"];

However, since release 7.1.0 we are able to use unboxed to get around this! What is this? Let's look at the OCaml manual:

unboxed can be used on a type definition if the type is a single-field record or a concrete type with a single constructor that has a single argument. It tells the compiler to optimize the representation of the type by removing the block that represents the record or the constructor (i.e. a value of this type is physically equal to its argument).

This now means a singleton variant is not compiled as an array but is instead unboxed to the underlying type. Essentially, like with Scala, the OCaml compiler will erase the singleton variant in a later stage of compilation as it's not required at runtime. To use this mark the type as [@unboxed] like so:

[@unboxed]
type hometown = Hometown(string);
let tokyo = Hometown("tokyo");

This will then be unboxed from the array to a simple var during compilation:

var tokyo = "tokyo";
No more array!

So no more performance penalties! According to the release notes, this can also be used to unbox singleton records . Note, that whilst the release notes are for development version, this feature was released with bs-platform@7.1.0.

To give you a quick example of using singleton records:

[@unboxed]
type hometown = {name: string};
let kyoto = {name: "kyoto"};

That will compile to the following JavaScript:

var kyoto = "kyoto";

As a record it may be simplier to extract the values, there's no need for pattern matching:

[@unboxed]
type hometown = {name: string};
let kyoto = {name: "kyoto"};

Js.log("The name is: " ++ kyoto.name);

Whether you prefer to use singleton variants or records for this is a personal choice. As you can see, variants are not as clean to extract from as records, but are quicker to construct.