30 minute introduction to ReasonML for React Developers

theodesp

Theofanis Despoudis

Posted on March 23, 2020

30 minute introduction to ReasonML for React Developers

The next level of React Development is with ReasonML. It allows existing Javascript developers to write OCaml code. The major benefits here are type safe inference (much more pleasant and advanced than Typescript) and really fast compilation times (orders of magnitude faster than Typescript). Not to mention is also very fun to work with.

In this article we'll try to go through as many ReasonML snippets as we can, and explain what the keywords and symbols they contain mean.

Let's get started...

Variable Bindings

let introduces variable bindings. This works the same as const in Javascript:

let greeting = "Hello World"
Enter fullscreen mode Exit fullscreen mode

let bindings are immutable, thus they cannot change after the first assignment:

let greeting = "Hello World"
greeting = "Hello Again"
^^^^^^^^
Error
Enter fullscreen mode Exit fullscreen mode

The let assignment should be done immediately as the compiler needs to infer the type:

let greeting
greeting = "Hello Again"
^^^^^^^^
Error
Enter fullscreen mode Exit fullscreen mode

However, using a ref with a wrapped value passed though allows us to assign a new value later on:

let greeting = ref("")
greeting = "Hello World"
Enter fullscreen mode Exit fullscreen mode

You can create new scopes using braces {} and then assign the result to a binding. Everything you bind inside the scope is not available outside. The last expression evaluated is returned as the result. This is very useful for logical grouping of expressions and for better readability:

let fullName = {
  let first = "Theo";
  let last = "Despouds";
  first ++ " " ++ last
};
"Theo Despoudis"
Enter fullscreen mode Exit fullscreen mode

You can bind new values to existing variables. The value of the last binding is what is referred in subsequent calculations:

let a = 10.;
let a = 11;
let a = a * a; // 121
Enter fullscreen mode Exit fullscreen mode

Type Inference

When we use let without specifying the type, the compiler will infer it:

let a = 10.; // float
let a = 10; // int
let a = "abc"; // string
let a = 'a' // char
Enter fullscreen mode Exit fullscreen mode

If we want to be more explicit about the type we can declare it:

let a: float = 10.;
let a: int = 10;
let a: string = "abc";
let a: char = 'a';
Enter fullscreen mode Exit fullscreen mode

You can assign a different name to a type using type aliases:

type statusCode = int
let notFound: statusCode = 404;
Enter fullscreen mode Exit fullscreen mode

Note that a type name must start with a lower-case letter or an underscore. The following will fail:

type StatusCode = int
     ^^^^^^^^^^
let notFound: StatusCode = 404;
Enter fullscreen mode Exit fullscreen mode

The type system of ReasonML is completely "sound" compared to Typescript which is not. See this article for more information.

Strings

Strings are wrapped in double quotes. Characters are wrapped in single quotes. Strings can span multiple lines:

"aaa";
"bbb;
bbb";
'a';
Enter fullscreen mode Exit fullscreen mode

Strings are unicode encoded but chars are not. They are just ASCII so anything other than ASCII throws an error:

"α" // Greek letter alpha

'α';
^^^
Enter fullscreen mode Exit fullscreen mode

Booleans

true and false represent the bool type. All relevant operations that we use in Javascript work the same in ReasonML:

true && false;
true || true;
1 < 2;
2 >= 3;
2 == 2;
3 === 3;
Enter fullscreen mode Exit fullscreen mode

There are no binary or xor operators. The following would not work:

true | true;
false & true;
true ^ true;
Enter fullscreen mode Exit fullscreen mode

Numbers

There are two types of numbers. Integers and floats. Float numbers end with a dot . whereas ints do not.

We use standard operators for integers such as +, -, * and /.

We use different operators for floats such as +., -., *. and /..

1 + 10; // 11
1. +. 10.; // 11.

10. *. 5.; // 50.
Enter fullscreen mode Exit fullscreen mode

We cannot mix operations between types. The following expressions will fail:

1 +. 10 // +. works on floats only
1. + 10; // + works on ints only
Enter fullscreen mode Exit fullscreen mode

Lists and Arrays

Lists and Arrays are collections of similar items. Lists are immutable and the notation is the same as Javascript:

let groceryList = ["eggs", "pasta", "milk"];
Enter fullscreen mode Exit fullscreen mode

You cannot mix types:

let ids = [1,2, "3"];
                ^^^
Enter fullscreen mode Exit fullscreen mode

The type of list is list(<type>) for example list(int) or list(string).

There are not list methods available so you cannot do ids.length. Instead you need to use the List module methods, for example:

let ids: list(int) = [1, 2, 3];
List.length(ids); // 3
let ids = List.append(ids, [4]); // [1, 2, 3, 4]
Enter fullscreen mode Exit fullscreen mode

You can also use the spread(...) operator once to prepend items:

let ids: list(int) = [1, 2, 3];
let ids = [0, ...ids];
Enter fullscreen mode Exit fullscreen mode

Note that appending does not work. You need to use List.concat for anything else:

let ids = [...ids, 4];
           ^^^^^^
Enter fullscreen mode Exit fullscreen mode

To access a list index you need to use List.nth using 0-based indexing:

let ids: list(int) = [1, 2, 3];
let first = List.nth(ids, 0); // 1
Enter fullscreen mode Exit fullscreen mode

Arrays are mutable collections of similar items. We surround them with [| and |] and we can use standard index notation for access:

let ids: array(int) = [|1, 2, 3|];
let first = ids[0]; // 1
ids[0] = 4;
// ids = [|4, 2, 3 |]
Enter fullscreen mode Exit fullscreen mode

Conditional Expressions

if and else are expressions (they return a value) so we can assign them to let bindings. For example:

let ids: array(int) = [|1, 2, 3|];

let safeFirst = if (Array.length(ids) > 0) {
    ids[0]
} else {
    0
}
// safeFirst = 1
Enter fullscreen mode Exit fullscreen mode

You cannot have a naked if expression without an else one:

let ids: array(int) = [|1, 2, 3|];

let safeFirst = if (Array.length(ids) > 0) {
    ids[0]
}^^^^^^^^^^^^^
Enter fullscreen mode Exit fullscreen mode

There is also a ternary operator just like Javascript:

let isLoading = false;
let text = isLoading ? "Loading" : "Submit";
Enter fullscreen mode Exit fullscreen mode

Records

Records in ReasonML are like Objects in Javascript. However they have stronger type guarantees and are immutable:

type user = {
  name: string,
  email: string
};
// Type inference here. This will only work in the same file that the user type is defined.
let theo = {
  name: "Theo",
  email: "theo@example.com"
}
Enter fullscreen mode Exit fullscreen mode

Note that you cannot just define an object without a type:

let theo = {
  name: "Theo",
  ^^^^
  email: "theo@example.com"
}
Enter fullscreen mode Exit fullscreen mode

To use a Record defined in a different file you need to prefix the type. For example if we defined the user model in Models.re:

let theo: Models.user = {
  name: "Theo",
  email: "theo@example.com"
};
Enter fullscreen mode Exit fullscreen mode

Records are immutable:

type user = {
  name: string,
  email: string
};
let theo = {
  name: "Theo",
  email: "theo@example.com"
}

theo.name = "Alex"
^^^^^^^^^^^^^^^^^^
Enter fullscreen mode Exit fullscreen mode

But you can create another Record using the spread operator:

type user = {
  name: string,
  email: string
};
let theo = {
  name: "Theo",
  email: "theo@example.com"
}

let theo = {
  ...theo,
  name: "Alex"
}
// {name: "Alex", email: "theo@example.com"}
Enter fullscreen mode Exit fullscreen mode

Alternatively you can mark a field as mutable and perform updates:

type user = {
  mutable name: string,
  email: string
};
let theo = {
  name: "Theo",
  email: "theo@example.com"
}

theo.name = "Alex"
// {name: "Alex", email: "theo@example.com"}
Enter fullscreen mode Exit fullscreen mode

You can combine different types inside a Record type using type shorthands:

type email = string;
type username = string;

type user = {
  email,
  username
}
Enter fullscreen mode Exit fullscreen mode

Functions

Functions are like es6 lambda expressions. We use parenthesis and an arrow and return a value:

let addOne = (n) => n + 1;
addOne(2); // 3
Enter fullscreen mode Exit fullscreen mode

If the function spans multiple lines we can use a block scope:


let getMessage = (name) => {
  let message = "Hello " ++ name;
  message
}
getMessage("Theo"); // "Hello Theo" 
Enter fullscreen mode Exit fullscreen mode

By default, function arguments are positional and the order matters. We have the option to use named (or labeled) arguments (similar to Python) using the tilde (~) operator.

let getMessage = (~greeting, ~name) => {
  let message = greeting ++ " " ++ name;
  message
}
getMessage(~name="Hello", ~greeting="Theo"); // "Theo Hello"
Enter fullscreen mode Exit fullscreen mode

However once we use one named argument we have to use all of them and not skip anything:

let getMessage = (~greeting, ~name) => {
  let message = greeting ++ " " ++ name;
  message
}
getMessage(~name="Hello", "Theo");
                          ^^^^^^
Enter fullscreen mode Exit fullscreen mode

Any function with more than one argument is automatically carried:

let mul = (a, b) => a * b;
let times2 = mul(2);
let result = times2(3); // 6
Enter fullscreen mode Exit fullscreen mode

Recursive functions are declared via the rec keyword:

let rec fact (n) {
  if (n === 0) {
    1
  } else {
    fact(n-1) * n
  }
}

fact(5); // 120
Enter fullscreen mode Exit fullscreen mode

Nulls, Optionals and Undefined

There is no null or undefined in ReasonML. Instead we have the Option Monad which represent either a value - Some(value) or no value at all - None:

let userName = Some("Alex");
let userName = None;
let userName: option(string) = Some("Alex");
Enter fullscreen mode Exit fullscreen mode

You can use the Belt.Option module to perform common operations for Optionals:

let userName = Some("Theo");
print_string(string_of_bool(Belt.Option.isSome(userName))); // true
Belt.Option.isNone(userName); // false
Enter fullscreen mode Exit fullscreen mode

To check is some object is null or undefined (coming from a network response for example) you can use the following API methods:

Js.Nullable.isNullable();
Js.eqNull();
Js.eqUndefined();
Enter fullscreen mode Exit fullscreen mode

Tuples

Tuples are like lists but they can contain different types of items. For example:

let pair = (1, "Theo Despoudis");
let pair : (int, string) = (1, "Theo Despoudis");
Enter fullscreen mode Exit fullscreen mode

As with lists we cannot use the indexing operator [index]. Instead we need to use destructing to extract the i'th element. This makes tuples useful only when there are small in sized (< 3 elements):

let triplet = (1, "Theo Despoudis", "theo@example.com");
let (_, name, _) = triplet;  // use _ for ignoring the extracted value
name // "Theo Despoudis"
Enter fullscreen mode Exit fullscreen mode

Type Variants

Variants are like Union Types in Typescript. It allows us the describe an OR (|) relationship between two or more types:

type status =
  | NotFound
  | Error
  | Success;

let responseStatus = Error;
Enter fullscreen mode Exit fullscreen mode

You can also pass the types of the arguments in some or all of the Type Names of the variant types:

type animalType =
  | Dog(string)
  | Cat(string)
  | Bird;

let myDog = Dog("Wallace");
Enter fullscreen mode Exit fullscreen mode

You cannot use plain types as variants as they need to be Unique tag names or Types with a Constructor:

type number = int | float;
                  ^^^^^^^^
Enter fullscreen mode Exit fullscreen mode

Destructuring

We have seen destructuring before. When we have a tuple or a Record we can extract some or all of their fields to a binding:

type user = {id: int, name: string, email: string};
let me = {id: 1, name: "Theo", email: "theo@example.com"};
let {name, email} = me;
Enter fullscreen mode Exit fullscreen mode

The above is just a syntactic sugar for:

let name = "Theo";
let email = "theo@example.com"
Enter fullscreen mode Exit fullscreen mode

Pattern Matching

Pattern matching is the golden fleece of Functional Programming languages. Essentially they are switch statements on steroids. For example:

type result =
  | OK(string)
  | NotOK(string)
  | Empty;

let response = OK("Success!");

let log =
  switch (response) {
  | OK(message) => "OK:" ++ message
  | NotOK(message) => "Error: " ++ message
  | Empty => "Nothing happened!"
  };

log // OK:Success
Enter fullscreen mode Exit fullscreen mode

Pipes

Pipes act as syntactic shorthand for function composition. If you have 3 functions f, g, h and you want to call them like f(g(h(a))) you can instead use pipes to call them like:

a
 ->h
 ->g
 ->f
Enter fullscreen mode Exit fullscreen mode

For example:

let userName = Some("Theo");
print_string(string_of_bool(Belt.Option.isSome(userName)));

// or

userName
    -> Belt.Option.isSome
    -> string_of_bool
    -> print_string
Enter fullscreen mode Exit fullscreen mode

Modules

Modules are like namespaces. We use blocks {} to define a module name where we can associate similar types or bindings. This aims to improve the code organisation:

module Arena = {
  type warriorKind =
    | Gladiator(string)
    | Hoplite(string)
    | Archer(string);

  let getName = (warriorKind) =>
    switch (warriorKind) {
    | Gladiator(name) => name
    | Hoplite(name) => name
    | Archer(name) => name
    };
};

Enter fullscreen mode Exit fullscreen mode

Then when we need to reference a module in another file we use the module name:

let warrior: Arena.warriorKind = Arena.Gladiator("Brutus");
print_endline(Arena.getName(warrior)); // "Brutus"
Enter fullscreen mode Exit fullscreen mode

For convenience we can use a shorthand for the module name using the open keyword ideally within it's own block scope:

let event = {
  open Arena;
  let warrior: warriorKind = Gladiator("Brutus");
  print_endline(getName(warrior)); // "Brutus"
};
Enter fullscreen mode Exit fullscreen mode

Promises

Using the Js.Promisehttps://bucklescript.github.io/bucklescript/api/Js.Promise.html) module we can create or interact with promise objects:

let messagePromise =
  Js.Promise.make((~resolve, ~reject) => resolve(. "Hello"))
  |> Js.Promise.then_(value => {
       Js.log(value);
       Js.Promise.resolve("World");
     })
  |> Js.Promise.catch(err => {
       Js.log2("Failure!!", err);
       Js.Promise.resolve("Error");
     });
Enter fullscreen mode Exit fullscreen mode

Note that we prepended a dot . or the uncurry annotation before calling resolve as the compiler will complain. This is because we want the callbacks to be uncurried.

The above will compile to the following Javascript code:

var messagePromise = new Promise((function (resolve, reject) {
            return resolve("Hello");
          })).then((function (value) {
          console.log(value);
          return Promise.resolve("World");
        })).catch((function (err) {
        console.log("Failure!!", err);
        return Promise.resolve("Error");
      }));
Enter fullscreen mode Exit fullscreen mode

That's it

There are more little things to know about ReasonML but in this tutorial we explored the most common ones. Here are some further reference links for learning more about the ReasonML ecosystem:

💖 💪 🙅 🚩
theodesp
Theofanis Despoudis

Posted on March 23, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related