Variants
Quick overview: Variants
Variant types model values that may assume one of many known variations. This feature is similar to "enums" in other languages, but each variant form may optionally specify data that is carried along with it.
This is a very powerful feature of the Reason language when used with pattern matching.
Creation
Variants require a type definition that specifies all of the "constructors", or "tags", for that variant. Constructors must start with an uppercase letter.
Here the constructors for the animal
variant are Cat
, Dog
, Horse
,
and Snake
:
type animal =
| Cat
| Dog
| Horse
| Snake;
To create a value of type animal
use one of the constructors:
let fluffy = Cat;
let spot = Dog;
Constructor Arguments
Variant constructors can have data associated with them:
type linkedList =
| Node(int, linkedList)
| Empty;
The constructors can now accept their data as arguments. Creating a linked
list [1, 2, 3]
looks like:
let x = Node(1, Node(2, Node(3, Empty)));
Note: This is how the core list
structure is
actually implemented. The constructors are just hidden behind some syntax.
Usage
The primary way to interact with variants is through pattern matching. When pattern matching over a variant the compiler can ensure that all cases are covered exhaustively so that there is no undefined behavior. This is also helpful when adding new cases to a variant later, because the compiler will present a clear list of code that needs to be updated.
let interact = (animal) => {
switch (animal) {
| Cat => "pet"
| Dog => "throw ball"
| Horse => "ride"
| Snake => "run away"
};
};
It is possible to interact with a constructor's data while pattern matching:
let checkFirst = (linkedList) => {
switch (linkedList) {
| Node(value, _) => "First is:" ++ string_of_int(value)
| Empty => "The list is empty"
};
};
Option
Options are a commonly used variant built into the Reason language. They represent the presence or absence of a value. It is similar to the concept of "nullable" values in other languages.
Details: Options
Inline Records
Often times variants will hold a single predefined record type:
type cat = {
name: string,
};
type dog = {
breed: string,
};
type animal =
| Cat(cat)
| Dog(dog);
let x = Cat({name: "Fluffy"});
If that record type is only ever used within that single variant, it can be more concise to define the variant and record type at the same time using inline records:
type animal =
| Cat({name: string})
| Dog({breed: string});
let x = Cat({name: "Fluffy"});
Note: This is a somewhat recent language feature and not all online REPL's will support this syntax. Any recent version of Reason will support inline records though.
Tips
Unbound Constructor Error
The variant type needs to be in scope when referencing any constructor of that variant. An easy way to fix this error is with an explicit type annotation:
module Animal = {
type t =
| Cat
| Dog
| Horse
| Snake;
};
/* Error: Unbound constructor */
let x = Cat;
/* No error */
let y: Animal.t = Cat;
Tuples as Arguments
There is a subtle difference between constructors that accept two arguments and constructors that accept one argument that is a 2-tuple. Pay close attention to which kind of variant you are dealing with:
type oneArgument =
| OneArg((int, string));
type twoArguments =
| TwoArgs(int, string);
let x = OneArg((1, "one"));
let y = TwoArgs(2, "two");
Using a one-argument variant that accepts a 2-tuple is common when dealing with
structures that use type parameters.
list((int, int))
is used because list(int, int)
is not valid.
Variants Must Have Constructors
In some cases it can be tempting to write a variant that is "un-tagged":
type t =
| int
| string;
let x: t = 100;
let x: t = "one";
The Reason language does not support this feature. All variants must have constructors. The above code is actually a syntax error. Instead write:
type t =
| Int(int)
| String(string);
let x: t = Int(100);
let x: t = String("one");