Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
ReasonML Quick Start Guide

You're reading from   ReasonML Quick Start Guide Build fast and type-safe React applications that leverage the JavaScript and OCaml ecosystems

Arrow left icon
Product type Paperback
Published in Feb 2019
Publisher Packt
ISBN-13 9781789340785
Length 180 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (2):
Arrow left icon
Bruno Joseph D'mello Bruno Joseph D'mello
Author Profile Icon Bruno Joseph D'mello
Bruno Joseph D'mello
Raphael Rafatpanah Raphael Rafatpanah
Author Profile Icon Raphael Rafatpanah
Raphael Rafatpanah
Arrow right icon
View More author details
Toc

Exploring Reason

Ask yourself whether the following is a statement or an expression:

let foo = "bar";

In JavaScript, it's a statement, but in Reason, it's an expression. Another example of an expression is 4 + 3, which can also be represented as 4 + (2 + 1).

Many things in Reason are expressions, including control structures such as if-else, switch, for and while:

let message = if (true) {
"Hello"
} else {
"Goodbye"
};

We also have ternaries in Reason. Here is another way to express the preceding code:

let message = true ? "Hello" : "Goodbye";

Even anonymous block scopes are expressions that evaluate to the last line's expression:

let message = {
let part1 = "Hello";
let part2 = "World";
{j|$part1 $part2|j};
};
/* message evaluates to "Hello World" */
/* part1 and part2 are not accessible here */

A tuple is an immutable data structure that can hold different types of values and can be of any length:

let tuple = ("one", 2, "three");

Let's use what we know so far and dive right in with the FizzBuzz example from Reason's online playground. FizzBuzz was a popular interview question to determine whether a candidate is able to code. The challenge is to write a problem that prints the numbers from 1 to 100, but instead prints Fizz for multiples of three, Buzz for multiples of five, and FizzBuzz for multiples of both three and five:

/* Based on https://rosettacode.org/wiki/FizzBuzz#OCaml */
let fizzbuzz = (i) =>
switch (i mod 3, i mod 5) {
| (0, 0) => "FizzBuzz"
| (0, _) => "Fizz"
| (_, 0) => "Buzz"
| _ => string_of_int(i)
};

for (i in 1 to 100) {
Js.log(fizzbuzz(i))
};

Here, fizzbuzz is a function that accepts an integer and returns a string. An imperative for loop logs its output to the console.

In Reason, a function's last expression becomes the function's return value. The switch expression is the only fizzbuzz expression, so whatever that evaluates to becomes the output of fizzbuzz. Like JavaScript, the switch evaluates an expression and the first matched case gets its branch executed. In this case, the switch evaluates the tuple expression: (i mod 3, i mod 5).

Given i=1, (i mod 3, i mod 5) becomes (1, 1). Since (1, 1) isn't matched by (0, 0), (0, _), or (_, 0), in that order, the last case of _ (that is, anything) is matched, and "1" is returned. Similarly, fizzbuzz returns "2" when given i=2. When given i=3, "Fizz" is returned.

Alternatively, we could have implemented fizzbuzz using if-else:

let fizzbuzz = (i) =>
if (i mod 3 == 0 && i mod 5 == 0) {
"FizzBuzz"
} else if (i mod 3 == 0) {
"Fizz"
} else if (i mod 5 == 0) {
"Buzz"
} else {
string_of_int(i)
};

However, the switch version is much more readable. And as we'll see later in this chapter, the switch expression, also called pattern matching, is much more powerful than we've seen so far.

Data structures and types

A type is a set of values. More concretely, 42 has the int type because it's a value that's contained in the set of integers. A float is a number that includes a decimal point, that is, 42. and 42.0. In Reason, integers and floating point numbers have separate operators:

/* + for ints */
40 + 2;

/* +. for floats */
40. +. 2.;

The same is true for -., -, *., *, /., and /.

Reason uses double quotes for the string type and single quotes for the char type.

Creating our own types

We can also create our types:

type person = (string, int);

/* or */

type name = string;
type age = int;
type person = (name, age);

Here's how we create a person of the person type:

let person = ("Zoe", 3);

We can also annotate any expression with its type:

let name = ("Zoe" : string);
let person = ((name, 3) : person);

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.

Making Invalid States Impossible

Let's say that we'd like to add a field to seat to hold the date a seat was purchased:

type seat = {
section,
price: int,
person: option(person),
dateSold: option(string)
};

Now, we've introduced the possibility of an invalid state in our code. Here's an example of such a state:

let seat = {
section: Pit,
price: 42,
person: None,
dateSold: Some("2018-07-16")
};

In theory, the dateSold field should only hold a date when the person field holds a ticket holder. The ticket has a sold date, but no owner. We could look through our imaginary implementation to verify that this state would never happen, but there would still be the possibility that we missed something, or that some minor refactor introduced a bug that was overlooked.

Since we now have the power of Reason's type system at our disposal, let's offload this work to the compiler. We are going to use the type system to enforce invariants in our code. If our code breaks these rules, it won't compile.

One giveaway that this invalid state could exist is the use of option types within our record field. In these cases, there may be a way to use a variant instead such that each constructor only holds the relevant data. In our case, our sold-date and ticket-holder data should only exist when the seat has been sold:

type person = {
age: int,
name: string,
};

type date = string;

type section =
| Pit
| Floor
| Mezzanine
| Balcony;

type status =
| Available
| Sold(date, person);

type seat = {
section,
price: int,
status
};

let getSeatPrice = (seat) => seat.price;

let isAvailable = (seat) =>
switch (seat.status) {
| Available => true
| Sold(_) => false
};

Check out our new status type. The Available constructor holds no data, and Sold holds the sold date as well as the ticket holder.

With this seat type, there's no way to represent the previous invalid state of having a sold date without a ticket holder. It's also a good sign that our seat type no longer includes option types.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime