Behold, the crown jewel of Reason data structures!
Most data structures in most languages are about "this and that". A variant allows us to express "this or that".
type myResponseVariant =
| Yes
| No
| PrettyMuch;
let areYouCrushingIt = Yes;
Yes
, No
and PrettyMuch
aren't strings, nor references, nor some special data type. They're called "constructors" (or "tag"). The |
bar separates each constructor.
Note: a variant's constructors need to be capitalized.
Along with a variant comes one of the most important features of Reason, the switch
expression.
A Reason switch
is visually similar to other languages' switch
(aka a large if/elseif/elseif...
). It allows you to check every possible case of a variant. To use it, enumerate every variant constructor of the particular variant you'd like to use, each followed by an =>
and the expression corresponding to that case.
let message = switch (areYouCrushingIt) {
| No => "No worries. Keep going!"
| Yes => "Great!"
| PrettyMuch => "Nice!"
};
/* message is "Great!" */
A variant has an extremely rich amount of type system assistance. For example, we'll give you a type error if you've forgotten to cover a case of your variant, or if two cases are redundant. Be sure to check out switch and pattern-matching in a later section!
If the variant you're using is in a different file, bring it into scope like you'd do for a record:
/* Zoo.re */
type animal = Dog | Cat | Bird;
/* example.re */
let pet: Zoo.animal = Dog; /* preferred */
/* or */
let pet = Zoo.Dog;
A variant's constructors can hold extra data separated by space.
type account =
| None
| Instagram string
| Facebook string int;
Here, Instagram
holds a string
, and Facebook
holds a string
and an int
. Usage:
let myAccount = Facebook "Josh" 26;
let friendAccount = Instagram "Jenny";
Notice how using a constructor is like calling a function? It's as if Facebook
was a function that accepts two arguments. This isn't a coincidence; there's a reason why a constructor's data is called "constructor argument".
Using switch
, you can pattern-match (again, described in a later section) a constructor's arguments:
let greeting = switch (myAccount) {
| None => "Hi!"
| Facebook name age =>
"Hi " ^ name ^ ", you're " ^ (string_of_int age) ^ "-year-old."
| Instagram name => "Hello " ^ name ^ "!"
}
The standard library exposes two important variants you'll come to hear a lot.
option
type option 'a = None | Some 'a;
This is the convention used to simulate a "nullable" (aka undefined
or null
) value in other languages. Thanks to this convenience type definition, Reason can default every value to be non-nullable. An int
will always be an int, never "int
or null
or undefined
". If you do want to express a "nullable int", you'd use option int
, whose possible values are None
or Some int
. switch
forces you to handle both cases; therefore, a pure Reason program doesn't have null errors.
list
type list 'a = Empty | Head 'a (list 'a);
Not the actual type definition. Just an illustration.
This says: "a list that holds a value of type a
(whatever it is) is either empty, or holds that value plus another list".
Reason gave list
a syntax sugar. [1, 2, 3]
is conceptually equivalent to Head 1 (Head 2 (Head 3 Empty))
. Once again, switch
forces you to handle every case of this variant, including Empty
(aka []
). This eliminates another big category of access bugs.
Did you know that you can use switch
on string, int, float, array, and most other data structures? Try it!
Be careful not to confuse a constructor carrying 2 arguments with a constructor carrying a single tuple argument:
type account =
| Facebook string int /* 2 arguments */
type account2 =
| Instagram (string, int) /* 1 argument - happens to be a 2-tuple */
If you come from an untyped language, you might be tempted to try type foo = int | string
. This isn't possible in Reason; you'd have to give each branch a constructor: type foo = Int int | String int
. Though usually, needing this might be an anti-pattern. The Design Decisions section below explains more.
This section assumes knowledge about BuckleScript's FFI. Skip this if you haven't felt the itch to use variants for binding to JS functions yet.
Quite a few JS libraries use functions that can accept many types of arguments. In these cases, it's very tempting to model them as variants. For example, suppose there's a myLibrary.draw
JS function that takes in either a number
or a string
. You might be tempted to bind it like so:
/* reserved for internal usage */
external draw: 'a => unit = "draw" [@@bs.module "myLibrary"];
type animal =
| MyFloat float
| MyString string;
let betterDraw animal =>
switch animal {
| MyFloat f => draw f
| MyString s => draw s
};
You could definitely do that, but there are better ways! For example, simply two external
s that both compile to the same JS call:
external drawFloat: float => unit = "draw" [@@bs.module "myLibrary"];
external drawString: string => unit = "draw" [@@bs.module "myLibrary"];
Or, get fancy and use an advanced feature of variant called GADT, then use BuckleScript's phantom argument FFI feature. If these words mean absolutely nothing to you, no worries; just use the previous suggestion.
Please refer to this record section. Variants are the same: a function can't accept an arbitrary constructor shared by two different variants. Again, such feature exists, it's called a polymorphic variant. We'll talk about this in the future =).
Variant in its many forms (polymorphic variant, open variant, GADT, etc.) are likely the feature of a type system such as Reason's. The aforementioned option
variant, for example, obliterates the need for nullable types, a major source of bugs in other languages. Philosophically speaking, a problem is composed of many possible branches/conditions. Mishandling these conditions is the majority of what we call bugs. A type system doesn't magically eliminates bugs; it points out the unhandled conditions and asks you to cover them*. The ability to model "this or that" correctly is crucial.
For example, some folks wonder how the type system can safely eliminate badly formatted JSON data from propagating into their program. They don't, not by themselves! But if the parser returns the option
type None | Some actualData
, then you'd have to handle the None
case explicitly in later call sites. That's all there is.
Performance-wise, a variant can potentially tremendously speed up your program's logic. Here's a piece of JavaScript:
let data = 'dog';
if (data === 'dog') {
...
} else if (data === 'cat') {
...
} else if (data === 'bird') {
...
}
There's a linear amount of branch checking here (O(n)
). Compare this to using a Reason variant:
type animal = Dog | Cat | Bird;
let data = Dog;
switch data {
| Dog => ...
| Cat => ...
| Bird => ...
}
The compiler sees the variant, then
conceptually turns them into type animal = 0 | 1 | 2
compiles switch
to a constant-time format (O(1)
).
You might wonder why typed functional languages are used so often for parsing; switching on a large tree efficiently and safely is pretty much the best-case scenario for variants.
Mind blown yet? Variants have a deep connection to other fields of mathematics; See here for an interesting exploration.
* It's always nicer to design away the problem rather than resorting to a type system to cover the pitfalls; In reality, it's unrealistic to do so for every problem, or even just to understand every problem fully in order to design a solution. A type system allows you to safely make a big category of changes to codebases without needing to understand the whole thing upfront. In this regard, types also allows us not needing to overly design an API just to circumvent callers' simple pitfalls. They reduce the layers of abstractions needed to "get things done", which in return reduces callers' cognitive burden.