Rajat S

@geeky_writer_

Get Reason-able with ReasonML — Part 2

Illegal States, Option Type, Functions, Tuples, Lists, Arrays, and Equality

This is Part 2 of a 3 part series where I will help you make sense of ReasonML and all of its syntax and semantics. I will go through everything from basic datatypes to declaring functions in ReasonML.

If you haven’t read Part 1 yet, I suggest you to do so first, as all the posts in this series are linked to one another.

Eliminating Illegal States with Variant

Expressing multiple options that are exclusive as a data structure is one of ReasonML’s powerful concepts. But to use it wisely, we need to rethink on how to model our data.

First, I am going to create a simple app that fetches and renders some data. For this app, I first need to define the UI states. I want the app to print a loading text while we are waiting for the data, and print some text when app is successful or unsuccessful in fetching data.

We start by declaring your record type request. We add a field loading of type bool to indicate if the request is loading. Do the same for the error field. I also need to add a name field of type string. name is the data I want my app to fetch.

The logic is straightforward. If the request is currently loading, we return loading. In case it fails, we print Something went wrong. When the request succeeds, we will print the name.

# type request = {
loading: bool,
error: bool,
name: string,
};
# let myRequest = {
loading: false,
error: false,
name: "Rajat"
};
- : string = "Rajat"

But there is still a lot that could go wrong. For example, what if someone sets both loading and error to true?

The if/else expression renders loading, but by definition that should never happen. Due to the way our data is structured, it opens up the question, what should our UI code do in such a case? Maybe show the error instead of loading?

We can use variants to deal with this problem. I will recreate the type request with a variant. Each tag represents one of the states, loading, error, success. Success was an additional string for the same.

# type request = Loading | Error | Success(string);

Set the state and write a switch expression for a text to be rendered:

# let state = Loading;
# let ui =
| Loading => "Loading..."
| Error => "Something went wrong"
| Success => "Your name is " ++ name
};

We can change the state and rerun our switch expression. All cases work as expected. The great thing about this is that if we add a request variant, we can never have loading, error, and success at the same time.

Using the switch expression with variant will prevent us from forgetting a state. Reason will throw you an error if you miss a state.

This will solve most of our problems. But there still one more case we need to take care of. What if there is no data stored in your backend. Then the app will return an empty string an the output will be something like Your name is .

To take care of this broken UI, we will match an empty string and return an appropriate case.

# let ui =
switch(state) {
| Loading => "Loading..."
| Error => "Something went wrong"
| Success("") => "Your name is missing"
| Success => "Your name is " ++ name
};

In a real-world case, the Success constructor will contain another data structure like another variant such as Record or List.

# type userResponse = {
id: int,
name: string,
age: int,
};
# type request = Loading | Error | Success(userResponse);
# let state = Success({id: 1, name: "Rajat", age: 22});

Note: The type system doesn’t eliminate bugs. It can only point out the unhandled conditions and ask you to cover them.

Option Type

Reason doesn’t do null. If you try to bind null with age whose type is int, it will not work.

# let age: int = null;
Error Unbound value null

Some might say that this is great. null is the cause of many, many errors in our app. But null does serve a purpose. For example, you want to refer to something in your app, but you yet do not know if the corresponding value is available.

This is why Reason offers us with a variant called option. This variant has two tags:

  • None — This is to indicate that no value is available.
  • Some — This is to indicate that there is a value.
# None;
- : option('a) = None
# Some(42);
- : option(int) = Some(42)

We can use these with a name like this:

# let name = None;
let name: option('a) = None;
# let name = Some("Rajat");
let name: option(string) = Some("Rajat")

Like any other variant, we can also use it with a switch expression.

let message = 
switch (name) {
| None => Sadly I don't know"
| Some(value) => "The meaning of life is: " ++ value
};

With option, we have a tool at our disposal that allows us to simulate a novel value while still being type-safe, meaning a pure Reason program doesn’t have null errors.

Functions

Declaring and using functions in Reason is quite straightforward. An anonymous function looks like this:

# (x) => x + 1;
- : int => int = <fun>

A function in Reason is defined by one or more parameters inside a parenthesis. The parenthesis can be omitted if you only have a single parameter.

# let plusOne = x => x + 1;
# plusOne(4);
- : int = 5

To write a function that adds an integer to a float, we can do something like this:

# let add = (x, y) => {
let z = float_of_int(x);
y + z;
};

Another feature of the language is that its partial application of arguments. We can add two integers together by calling the function with two arguments.

# let add = (x, y) => x + y;
# add(3, 2);
- : int = 5

Providing one argument is also possible. Instead of throwing an error, it will return as a function where only one, in our case the second parameter, needs to be provided. Let’s bind the function to a name and use it.

# let addThree = add(3);
# addThree(2);
- : int = 5;

Chaining Functions

Concise and readable code is an important aspect of good quality and maintainability. The reverse-application operator | allows us to chain functions together without the hassle of creating an intermediate let bindings or complicated nesting.

I am going to create a list of functions that when used together, will take a string, capitalize it, and then convert it to lowercase elements.

# let info = String.capitalize(String.lowercase("ALERT"));

Using the reverse-application operator |, we can do this instead:

# let info = "ALERT" |> String.lowercase |> String.capitalize;

Combining with the partial application of arguments, this syntax can be quite useful.

# [8, 3, 6, 1] 
|> List.sort(compare)
|> List.rev
|> List.find(x => x < 4);
- : int = 3

Recursive Functions

Function recursion requires the let binding to be accessible inside it’s own local scope. Using the rec keyword, we can enable this visibility to declare recursion functions.

I am creating a function that will count up to 10 and bring out the parameter after each increment.

# let countUntilTen = x => {   
if (x < 10) {
print_int(x)
countUntilTen(x + 1);
};
};
Error: Unbound value countUntilTen

The reason we are getting this error is that the function body does not have access to the let binding that the function points to. But including the rec keyword makes it possible. This will allow the function to see and call themselves to provide us the power of recursion.

# let rec countUntilTen = x => {
if (x < 10) {
print_int(x)
countUntilTen(x + 1);
};
};
let countUntilTen: int => unit = <fun>;
# countUntilTen(6);
6789- : unit = ()

If we want to create mutually recursive functions, we can do this by starting with a single recursive function using the rec keyword, and then add a second one using the and keyword.

# let rec add = x => ! even(x)
and even = x => {
if(x == 0) {
true;
} else {
odd(x - 1);
};
};
let odd: int => bool = <fun>;
let even: int => bool = <fun>;

Tuples, Lists and Arrays

Tuples

Tuple is a type of data structure that allows us to store a fixed number of values of any kind of type.

# ("Anna", 24);
- : (string, int) = ("Anna", 24)

Each element is identified by it’s position, and not its name. We can also nest a tuple.

# type point = (int, int);
# let myPoint: point = (4, 3);

To access elements of a tuple, Reason provides us with two convenient functions to access the first two elements. fst will access the first item and snd to access the second one.

# fst((0,2));
- : int = 0
# snd((0,2));
- : int = 2

Let’s take a look at another tuple:

# let (a,b,c) = ("Rajat","Writer",23);
let a: string = "Rajat";
let b: string = "Writer";
let c: int = 23;

Here, if you do not want to assign a value to each position, you can simply replace that value with _.

Every tuple is immutable. So we cannot update the tuple, we can only create new ones.

Lists

Unlike Tuples, Lists are homogenous and immutable. So we cannot store different types of items in a List.

# let List = ["Rajat", "Writer"];

But also, Reason allows us to use a variant to keep different types and create a list of tags.

# type rajat = Name(string) | Age(int);
# [Name("Rajat"), Age(23)];

Being immutable, we can append, prepend, or even replace an item in the List.

# List.append(["Rajat"],["Writer"]);
- : list(string) = ["Rajat", "Writer"]
# ["Rajat"] @ ["Writer"]
- : list(string) = ["Rajat", "Writer"]

To prepend one or multiple elements to the list, use the ... operator. Note that ... can only be used for prepending elements.

# [0, ...[1, 2, 3]];
- : list = [0, 1, 2, 3]

To access a list, you need to use the switch expression.

# let list = ["Rajat", "Writer"];
# let message =
switch(list) {
| [] => "There is no data"
| [head, ...rest] => "My name is " ++ head
};
let message: string = "My name is Rajat";

This can get pretty complicated. We can instead use List.nth to access elements in the list.

# List.nth([2, 3], 0);
- : int = 2

But again, this method can raise exceptions if your list is too small.

Instead, we can use List.map to manipulate each item inside the List. List.map can also accept a function as its first parameter and the local List as the second parameter.

# List.map(x => x * 5, [2, 4]);
- : list(int) = [10, 20]

We have other functions in Reason such as find, filter, sort, exist, split to help do more with Lists.

Arrays

Arrays are similar to Lists, except for the facts that the Array’s content is wrapped in pipes and brackets and unlike Lists, Arrays are mutable.

# let array = [|2, 3|];
# array[0];
- : int = 2
# let array[0] = 3;

Equality

There are generally two types of equality in any programming language:

  • Structural Equality — Where you compare if the two values have the same content.
  • Reference Equality — Where you check if the two values point to the same representation in memory.

Structural Equality

Structural Equality is used in comparing data structures, boolean values, tuples, lists, and records.

# "Rajat" == "Rajat";
- : bool = true
# true == false;
- : bool = false;

In Reason, we can use structural equality to even compare nested structures and also check for inequality like this:

# (false, [{name: "Rajat", job: "writer"}]) != (true, [(name: "Rajat", job: "writer"}]);
- : bool = false

Reference Equality

Unlike structural equality, reference equality doesn’t compare the content stored within the values. Instead, it checks if the values are point to the same representation in memory.

# let rajat = {name: "Rajat", job: "Writer"};
# rajat === rajat;
- : bool = true
# rajat === {name: "Rajat", job: "Writer"};
- : bool false

There are only a few scenarios where you will get to use reference equality. And though it is a very quick check, it might also cause our app to miss if a value with identical content is passed in. Comparing two integers referentially will always result in true to how they are implemented.

# 24 === 24;
- : bool = true

To Be Continued…

This concludes the Part 2 of 3 of this series. Part 3 will be out soon, so hold on tight :)

Here is Part 1, if you haven’t read it yet.

I am Rajat S, Technical Content Writer at GeekyAnts. Aspiring Coder who has a long way to go. A Die-Hard DC Comics Fan who loves Marvel Movies. Known for multi tasking.

Thanks for reading, and hopefully this was helpful! Please 👏 if you liked this post and follow me here and/or on Twitter to stay updated about new posts from me!

More by Rajat S

Topics of interest

More Related Stories