Introduction
Variants are one of the coolest features of reasonml
.
In the official documentation, they mention a limitation of the variants (also called ordinary variants):
A function can’t accept an arbitrary constructor shared by two different variants.
They also mention that this is possible to overcome this limitation using polymorphic variants.
The purpose of this article is to expose the limitations of the ordinary variants and to see how polymorphic variants overcome this limitation. We hope that the examples we bring with dogs and tulips will make the reading of this article somewhat enjoyable.
Ordinary Variants - brief recap
Let’s say you have an animal
variant
type animal =
| Dog
| Cat
And you want to write a function that stringifies an animal
.
let string_of_animal = x =>
switch (x) {
|Dog => "dog"
|Cat => "cat"
}
Now, a Dog
is a “dog” and a Cat
is a “cat”:
"The " ++ string_of_animal(Dog) ++ " bites the " ++ string_of_animal(Cat)
So far so good.
Now let’s do the same with flowers:
type flower =
| Rose
| Tulip;
let string_of_flower = x =>
switch (x) {
|Rose => "rose"
|Tulip => "tulip"
};
let a = "The " ++ string_of_flower(Rose) ++ " is more beautiful than the " ++ string_of_flower(Tulip);
The limitation of Variants
Now what happens if you try to write a function that stringifies both flowers and animals?
let string_of_flower_or_animal = x =>
switch (x) {
|Rose => "rose"
|Tulip => "tulip"
|Dog => "dog"
|Cat => "cat"
};
The constructor Dog
doesn’t belong to type flower
and in that case ocaml
doesn’t create a flower_or_animal
type on the fly!
Another limitation of ordinary variants is that you cannot mix elements of types animal
and flower
in a list or in an array:
let a = [Dog, Cat, Rose, Tulip]
Welcome to the world of polymorphic variants!
Polymorphic variants
Syntactically, polymorphic variants are distinguished from ordinary variants by the leading backtick:
let myDog = `Dog;
Note that unlike ordinary variants, polymorphic variants can be used without an explicit type declaration. Their type is inferred automatically.
Of course, it works also with variants that are parametrized:
let myNumber = `Int(4)
Now, let’s see how to write our string_of_animal_or_flower
function with polymorphic types:
let string_of_flower_or_animal = x =>
switch (x) {
|`Rose => "rose"
|`Tulip => "tulip"
|`Dog => "dog"
|`Cat => "cat"
};
Note that the system has automatically inferred the type of the function argument: it’s [< `Cat | `Dog | `Rose | `Tulip ]
. You probably wonder what is the meaning of the <
sign.
Before answering that question, let’s see how polymorphic variants allow us to mix elements of different types in a list:
let myNature = [`Dog, `Cat, `Rose, `Tulip]
Now, the type of the list is: [> `Cat | `Dog | `Rose | `Tulip ] list
.
Upper and lower bounds
Now it’s time to explain what is the meaning of <
and >
in the context of polymorphic variants.
The >
at the beginning of a variant type marks the type a being open to combination with other variant types. We can read the type [> `Cat | `Dog | `Rose | `Tulip]
as describing a variant whose tags include `Cat
, `Dog
, `Rose
and `Tulip
, but may include more tags as well.
In other words, you can roughly translate >
to mean: “these tags or more”.
Indeed, we are allowed concatenate list of animals and list of flowers:
let myAnimals = [`Dog, `Cat];
let myFlowers = [`Rose, `Tulip];
let myThings = List.concat([myAnimals, myFlowers]);
The <
at the beginning of a variant type means “these tags or less”. For instance, in our string_of_flower_or_animal
function defined above, the argument has been inferred to be of type [< `Cat | `Dog | `Rose | `Tulip ]
. Indeed the function has no way to deal with values that have tags other than `Cat
, `Dog
, `Rose
and `Tulip
.
Conclusion
You might now ask yourself why not always use polymorphic variants.
The answer is that the flexibility of polymorphic variant comes at a price.
- They are more complex that ordinary variants
- They are less likely to catch bugs that ordinary variants - precisely because of the flexxibility they allow
- They are a bit heavier and less performant that ordinary variants
Be sure to read this chapter of Real World Ocaml to go deeper with your understanding of ordinary and polymorphic variants. At the end of this chapter they explain with grat details what are the advantages and disadvantages of polymorphic variants over ordinary variants.