ReasonML: Making Types Have Your Back

I am forever grateful to Chirag Jain for contributing to JavaScript January.

Hi there, I have just started learning about ReasonML and loving it so far.

It's a new object-functional, OCaml based programming language created at Facebook.

The official website says: "Reason lets you write simple, fast and quality type safe code while leveraging both the JavaScript & OCaml ecosystems."

One of the hardest part of programs dealing with UI is state and side-effect management. David Khourshid's React rally talk Infinitely better UIs with Finite Automata (highly recommended watch) demonstrates it a hell lot better than I can.

A large chunk of the code we write is to keep different parts of the UI consistent with the state. It's impossible to keep track of all the possible states and the paths between them, inside your brain. It doesn't help that as the application grows the number of possible combination of states increase exponentially.

Imagine not having to keep track of all the places you need to make a change when a new state is introduced. What if there's a system to help you consider all the cases and make it impossible to represent invalid states inside your code, ReasonML's type system does exactly that when leveraged correctly.

Before I can show you how to do this, I need to introduce some major concepts that help you write better code in ReasonML:

  • Compile time type checking
  • Automatic type inference.
  • Immutability
  • No concept of Null.
  • No concept of return
  • All functions are curried.
  • Variant Types
  • Pattern Matching

Compile time type checking

ReasonML is a compiled language hence the compiler warns you about potential bugs at compile time i.e. less surprises in production.

Automatic type inference

The compiler tries to infer most of the types itself. This helps you leverage types without actually typing them out.

Immutability

All bindings are immutable by default i.e. once a value is assigned to a variable then it can't change, this might seem counterintuitive but this allows us to be confident as we can be sure that the value can't be changed by some other piece of code.

No concept of Null

There is no Null. We use the option type instead if we aren't certain that the value would be available or not. The () value, which is the only value of the unit type, to represent nothing.

No concept of return

The value of the last expression inside the scope is automatically returned and the default return value is () i.e. Nothing. Not having explicit returns inside the language makes it hard to write code with early returns, which tends to have multiple branch and is hard to reason about.

All functions are curried

All functions are curried i.e. you can create a new function by supplying a partial list of the required parameters. This makes it easy to create new functions from old functions and pass them around as values, which generally leads to more code reuse.

let mult = (a, b) => a * b;
let double = mult(2);

In this example we were able to create a double function which takes a single parameter from the mult function which takes two parameters and returns their product. In this way we can use currying to create specific functions from more generic ones.

Variant Types

Variants make it easy to explicitly specify the possible values of something.

type switchState = 
| ON
| OFF;

Here we define the switchState variant type which lists out the only two possible values that any variable of this type can hold, namely ON and OFF. This might not seem like a big deal, but we will delve in more complex use case later which will show the importance of the concept.

Pattern Matching

Pattern matching with switch expression, is where the power of ReasonML shines. It makes it a breeze to work with nested & complex data structures. In conjunction with all the concepts introduced above It makes it joy to work with states.

Now that we have a basic understanding of these concepts. Let's run through some examples.

Modelling a switch

We want to represent a switch's on and off states respectively. Without leveraging types we might write something like:

let ui = state =>
    switch(state) {
    | 1 => "on"
    | 0 => "off"
    };

Based on the state we will show "on" or "off" on the UI. The compiler will infer the state's type as int and would warn us that we haven't handled all the cases exhaustively. We can suppress the warning by creating a fall through case.

let ui = state =>
    switch(state) {
    | 1 => "on"
    | _ => "off"
    };

But the warning was pointing us to a code smell, which is that we need to make it explicit that the switch has only two possible states. Currently the function ui can be called with 3 as an argument and that shouldn't be possible. We can mitigate this using variants.

type switchState =
| ON
| OFF;
let ui = state =>
    switch(state) {
    | ON => "on"
    | OFF => "off"
    };

The compiler automatically infers the type of state as switchState using the patterns in the switch branches. The intent of the code is much clear now. If a new states gets added to switchState type then the compiler will warn us to cover that case inside our ui function.

option and switch

No let us assume that we have retrieve the switch's state from a function fetchState, this might fail hence it returns an option instead.

type switchState =
  | ON
  | OFF;
let ui = state =>
  switch (state) {
  | ON => "on"
  | OFF => "off"
  };
let fetchState = i =>
  switch (i) {
  | 1 => Some(ON)
  | 0 => Some(OFF)
  | _ => None
  };
let render = i =>
  switch (fetchState(i)) {
  | None => "unknown"
  | Some(state) => ui(state)
  };
render(0); /* off */
render(1); /* on */
render(2); /* unknown */

Here we are faking the failing request by passing an invalid argument to fetchState for simplicity's sake. Notice how the option type coupled with pattern matching allow us to cover both the success cases inside render without explicitly specifying them separately. The flexibility in the granularity of the pattern helps us avoid repetition, while allowing us to be specific if need be.

Todo List

Suppose you have a todo application with reminders. Here's how you can model it.

/* day, month, year */
type date = (int, int, int);
type item =
  | Todo(string, bool)
  | Reminder(string, date, bool);
type request =
  | Loading
  | Error
  | Success(item);

let ui = state =>
  switch (state) {
  | Loading => "Loading..."
  | Error => "Something went wrong"
  | Success(Todo(text, true) | Reminder(text, _, true)) =>
    "Good job on completing: " ++ text
  | Success(Todo(text, false)) => text ++ " is still pending"
  | Success(Reminder(text, (day, month, year), false)) =>
    text
    ++ " is due on"
    ++ string_of_int(day)
    ++ "/"
    ++ string_of_int(month)
    ++ "/"
    ++ string_of_int(year)
  };

Notice how we can compose types by nesting them in each other conveniently and how we can cover all the possible states exhaustively by just specifying the detail needed inside the pattern. The compiler will warn us if we miss a state. Also representing an invalid state is impossible.

I hope I was able to get you excited about ReasonML. I've barely scraped the surface here. There is so much more to discover like records, tuples, labelled parameters, lists, mutable refs, exceptions, type parameters, modules and much more.

Resources for further reading:

Exploring ReasonML

Reason · Reason lets you write simple, fast and quality type safe code while leveraging both the JavaScript & OCaml ecosystems.

First steps using Reason with BuckleScript

Enjoy abstractions? Check out Azure functions for Node.js.

The contributors to JavaScript January are passionate engineers, designers and teachers. Emily Freeman is a developer advocate at Kickbox and curates the articles for JavaScript January.