- ReasonML Quick Start Guide
- Raphael Rafatpanah Bruno Joseph D'mello
- 1326字
- 2021-07-02 12:34:15
Pattern matching
We can use pattern matching on our person:
switch (person) {
| ("Zoe", age) => {j|Zoe, $age years old|j}
| _ => "another person"
};
Let's use a record instead of a tuple for our person. Records are similar JavaScript objects except they're much lighter and are immutable by default:
type person = {
age: int,
name: string
};
let person = {
name: "Zoe",
age: 3
};
We can use pattern matching on records too:
switch (person) {
| {name: "Zoe", age} => {j|Zoe, $age years old|j}
| _ => "another person"
};
Like JavaScript, {name: "Zoe", age: age} can be represented as {name: "Zoe", age}.
We can create a new record from an existing one using the spread ( ... ) operator:
let person = {...person, age: person.age + 1};
Records require type definitions before they can be used. Otherwise, the compiler will error with something like the following:
The record field name can't be found.
A record must be the same shape as its type. Therefore, we cannot add arbitrary fields to our person record:
let person = {...person, favoriteFood: "broccoli"};
/*
We've found a bug for you! This record expression is expected to have type person The field favoriteFood does not belong to type person
*/
Tuples and records are examples of product types. In our recent examples, our person type required both an int and an age. Almost all of JavaScript's data structures are product types; one exception is the boolean type, which is either true or false.
Reason's variant type, which is an example of a sum type, allows us to express this or that. We can define the boolean type as a variant:
type bool =
| True
| False;
We can have as many constructors as we need:
type decision =
| Yes
| No
| Maybe;
Yes, No, and Maybe are called constructors because we can use them to construct values. They're also commonly called tags. Because these tags can construct values, variants are both a type and a data structure:
let decision = Yes;
And, of course, we can pattern match on decision:
switch (decision) {
| Yes => "Let's go."
| No => "I'm staying here."
| Maybe => "Convince me."
};
If we were to forget to handle a case, the compiler would warn us:
switch (decision) {
| Yes => "Let's go."
| No => "I'm staying here."
};
/*
Warning number 8 You forgot to handle a possible value here, for example: Maybe
*/
As we'll learn in Chapter 2, Setting Up a Development Environment, the compiler can be configured to turn this warning into an error. Let's see one way to help make our code more resilient to future refactors by taking advantage of these exhaustiveness checks.
Take the following example where we are tasked with calculating the price of a concert venue's seat given its section. Floor seats are $55, while all other seats are $45:
type seat =
| Floor
| Mezzanine
| Balcony;
let getSeatPrice = (seat) =>
switch(seat) {
| Floor => 55
| _ => 45
};
If, later, the concert venue allows the sale of seats in the orchestra pit area for $65, we would first add another constructor to seat:
type seat =
| Pit
| Floor
| Mezzanine
| Balcony;
However, due to the usage of the catch-all _ case, our compiler doesn't complain after this change. It would be much better if it did since that would help us during our refactoring process. Stepping through compiler messages after changing type definitions is how Reason (and the ML family of languages in general) makes refactoring and extending code a safer, more pleasant process. This is, of course, not limited to variant types. Adding another field to our person type would also result in the same process of stepping through compiler messages.
Instead, we should reserve using _ for an infinite number of cases (such as our fizzbuzz example). We can refactor getSeatPrice to use explicit cases instead:
let getSeatPrice = (seat) =>
switch(seat) {
| Floor => 55
| Mezzanine | Balcony => 45
};
Here, we welcome the compiler nicely informing us of our unhandled case and then add it:
let getSeatPrice = (seat) =>
switch(seat) {
| Pit => 65
| Floor => 55
| Mezzanine | Balcony => 45
};
Let's now imagine that each seat, even ones in the same section (that is, ones that have the same tag) can have different prices. Well, Reason variants can also hold data:
type seat =
| Pit(int)
| Floor(int)
| Mezzanine(int)
| Balcony(int);
let seat = Floor(57);
And we can access this data with pattern matching:
let getSeatPrice = (seat) =>
switch (seat) {
| Pit(price)
| Floor(price)
| Mezzanine(price)
| Balcony(price) => price
};
Variants are not just limited to one piece of data. Let's imagine that we want our seat type to store its price as well as whether it's still available. If it's not available, it should store the ticket holder's information:
type person = {
age: int,
name: string,
};
type seat =
| Pit(int, option(person))
| Floor(int, option(person))
| Mezzanine(int, option(person))
| Balcony(int, option(person));
Before explaining what the option type is, let's have a look at its implementation:
type option('a)
| None
| Some('a);
The 'a in the preceding code is called a type variable. Type variables always start with a '. This type definition uses a type variable so that it could work for any type. If it didn't, we would need to create a personOption type that would only work for the person type:
type personOption(person)
| None
| Some(person);
What if we wanted an option for another type as well? Instead of repeating this type declaration over and over, we declare a polymorphic type. A polymorphic type is a type that includes a type variable. The 'a (pronounced alpha) type variable will be swapped with person in our example. Since this type definition is so common, it's included in Reason's standard library, so there's no need to declare the option type in your code.
Jumping back to our seat example, we store its price as an int and its holder as an option(person). If there's no holder, it's still available. We could have an isAvailable function that would take a seat and return a bool:
let isAvailable = (seat) =>
switch (seat) {
| Pit(_, None)
| Floor(_, None)
| Mezzanine(_, None)
| Balcony(_, None) => true
| _ => false
};
Let's take a step back and look at the implementations of getSeatPrice and isAvailable. It's a shame that both functions need to be aware of the different constructors when they don't have anything to do with the price or availability of the seat. Taking another look at our seat type, we see that (int, option(person)) is repeated for each constructor. Also, there isn't really a nice way to avoid using the _ case in isAvailable. These are all signs that another type definition might serve our needs better. Let's remove the arguments from the seat type and rename it section. We'll declare a new record type, called seat, with fields for section, price, and person:
type person = {
age: int,
name: string,
};
type section =
| Pit
| Floor
| Mezzanine
| Balcony;
type seat = {
section, /* same as section: section, */
price: int,
person: option(person)
};
let getSeatPrice = seat => seat.price;
let isAvailable = seat =>
switch (seat.person) {
| None => true
| Some(_person) => false
};
Now, our getSeatPrice and isAvailable functions have a higher signal-to-noise ratio, and don't need to change when the section type changes.
As a side note, _ is used to prefix a variable to prevent the compiler from warning us about the variable being unused.