DEV Community

Cover image for Enjoy Painless Typing With ReasonML!
Demangeon Julien
Demangeon Julien

Posted on • Originally published at marmelab.com

Enjoy Painless Typing With ReasonML!

Note: This post was originally posted on marmelab.com.

For one of our regular hackdays at Marmelab, I wanted to learn a language I've kept in the back of my head since I heard of it at React Europe 2017.

This language, which is called Reason (shortcut for ReasonML), is in fact a syntax and a toolchain overlay for OCaml, a language that is known for its robustness, its strong static typing, and its ubiquitous functional paradigm approach.

According to the Reason website, its main goal is to provide a friendly syntax / environment to JavaScript developers looking for performance, consistency, and type safety.

Reason can almost be considered as a solidly statically typed, faster and simpler cousin of JavaScript, minus the historical crufts, plus the features of ES2030 you can use today, and with access to both the JS and the OCaml ecosystem!

By the way, I think that this simple example should suffice to illustrate its power, and whet your appetite for further reading.

type schoolPerson = Teacher | Director | Student(string);

let greeting = (stranger) =>
  switch (stranger) {
  | Teacher => "Hey professor!"
  | Director => "Hello director."
  | Student("Richard") => "Still here Ricky?"
  | Student(anyOtherName) => "Hey, " ++ anyOtherName ++ "."
  };
Enter fullscreen mode Exit fullscreen mode

To explore this language in practice, I've coded a reversi game running in the browser. I'll use it to give an overview of Reason capabilities, and explain why I think that it opens a whole new horizon on the JavaScript ecosystem.

But before going into technical details, let me introduce Reason from an historical and practical point of view.

Reason Is OCaml Made Easier

As I previously said, Reason is based on OCaml. This way, it benefits from all the OCaml strengths, like polymorphic / infered typing, pattern matching, garbage collector, sophisticated module system, and so on.

OCaml is the main implementation of Caml. Caml is a safe, reliable, and expressive programming language created in 1985 by a French research institute in Computer Science called INRIA. But, what's wrong with OCaml? Why not use it directly? Indeed, the question deserves to be asked.

OCaml is based on complex principles, and uses an awkward syntax. Here is an example of OCaml code, which adds values recursively from a list:

let rec sum xs =
  match xs with
    | [] -> 0
    | x :: xs' -> x + sum xs';;

(* sum [1;2;3;4;5] => 15 *)
Enter fullscreen mode Exit fullscreen mode

Tip: As a matter of fact, it's not really necessary to use recursive functions for this kind of purpose, because the Core Standard Library has many built-in functions covering most needs.

The complexity of OCaml explains why OCaml it confined to academic projects for a long time.

Until a few years ago, Jordan Walke, who works at Facebook, created the famous React library using SML (a derived OCaml language), and created ReasonML. Shortly after, he made the decision to migrate ReactJS to plain JavaScript for a wider adoption.

BuckleScript Brings OCaml to the Web

In fact, Reason does not directly compile to JavaScript. For that purpose, it maintains a strong dependency to another library called BuckleScript.

BuckleScript defines itself as a "sister" project of Reason. In fact, both share the same community (they have the same Discord), and the same purpose: bringing OCaml's capabilities to the browser. Moreover, the documentation between the two projects is very complementary.

Here is a little schema of the compilation workflow from Reason to JavaScript. Under the hood, reason files (.re) are transformed to plain OCaml AST through an OCaml preprocessor for Reason. This OCaml AST is then processed by the BuckleScript compiler called bsc, which produces plain JS Files.

The compilation process is a bit more complex than explained here. To understand it in more details, I suggest you take a look at this excellent repository owned by chenglou, the speaker who introduced me to Reason at React Europe.

Why Not TypeScript Or Flow?

JavaScript is an untyped language, but large projects often require additional programming safety throught types. That's why many tools have been created on top of JavaScript to fill this gap. Among them, the most popular are probably TypeScript and Flow.

  • TypeScript is an open-source programming language developed by Microsoft. It acts as a strict syntaxic superset of JavaScript that adds static typing to it. It is also considered as first class language for Angular development.
  • Flow is an open-source static type checker for JavaScript developed by Facebook. It acts as a specific syntax (kind of annotations) which adds types over an existing code.

How does Reason compare to those two? In fact, Reason is not a new syntax for JavaScript, or a simple typing overlay. Reason is a complete language. It introduces new control structures that you'll never find in TypeScript or Flow. Reason is built with typing at its core, and just compiles to JavaScript.

If you need strong types, I think that the only valid reason to use TypeScript or Flow is to port an existing JavaScript codebase. If you need strong types without an existing codebase, prefer a real typed language like Reason.

Reason Can Compile To Other Platforms

Reason uses OCaml as an intermediate language, and it's BuckleScript's job to translate that OCaml code into JavaScript.

But there are other toolchains for running OCaml code in other platforms. For instance, OCaml can be compiled to native code, thanks to the ocamlopt compiler. In this respect, there are some successful cross-platform projects written in Reason that are compiled to native, browser, Android and iOS.

From my point of view, this unexpected possibility opens up a whole new development horizon. Above all, this opportunity allows to move away from the "all JavaScript" trend, which I think is dangerous. We should never lock ourselves with a single technology.

OK, enough with the introduction of the language. Let's see some code!

Bindings And Expressions

Unlike JavaScript, there's only one way to declare / assign variables in Reason. Indeed, because everything is immutable by nature, the only assignation keyword is let. Therefore, the assignment action is called a "let binding".

let message = "hello world";
Enter fullscreen mode Exit fullscreen mode

"Immutability" means that a value can't change over time - it doesn't means that you can't create a new binding with the same name to replace an existing one. That's why let is called a binding and not an assignment. A binding gives a name to a value, it doesn't change the value.

let message = "hello";
print_endline(message); /* Prints "hello" */
/* totally legal */
let message = "world";
print_endline(message); /* Prints "world" */
Enter fullscreen mode Exit fullscreen mode

To effectively change an already bound "value", you must use a ref. This topic is discussed later in the "An Imperative Way Out" section.

Bindings can also be scoped into a "block" scope ({}). In that case, the last expression of the block is implicitly returned. There's no explicit return in Reason. As in JavaScript, bindings are only available in theirs respective scopes.

let message = {
    let part1 = "hello";
    let part2 = "world";
    part1 ++ " " ++ part2
};

/* part1 & part2 not availables here */
Enter fullscreen mode Exit fullscreen mode

Basic Types

Like most other programming languages, Reason supports all basic types such as booleans, numbers, strings and chars. Since Reason is a statically typed language, types can be manually defined, or can be infered at compile time from the program AST.

let score = 10; /* type is infered */
let score: int = 10; /* type is manually defined */
Enter fullscreen mode Exit fullscreen mode

With Reason, just like with Python, there's no implicit type casting. Developers must use explicit type conversion functions (like string_of_int or string_of_bool) to switch from one type to another. These functions are part of Pervasives, which is the initially opened module at the beginning of each compilation. It provides all the basic operations over the built-in types.

Custom Types

As in OCaml, it is also possible to create your own types with Reason. In this respect, here are 2 different kinds of types from the reversi "Cell" module.

The color type is called a Variant. A Variant is a kind of group of possible constants. These constants, which are called "constructors" or "tags", are separated by "|" bars. Variants are, from my point of view, the key feature of Reason. They allow us to carry values (as arguments), and enable pattern matching.

/* cell.re */
type color = White | Black;
Enter fullscreen mode Exit fullscreen mode

The cell type is called a Record. In other languages, it's usually called a struct. Objects created based on a Record are immutable, fixed, and very fast. Records needs a strong type definition. That's why each field is explicitly typed.

/* cell.re */
type cell = {
    x: int,
    y: int,
    color: option(color),
};
Enter fullscreen mode Exit fullscreen mode

As you see for the color field of the cell type, a type can contains another type. It allows to create complex recursives data structures (like trees) quickly and easily.

type intTree =
  | Empty
  | Node(int, intTree, intTree);
Enter fullscreen mode Exit fullscreen mode

Parameterized And Special Types

In one of the previous example, you may have asked yourself about the option(color) function call. In fact, option is not a function, it's a parameterized Variant, which is directly exposed by the standard library.

Since there's no null values in Reason (and therefore no null pointer exceptions), option allows to mimic the absence of value for anything. It can either be None (null equivalent) or Some(value). It can be compared to the famous Maybe Monad.

type option('a) =
    | None
    | Some('a);
Enter fullscreen mode Exit fullscreen mode

What does the 'a mean? In Reason, every type can accept parameters. The unique quote means "a value of any type". This is very useful to create generic type structures.

Lists And Arrays

In Reason, one of the most used Type is List. As its name suggests, a List is a collection of elements that are of the same type.

Lists are represented as linked lists underneath (even in the transpiled JavaScript!). Because of that, they're dynamically sized and immutable by nature, and they allow to add or remove elements very quickly.

/* board.re */
let directions = [
    (0, 1),  /* S  */
    (0, -1), /* N  */
    (1, 0),  /* E  */
    (1, 1),  /* SE */
    /* ... */
];
Enter fullscreen mode Exit fullscreen mode

Lists are very fast for updates, but very slow for access. Read operation speed is proportional to the size of the List (O(n) complexity). That's why Reason also provide an Array Type.

Contrary to Lists, Arrays are fixed-size collections, which are mutable and fast for read operations (O(1) complexity).

let myArray = [|"hello", "world", "how are you"|];
Enter fullscreen mode Exit fullscreen mode

In brief, Lists are better for dynamic and medium sized collections that do not require fast access. Array are better for fixed and large sized collections that require fast access.

You'll find more information about Lists and Arrays in the Exploring ReasonML online book, which is very complete.

Pattern Matching And Destructuring

I've introduced Types and Variants so that I can talk about one of the most interesting feature of Reason: pattern matching.

In brief, pattern matching allows both to check and to extract data from structures. It's a kind of mix between RegExp.test and RegExp.exec from JavaScript, but for all types of data, and anywhere (bindings and function args).

In the following example, I use the switch expression to test the color value against multiple patterns. When a pattern matches, the value just after the => is then returned and assigned to "identifier".

/* cell.re */
let identifier = switch (color) {
    | Some(Black) => "black"
    | Some(White) => "white"
    | None => ""
};
Enter fullscreen mode Exit fullscreen mode

The above example is of course the simplest one. You can also match one part of the value, use it afterwards, and even match on an exception!

/* board.re */
let getCell = (x, y, cells) =>
    switch (List.find(cell => cell.x == x && cell.y == y, cells)) {
        | ({ color }) => color /* color is extracted from the cell record */
        | exception Not_found => None
    };
Enter fullscreen mode Exit fullscreen mode

Naturally, matching also comes with destructuring. That's why it's even possible to extract parts of data structures easily, even from functions args!

/* board.re */
let init = (width, height) => {
    let (mw, mh) = (width / 2, height / 2); /* tuple destructuring */
    /* ... */
};

let isOutOfBound = ({ width, height }, (x, y)) => /* function args destructuring */
    x < 0 || y < 0 || x > (width - 1) || y > (height - 1);
Enter fullscreen mode Exit fullscreen mode

There are a lot of things to say about pattern matching, far too much to cover everything here. I advise you to take a look at this article, which is very comprehensive.

Functions

As you may have understood, Reason is fully focused on the functional paradigm. In this regard, it highlights a lot of concepts like higher-order functions, recursivity, partial application (via currying), and so on. The Reason function syntax is very close to the ES6 one. It uses the famous "arrow / body" pair.

let add = (first, second) => first + second;
add(1,2); /* 3 */
Enter fullscreen mode Exit fullscreen mode

In Reason, every function takes at least one argument, even if you don't declare / notice it! When you declare a function without any argument, under the hood, Reason adds a unit argument to it. In the example below, the pair of brackets () after locationReload is a unit. So, in reality, you effectively call locationReload with an argument without even realizing it.

let locationReload = () => {
  /* ... */
};
locationReload();
Enter fullscreen mode Exit fullscreen mode

You may be disappointed by this "unit", but you must known that it's a normal value. Above all, you must not confuse "unit" with an "option". Whereas an "option" represents "a value or an empty value", a "unit" represents an absence of value (think about undefined or "nothing").

Currying

Reason offers built-in currying of all functions. That means that every function with one or more arguments is transformed into a series of functions with one argument.

let add = (first, second) => first + second;
add(1)(2); /* 3 */
Enter fullscreen mode Exit fullscreen mode

You may think that it's a waste of resources to create additional function calls, but it's not. OCaml optimizes the output to avoid unnecessary function allocation if partial functions are never called in your program (see this example). This way, OCaml provides out of the box currying without any performance penalty.

Here is another example of currying, where I take advantage of partial application for my getCountForColor function from the reversi Board module:

let getCountForColor = (board, color) =>
    board.cells
        |> List.filter(c => c.color == color)
        |> List.length;

let countForColorFromMyBoard = getCountForColor(myBoard);

let countWhite = countForColorFromMyBoard(Some(White));
let countBlack = countForColorFromMyBoard(Some(Black));
Enter fullscreen mode Exit fullscreen mode

The pipe operator |> (also called "reverse-application operator") allows to pass the result of an expression as the first argument of the following expression. Think of the Linux pipe. Given that functions natively allow partial application, it works like a charm!

Labeled Arguments

Reason functions also work with named arguments (called labeled arguments). They are compatible with currying as well:

let add = (~first, ~second) => first + second;
let addFirst = add(~second=1);
addFirst(~first=2);
Enter fullscreen mode Exit fullscreen mode

To take full advantage of partial application, it's recommended to place args that change most often at the end of the function, or to use labeled args.

Labeled arguments can also be optional. For that purpose, you just have to add a question mark as default value, like in the example below. This way, the corresponding argument is automatically provided as an option type, described earlier.

let addOptional = (~first, ~second=?) => first + switch(second) {
  | None => 0
  | Some(nb) => nb
};

let addDefault = (~first, ~second=0) => first + second
Enter fullscreen mode Exit fullscreen mode

It would be possible to write an entire book about functional paradigms and Reason. A lot of concepts were deliberately bypassed in this section.

For more informations about functors, recursivity, mutual recursivity, I suggest you to take a look at 2ality - ReasonML Functions and 2ality - ReasonML Functors. If you're interested about functional programming, I also advise you to read this Marmelab blog post by my colleague Thiery :)

Setting Up a Reason Project

During my hack day, the first step was to setup the whole Reason stack inside Docker, like we do for all our projects at Marmelab. Using Docker allows us to share projects across various environments with an easy installation.

To be honest, this setup experience was the worst I had in a long time. It took me about one hour to deal with many permission issues [1] [2]. The recommended global install from the official setup guide seems to be at the core of the problem.

Nevertheless, I'm pretty sure that the community will find solutions to make this first step easier. By the way, I'm not the only one who struggled with that. Using the "node:6" docker image seems to do the job for the moment...

Developer Experience First!

Once installed, the Reason developer experience is just amazing. The underlying BuckleScript compiler is quite fast, it builds mosts projects in less than 100ms, incrementally.

Moreover, the error reporter (based on Merlin) is just perfect. It gives a detailed explanation of all possible mistakes thanks to the 100% type coverage of OCaml. Syntax errors are a little bit less clear but still give a good feedback. Here are two little examples to give you a preview of these powers.


A React App In Seconds!

Because of its history, Reason maintains a strong relationship with React. In this regard, it is quite easy to setup a React project with Reason. I was able to confirm that during my reversi project.

create-react-app allows to focus on functionalities without worrying about toolchain configuration. So I took the decision to use it in association with reason-scripts.

create-react-app reversi-reason --scripts-version reason-scripts
Enter fullscreen mode Exit fullscreen mode

After the install step, I found myself with this familiar folder structure:

reversi-reason/
  README.md
  node_modules/
  package.json
  bsconfig.json
  .gitignore
  public/
    favicon.ico
    index.html
  src/
    index.re
    index.css
    app.re
    app.css
    logo.svg
Enter fullscreen mode Exit fullscreen mode

The only difference with classic JS projects are files with a .re extension (which are, as you probably guessed, Reason files), and the bsconfig.json file, which is the BuckleScript configuration.

{
  "name": "reversi-reason",
  "sources": [
    "src"
  ],
  "bs-dependencies": [
    "reason-react",
    "bs-jest"
  ],
  "reason": {
    "react-jsx": 2
  },
  "bsc-flags": [
    "-bs-super-errors"
  ],
  "refmt": 3
}
Enter fullscreen mode Exit fullscreen mode

The BuckleScript configuration is a kind of mix between a linter config (like .eslintrc), and a compiler config (like .babelrc). It's quite normal because BuckleScript fulfills these 2 missions.

Domain Driven Design Made Easy

When I start a fresh new project, I always try to define the ubiquitous language and the associated domain objects before starting to code. For this project, I already knew my domain, because the reversi game is my favorite project to learn a new language, and I've already written a lot of code around it.

So, my domain objects are the following: Game, Player, Board and Cell. My first step was to create one module per object, with the associated test file. You can see them find them in the reversi-reason Github repository!

src/
├── app.re
├── app_test.re
├── board.re
├── board_test.re
├── cell.re
├── cell_test.re
├── game.re
├── game_test.re
├── index.css
├── index.re
├── player.re
└── player_test.re
Enter fullscreen mode Exit fullscreen mode

In OCaml / Reason, every file maps to a module; this built-in capability empowers most projects with an out of the box Domain Driven Design architecture and a strong expressivity.

There's no need for require, use, or import to use a module in Reason. Just call the module directly. This way, all the domain objects I talked before are automatically available through their names.

It's also possible to manually create modules using the module keyword. So, you can nest and access them using the dot notation (eg: MyModuleFile.MyModule.MySubModule.myFunction).

/* MyModuleFile.re */
module MyModule = {
  module MySubModule = {
    let myFunction = () => "hello";
  };
};

/* ... in another file ... */

let message = MyModuleFile.MyModule.MySubModule.myFunction;
Enter fullscreen mode Exit fullscreen mode

In fact, you don't have to use the dot notation every time you want to access a value in a module. The Module definition can be opened both locally and globally to avoid this repetition.

An example "local" opening is the applyCellClick function below. The Board.({ ... }) call exposes applyCellChange and getCountForColor from the Board module without having to repeat the module name.

/* game.re */
let applyCellClick = (game, x, y) => Board.({
    let color = Some(currentPlayer(game).color);
    let board = applyCellChange(game.board, { x, y, color });

    switch(getCountForColor(board, None)) {
        | 0 => { ...game, finished: true }
        | _ => switchPlayer({ ...game, board })
    };
});
Enter fullscreen mode Exit fullscreen mode

This "local" opening could have been replaced by a "global" opening at the top of the Game module. Nevertheless, using global open is not advised and must be used sparingly because it can break reasoning.

/* game.re */
open Board;

let applyCellClick = (game, x, y) => {
    /* ... */
};
Enter fullscreen mode Exit fullscreen mode

An Imperative Way Out

Reason uses a lot of concepts that are not always easy to handle (recursivity, currying, ...). Fortunately, it isn't as strict as Haskell, and it makes it possible to use some imperative and unsafe code when needed. This pragmatic approach is well highlighted in the Reason website.

Reason has great traditional imperative & mutative programming capabilities. You should use these features sparingly, but sometimes they allow your code to be more performant and written in a more familiar pattern.

Here is one of the "ugliest" code snippets from my reversi-reason project. This function collects all flipped cells in a predefined direction. It's the most suitable example because it uses a lot of "legacy" (hear "not functional") capabilities that are allowed by Reason.

/* board.re */
let flippedCellsInDirection = (board, cell, (dirx, diry)) => {
    let cells = ref([]);
    let cursor = ref((cell.x + dirx, cell.y + diry));
    let break = ref(false);

    while (! break^) {
        cells := switch(cursor^) {
            | cursor when isOutOfBound(board, cursor) => break := true; []
            | (x, y) => switch(getCell(x, y, board.cells)) {
                | None => break := true; []
                | color when (color == cell.color) => break := true; cells^
                | _ => {
                    cursor := (x + dirx, y + diry);
                    [{ x, y, color: cell.color }, ...cells^]
                }
            }
        };
    };

    cells^
};
Enter fullscreen mode Exit fullscreen mode

When you look at this code, the first thing that stands out is the usage of the well-known while loop. Effectively, Reason (as well as OCaml) allows the usage of imperative loops.

Also, to be able to break a while loop, I had to use a mutable flag. In Reason, all variables are immutable by nature. To be able to mutate a value, I had to wrap it with a ref that acts like a box.

Afterward, to retrieve the underlying ref value, the postfix ^ operator is used. The truth is that ref is just a syntatic sugar for a predefined mutable record type. Test by yourself!

In the next sections, I'll try to cover how strong the link between Reason and React is. First, talking about ReasonReact, and then talking about the associated tests with Enzyme.

Built-in JSX support

Before going further, you should know that Reason natively includes JSX support. In fact, JSX only acts as a syntaxic sugar which is translated to normal function calls wrapped into expressions.

JSX translates to a make function call on the same module name as the JSX tag:

<MyComponent foo={bar} />
Enter fullscreen mode Exit fullscreen mode

Becomes

([@JSX] MyComponent.make(~foo=bar, ~children=[], ()));
Enter fullscreen mode Exit fullscreen mode

That's why it's necessary to properly name modules. If you wish, you can still create multiple components in the same file thanks to nested modules.

The Reason JSX syntax is not exactly the same as the JavaScript one. Indeed, there's no props spread but children spread. i.e. you can't do <Foo {...bar} /> but you can do <Foo> ...baz </Foo>.

ReasonReact As Cornerstone

How to create React components in Reson? Here is an example:

/* player.re */

let component = ReasonReact.statelessComponent("Player");

let make = (~player, _children) => {
    ...component,
    render: (_self) => 
        <div className=("player")>
            (stringToElement(player.name))
        </div>
};
Enter fullscreen mode Exit fullscreen mode

I created the component template in combination with the statelessComponent function from the ReasonReact module. Spreading ...component is a bit like saying my component "inherits" from statelessComponent, except that
class components don't exist in Reason.

The "Player" string passed to statelessComponent primarily acts as a debug marker, it's the ReactJS equivalent of displayName.

Redux-Like Components!

While the statelessComponent acts as a functional component mixin, there's also another special ReasonReact function called reducerComponent. This function allows to directly include a "state machine"-like architecture into our components.

Using this component requires to define an initialState and a reducer function, which contain all the state manipulation logic. Those who have already used redux will certainly recognize this pattern.

/* game.re */

/* ... action type, state type and reducer ... */

let component = ReasonReact.reducerComponent("Game");

let make = (~game, _children) => {
  ...component,
  initialState: () => { game, message: None },
  reducer,
  render: (self) => {
    let { game, message } = self.state;

    let messageElement = switch(message) {
        | None => nullElement
        | Some(message) => stringToElement(message)
    };

    <div className="game">
        (messageElement)
        <Board
            board=game.board
            onCellClick={(x, y) => self.send(Click(x, y))}
        />
        /* ... */
    </div>
  }
};
Enter fullscreen mode Exit fullscreen mode

In combination with the reducerComponent, it is usual to define 2 types:

  • One type for the actions (represented as a variant), and
  • One type for the state (represented as a record)

This way, Reason is able to infer by itself the initialState type. The action type is used to represent actions which can then be pattern-matched in the reducer function.

/* game.re */

type action = Click(int, int) | Restart;

type state = { game, message: option(string) };

let reducer = (action, state) => switch (action) {
    | Restart => ReasonReact.SideEffects(locationReload)
    | Click(x, y) => switch(applyCellClick(state.game, x, y)) {
        | game => ReasonReact.Update({
            game,
            message: None
        })
        | exception Board.InvalidMove => ReasonReact.Update({
            ...state,
            message: Some("Invalid Move")
        })
        | exception InsolubleGame(game) => ReasonReact.Update({
            game,
            message: Some("No One Can Play")
        })
        | exception CantSwitchPlayer(game) => ReasonReact.Update({
            game,
            message: Some("Opponent can't play, play again!")
        })
    }
};

/* ... react component ... */
Enter fullscreen mode Exit fullscreen mode

According to the Reason philosophy, the reducer must be pure. Also using a pure function makes the code much more testable and easier to read. Only 4 distinct values can be returned:

  • ReasonReact.NoUpdate: don't update state
  • ReasonReact.Update: update state
  • ReasonReact.SideEffects: don't update state but trigger a side effect
  • ReasonReact.UpdateWithSideEffects: update state and trigger a side effect

A Rigorous Interoperability

Reason allows to communicate with other languages thanks to Externals (also known as FFI or "interop"). Using "Externals" is the way to go to write safe, predictive, and reliable typed code between Reason and any other language.

An Externals is a kind of typed contract / interface between the safe Reason world and the unsafe external world. It's this system that allows the bs-jest and bs-enzyme libraries that you'll find further to work.

Here is a simple example from the famous alert() function of JavaScript, which takes one arg and returns nothing (a.k.a. a "unit").

[@bs.val] external alert : string => unit = "alert";
alert("hello");
Enter fullscreen mode Exit fullscreen mode

If you're an adventurous man, and you are using Reason with JavaScript, you can also use JS code through the JS module, or inject code directly.

/* Using the JS module */
Js.log("I'm logged via JS Module externals");

/* Direcly inject raw code */
[%bs.raw {| console.log("I'm logged via raw JS code"); |}];
Enter fullscreen mode Exit fullscreen mode

A complete guide for JavaScript interop is available on Reason and Bucklescript documentations.

Unit Testing Reason Code

Under the hood, "create-react-app" uses Jest as test runner thanks to the bs-jest binding. Nothing special about it, the test architecture is almost the same as in JavaScript.

/* board_test.re */
open Jest;
open Expect;

describe("Board", () => {
    /* ... */

    describe("getCountForColor", () => {
        test("should return the number of cells of corresponding color", () => Board.({
            expect(getCountForColor(init(4, 4), Some(Cell.Black))) |> toBe(2);
            expect(getCountForColor(init(4, 4), Some(Cell.White))) |> toBe(2);
            expect(getCountForColor(init(4, 4), None)) |> toBe(12);
        }));
    });

    /* ... */
});
Enter fullscreen mode Exit fullscreen mode

By the way, it's also possible to use enzyme to test components as in any other ReactJS project.

/* cell_test.re */
open Jest;
open Enzyme;
open Expect;

configureEnzyme(react_16_adapter());

describe("<Cell />", () => {
    test("should render a disk with the right identifier", () => {
        let test = (color, expectedClass) => {
            let wrapper = shallow(
                <Cell
                    color
                    onClick=((_) => ())
                />
            );

            expect(wrapper |> find(expectedClass) |> length) |> toBe(1);
        };

        test(Some(Cell.Black), ".cell.black");
        test(Some(Cell.White), ".cell.white");
        test(None, ".cell");
    });
});
Enter fullscreen mode Exit fullscreen mode

A Pragmatic Community

During development, I had plenty of questions about best practices. Faced with the lack of documentation, I went to the language Discord.

Despite a notable lack of online resources, Reason benefits from a large and reactive community (~200 people always connected). My questions didn't go unanswered more than 5 minutes.

Recently, I also discovered that the community has introduced a centralized forum that appears to be very crowded and active. There are also fabulous podcasts that I've all listened to! Don't hesitate to listen to them, this initiative is rare enough to be highlighted.

Some of these quotes from other developers sound very relevant to me. They perfectly reflect the language's philosophy, which seems to be pragmatism.

My philosophy is that my time is more valuable than the computer’s time, so if I write it in a way that I understand it, it’s OK. - jdeisenberg

If this is a game that is going to be played by a human, then if evaluation of the board takes 2 milliseconds instead of 2 microseconds, will the person notice? Probably not. - jdeisenberg

A section from the "what and why?" of the language website confirms that this philosophy is not specific to the community, but to the language itself.

An eye for simplicity & pragmatism. We allow opt-in side-effect, mutation and object for familiarity & interop, while keeping the rest of the language pure, immutable and functional.

Indeed, Reason is really a pragmatic language, and I like it. Moreover, this principle is consistent with agility, which we practice on every project at marmelab.

Is This Ready To Use ?

Yes, it is ready to use in production! Reason is supported and used by Facebook everyday on big projects. This way, I think you can fairly trust it.

Moreover, Reason is a very stable programming language, it results from decades of research and compiler engineering from the OCaml language. Its interoperability capabilities give to Reason the ability to be integrated into any existing JavaScript project incrementally, so why wait?

On my side, there's no doubt that I would like to develop with Reason again. During this project, I've never felt stuck, everything is designed to produce clean code, with pragmatic alternatives in case of difficulty! The developer experience is just awesome!

To finish, its active community and cross-platform capabilities make me believe that Reason still has some beautiful days ahead. So, don't pass by it! And so, using types saves kittens, don't forget that!

You can test Reason without setting up a complete stack thanks to the online compiler which is very complete and impressively fast!

If you want to read more on the subject by other authors, I recommend the following:

Oldest comments (0)