Moving from JavaScript to ReasonML

Web / Mobile / VR / AR / IoT / AI

Software architect, consultant, author

Why ReasonML

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

Why OCaml?

  • General purpose language (1996) used in critical systems
  • Facebook is using it in several projects (Flow)
  • Functional programming language with Hindler-Minler type system
  • Can be compiled to bytecode, native code or JS
  • Performance and compilation time is blazing fast

  • A language for writing React (first React prototypes were in SML)
  • While being functional it still has escape hatches
  • Amazing community
  • compiles === works
  • Amazing tooling in the language itself 

just take a look at JavaScript world

Show me the Syntax

yarn global add reason-cli@latest-macos

Install Reason CLI

open ReasonML repl

rtop

100% Type inference

Types can be inferred

Tupples

Block scope

Destructuring

Variants

Parameterized variants

Built in variants

We can easily create more complex data structures

Binary tree definition:

Records

record fields can be mutable only if explicitly specified

you can use any types within record. such as tupples or variants

Pattern matching

Looks like switch statement but it's not.

You can destructure into variables when pattern matching

If not covering all use cases it won't compile

Functions

let add = (a,b) => a + b;

There are no nullary functions. Every function return unit type

Destructuring function params

let cross = ((a1, a2, a3), (b1, b2, b3)) => (
  a2 * b3 - a3 * b2,
  a3 * b1 - a1 * b3,
  a1 * b2 - a2 * b1,
);
let computation = (~x, ~y, ~z) => x + y / z;

named params

let cross = (~vector1 as (a1, a2, a3), ~vector2 as (b1, b2, b3)) => (
  a2 * b3 - a3 * b2,
  a3 * b1 - a1 * b3,
  a1 * b2 - a2 * b1,
);

named params destructuring

let add = (~x=0, ~y=0, ()) => x + y;
let add = (~x: int=0, ~y: int=0, ()) => x + y;

defaults

you can use "guards" in pattern matching

functions can be recursive by stating that explicitly 

let add = (a,b) => a + b
add(2)(3)

all functions are curried by default

Reverse application operator enables chaining

[4, 2, 1, 3, 5]
|> List.map(x => x + 1)
|> List.filter(x => x < 5)
|> List.sort(compare);

operators are just functions and you can create your own

Under the hood list is self-recursive paramterized variant

type mylist('a) = | Nil; | Cons('a, mylist('a))
let abc = Cons("a", Cons("b", Cons("c", Nil)));

Lists

  • homogeneous
  • immutable
  • fast at prepending items
let abc = ["a", "b", "c"]

Lists

List.nth(someList, 2);

access list item

sort and then reverse

List.rev(
  List.sort(
    compare, 
      [8,6,4,3,3,2,6,8,4]
  )
);
[8,6,4,3,3,2,6,8,4] |> List.sort(compare) |> List.rev;

Or easier

let test = 
  switch (someList) {
   | [] => "Empty"
   | [first, ...last] => "Head of the list is " ++ string_of_int(first)
  };
  

pattern matching on lists

Arrays

  • mutable
  • fast at random access & updates
  • fix-sized on native (flexible on JS)

sort and then reverse

let myArray = [| "a", "b", "c" |];
myArray[2];
let filterArray = (filter, arr) =>
  arr
  |> Array.to_list
  |> List.filter(filter)
  |> Array.of_list;

Filtering

Modules

let make = () => "";
let logStr = (str: string, log: string) => log ++ str ++ "\n";

let print = (log: string) => print_string(log);

Every file is a module Everything is exported from module

You can control how much to export via interfaces. Interface of a module is also called its signature

signatures are defined in rei files.

Log.re

Log.rei

type t;
let make: (unit) => t;
let logStr: (string, t) => t;
let print: (t) => unit;

Where is JS and how Reason compiles to it

Reason -> Bucklescript -> JS

Let's get started in VSCode

yarn add bs-platform --dev --exact
yarn add reason-react --exact

Bucklescript

Installing libraries

{
  "dependencies": {
    "bs-jest": "^0.1.5"
  }
}
{
  "bs-dependencies": [
    "bs-jest"
  ],
  ···
}

Bucklescript(Rescript) objects are like records

  • No type declaration needed.
  • Structural and more polymorphic, unlike records.
  • Doesn't support updates unless the object comes from the JS side.
  • Doesn't support pattern matching.
type person = {
  "age": int,
  "name": string
};

type declaration is optional

let me = {
  "age": 5,
  "name": "Big ReScript"
}

access

let age = me["age"]

FFI

[%raw {| 
  console.log('here is some javascript for you') 
|}];

raw js

type person = {
  name: string,
  friends: array<string>,
  age: int,
}

[@bs.module("MySchool")] external john: person = "john"

let johnName = john.name
var MySchool = require("MySchool");

var johnName = MySchool.john.name;

convert JS object to Record

Bucklescript Externals

[@bs.val] external bindingToBeCalledInReason: typeSignature = "functionNameOnGlobalScope"

external is a keyword for declaring a value in BuckleScript/OCaml/Reason:

[@bs.val] external setTimeout: (unit => unit, int) => float = ""; 
[@bs.val] external clearTimeout : float => unit = "";

It's like let but the body is a string

Globals

Abstract type

type timeout;
[@bs.val] external setTimeout: (unit => unit, int) => timeout = "";
[@bs.val] external clearTimeout: timeout => unit = "";

Null, Undefined, Option

If you're receiving, for example, a JS string that can be null and undefined, type it as:

[@bs.module "MyConstant"] external myId: Js.Nullable.t(string) = "myId"

To pass nullable string to a module

[@bs.module "MyIdValidator"] external validate: Js.Nullable.t(string) => bool = "validate";
let personId: Js.Nullable.t(string) = Js.Nullable.return("abc123");

let result = validate(personId);

Import Exports

[@bs.module "path"] external dirname : string => string = "dirname";

default imports

[@bs.module "./student"] external studentName : string = "default";
module Decode {
    
  let planet = planet =>
    Json.Decode.{
      name: planet |> field("name", string),
      rotation_period: planet |> field("rotation_period", string),
      orbital_period: planet |> field("orbital_period", string),
      diameter: planet |> field("diameter", string),
      climate: planet |> field("climate", string),
      gravity: planet |> field("gravity", string),
      terrain: planet |> field("terrain", string),
      surface_water: planet |> field("surface_water", string),
      population: planet |> field("population", string),
      residents: planet |> field("residents", array(string)),
      films: planet |> field("films", array(string)),
      created: planet |> field("created", string),
      edited: planet |> field("edited", string),
      url: planet |> field("url", string)
    }
  let planets = json => Json.Decode.list(planet, json);

  let result = json => Json.Decode.{
      count: json |> field("count", int),
      previous: json |> field("previous", optional(string)),
      next: json |> field("next", optional(string)),
      results: json |> field("results", array(planet))
  }
}

let print_decoded_planets = _ => {
  Js.Promise.(
    Fetch.fetch("https://swapi.co/api/planets")
    |> then_(Fetch.Response.json)
    |> then_(
      json => {
        let response = Decode.result(json);
        response.results |> Array.to_list |> List.iter((planet) => {
          switch (planet) {
            | plnt => print_endline(plnt.name)
          }
        })
        resolve(json);
      }
    )
    |> then_(
      json => json |> Js.Json.stringify 
      |> resolve
    )
    |> catch(err => {
      Js.log(err)
      resolve("")
      }
    )
  );
}

Json encoding and decoding with bs-json

Js.Promise.(
    Fetch.fetch("https://swapi.co/api/planets")
    |> then_(Fetch.Response.json)
    |> then_(
      json => Js.Json.stringify(json) 
      |> print_endline 
      |> resolve
    )
  );

Fetching data with bs-fetch

What about React?

Let's convert Create React App to ReasonML

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

switch (ReactDOM.querySelector("#root")) {
 | Some(root) => ReactDOM.render(<React.StrictMode><div>{"Hello" |> ReasonReact.string }</div></React.StrictMode>, root)
 | None => ()
}

First let's convert rendering to DOM

To bring in our App component from JS we need to type it explicitly for Reason.

module App = {
  [@bs.module "./App.js"][@react.component]
  external make: (_) => React.element = "default";
}

For that we can use one Bucklescript external


[@bs.module "./serviceWorker.js"]
external unregister: _ => unit = "unregister"


unregister();

Now let's convert App component

[@bs.val] external require: string => string = "";


let logo = require("./logo.svg");
require("./App.css");

The rest is pretty straightforward

[@react.component]
let make = () => {
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          { "Edit <code>src/App.js</code> and save to reload."  |> ReasonReact.string  }
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          {"Learn React" |> ReasonReact.string}
        </a>
      </header>
    </div>
}

Let's convert create-react-app to ReasonML

Thank You

  @VladimirNovick