Rajat S

@geeky_writer_

Get Reason-able with ReasonML — Part 1

RTOP, Datatypes, Let Bindings, Lexical Scoping, If-Else and Switch, Records, and Variants

More than being a new language, ReasonML (Reason for short) is a syntax and toolchain that is powered by OCaml. It gives OCaml a familiar syntax that is geared towards JavaScript programmers, and also caters to the NPM/Yarn workflow.

With the help of BuckleScript, Reason can compile into readable JavaScript code. It can also be compiled to a fast, bare-bone assembly.

Reason is statically typed, providing us with a better clarity while evolving the code base. Instead of writing all the types all the time, the compiler itself can infer most of the types.

This is part 1 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.

Setup and RTOP

Reason CLI

Before we begin, make sure that you have installed the latest version of Reason CLI on your system.

If you are using macOS, open a terminal and enter this command.

npm install -g reason-cli@3.1.0-darwin

This will install Reason CLI on your system, along with Reformat and Merlin. You will also get RTOP, an interactive command line tool. This tool will make things really easy for us.

Using RTOP

If you type rtop in your command terminal, you will something like this:

If you type some basic math like 1 + 1;, RTOP will get evaluate it and get you the result.

Reason # 1 + 1;
- : int = 2

RTOP’s output is made up of three things.

let binding: type definition = result

If you don’t type the semicolon after 1 + 1, RTOP will not trigger the evaluation.

You might have noticed that there is list at the bottom of the terminal. This list will give us a possible set of modules and functions that we can use.

When we print out the value, we will see the value and the output of the evaluation.

Reason # print_int(42);
42- : unit = ()

Datatypes and Operators

Ints and Floats

If you try to do 1.1 + 2.2 using RTOP, you will get an error message because Reason has a special operator for working on float values.

In order to perform operations of float values, we need to add a . next to the operator. So 1.1 + 2.2 will become:

Reason # 1.1 +. 2.2
- : float = 3.30000000000000027

Comparing values

To compare values, we can use several relational operators, or structural equality using ==.

Reason # 2 > 3;
- : bool = false
Reason # 2 == 3;
- : bool = false

You cannot directly compare an int value with a float value. To do so, we will need to first convert the int into a float.

Reason # float_of_int(2) > 3.1;
- : bool = false

Reason comes with many such utility functions that can help us convert types.

Reason # bool_of_string(“true”);
- : bool = true;

Boolean

Since booleans can only be true or false, there isn’t much difference in Reason from what we usually do in JavaScript. Boolean operators are ! for NOT, && for AND, and || for OR.

Strings

Strings are also straight forward. They are only limited by the use of "". Strings can be concatenated using ++.

Reason # "Hello" ++ "World"
- : string = "HelloWorld"

Reason also comes with a special data type for single-letter strings.

Reason # 'a';
- : char = 'a';

Null

Passing null value in Reason is similar to how we do it in JavaScript. Null is defined with () and has its own type called unit.

Reason # ();
- : unit = ()

Let Bindings, Type Inference and Type Aliases

Let Bindings

Let bindings allow us to bind values to names in a way that is very similar to variable declaration in other languages.

Using let, we can bind a string value to a variable like this:

Reason # let name: string = "Rajat";
let name: string = "Rajat";

The general pattern of let binding look like this:

Reason # let <name>: <type> = <expression>;

If you are coming from a JavaScript background, then you might wonder why we need to provide the type. This is because Reason is statically typed, which is quite different from a language like JavaScript, which is dynamically typed. Statically typed languages requires us to declare or infer the types at compile time.

Type Inference

We have seen how to declare our types. Now let’s take a look at how to infer them.

Reason # let rajat = "Rajat";
let rajat: string = "Rajat";

The Reason compiler infers that the type of value is a string. This is a great feature as it allows us to have full type safety without declaring the types all the time. This means that types are optional in Reason, but you also explicitly write them down if you want to.

Immutability

let bindings are immutable. So if we bind a value to a variable, we cannot change it later on.

But, we can create a new let binding of the same name. This new binding will shadow over the previous the binding, and the binding will now refer to the newly-assigned value.

Type Aliases

After shadowing a let binding, it has nothing to do anymore with the previous one. So we can even use a different type for the new binding.

Reason # type score = int;
type score = int;
Reason # let x: score = 10;
let x: score = 10;

Lexical Scoping

Reason has Lexical Scoping. This sets the scope (range of functionality) of a variable so that it may only be referred from within the block of code in which it is defined. A variable declared like this is sometimes referred to as a private variable.

First, I will create a local scope in Reason using RTOP.

Reason # { 100; };
- : int = 100

The scope can contain multiple imperative statements. The last statement will be automatically returned.

Reason # 
{ print_endline("Rajat"); 100; };
Rajat
- : int = 100

Inside the scope, we can access bindings that are outside the current scope. But let bindings defined defined inside a scope aren’t accessible from the outside.

Reason # 100;
- : int = 100
Reason # let x = 10;
let x: int = 10;
Reason # {100 + x; };
- : int = 110
Reason #
{
let y = 1;
110 + y;
};
- : int = 111

We can also shadow a let binding inside a scope, it won’t affect the let bindings outside this local scope, even with different types.

Reason # let rajat = "Rajat";
let rajat: string = "Rajat";
Reason #
{
let rajat = 100;
rajat;
};
- : int = 2
Reason # rajat;
- : string = "Rajat"

If-Else and Switch

If-Else

if-else allows us to perform different expressions based on the provided condition.

Reason # let isHungry = true;
let isHungry: bool = true;
Reason # if (isHungry) {"Pizza!"} else {"Still Pizza!"};
- : string = "Pizza"

Here, if is an expression and therefore can be reduced to a value. This means it can be bound to a let binding. In languages like JavaScript, if is a statement, not an expression. Trying to bind it to a name will throw a syntax error.

The fact that if is an expression in Reason can be quite useful. But it also comes with its limitations. Every branch of if-else needs to be evaluated to the same type, so we can’t do the following:

Reason # let food = if (isHungry) {"Pizza"};
Error: This expression has type string but an expression was expected of type unit

So we can still use if for things like printing a value as long as the last statment returns the type unit. print_endline does so.

Reason # if (isHungry) {print_endline("isHungry is set to true")};
isHungry is set to true
- : int = ()

Switch

Switch accepts a value and matches it against the pattern. The case of the matching, which has to be an expression, then is evaluated. In its simplest form, pattern just matches for structural equality.

Reason # let lamp =
switch (1) {
| 0 => "off"
| 1 => "on"
| _ => "off"
};
let lamp: string = "on";
Reason # lamp;
- : string = "on"

If you don’t add the switch statement for _, then Reason will keep throwing you warning for case 2, then 3, and so on. _ is like the default case for switch in JavaScript.

Pattern matching can be done with any type of data. For a string:

switch ("Evie") {
| "Altair" => "One"
| "Ezio" => "Two"
| "Connor" => "Three"
| "Edward" => "Black Flag"
| "Arno" => "Unity"
| "Jacob" => "Syndicate"
| _ => "Unknown"
};
_ : string = "Unknown"

Records

Records allow us to store various types of data into one structure and reference them by name.

To create a record, we first have to define its structure.

type super = {
hero: string,
alias: string,
};

Once defined, we can create a record for a superhero like this:

{ hero: "Superman", alias: "Clark Kent" };
- : super = {hero: "Superman", alias: "Clark Kent"}

The type is inferred by default and we do not need to specify it. We can also access a particular field of a record like this:

# let super = { hero: "Superman", alias: "Clark Kent"};
let super: super = {hero: "Superman", alias: "Clark Kent"};
# super.hero;
- : string = "Superman"

If you try to access a field that doesn’t exist for a record, Reason will throw you an error.

Alternatively, you can leverage structuring in combination with the let binding. We start with a let binding, then describe how to map the fields, and place the record to be structured.

# let {hero: heroName, alias: aliasName} = super;
let heroName: string = "Superman";
let aliasName: string = "Clark Kent";

We can also restructure a particular field of a record.

# let {hero: heroName} = super;
let heroName: string = "Superman";

Variants

Variants allow us to express module options that are exclusive to a data structure.

# type answer = Yes | No | Maybe;

This is a variant that is referrring to a set of tags. Note that tags in variants need to be capitalized.

We use let to bind these tags:

# let isItRaining: Yes;
let isItRaining: answer = Yes;

Using variants, we can express anything with as many options as we want. Variant is most useful with the switch expression. It allows us to check every possible case of a variant.

# let message = 
switch(isItRaining) {
| Yes => "Better take an umbrella"
| No => "Ok then"
| Maybe => "So take an umbrella to be safe"
};
let message: string = "Better take an umbrella";

The same could be achieved using an if-else expression. But by using a variant with a switch gives us a great number of type system assistants. For example, the compiler will give us a type error if we forget to cover a case.

Each tag of a variant can hold extra data. Say you want to create an app that acts as both, a Note as well as a To-do. The Note will only expect a string input, whereas the todo requires a string as well as a boolean input to indicate if the task is completed or not.

# type item = Note(string) | Todo(string, bool);
# let myItem = Todo("write article", false);

We can use destructuring to do this. This way, we can give the parameters’ names and use them each after the arrow.

# switch (myItem) {
| Note(text) => text
| Todo(text, checked) => text ++ " is done: " ++ string_of_bool(checked)
};
- : string = "write article: false"

We can give them names, we don’t necessarily have to. We can also match against exact values of the tag.

We can add a pattern that matches exactly against a redesigned website with the boolean set to false.

# switch(myItem) {
| Note(text) => text
| Todo("redesign website", false) => "Please first fix the app"
| Todo(text, checked) => text ++ " is done: " ++ string_of_bool(checked)
};

I am Rajat S. 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