Introducing ReScript

Posted: ; Updated: 22 Aug, 2022

A few weeks ago I revisited ReScript and experimented with the ecosystem during Advent of Code. I was pleased to discover that the language is in a much better place than four years ago.

Although I don't think I'll be writing ReScript in production any time soon, I put together a presentation today to introduce it to my colleagues. Below is the transcript of that presentation.

What is it?

A strongly-typed language that compiles to JavaScript.

Why use it instead of TypeScript?

Emphasizes:

History

You may recognize this project under a different name, Reason.

Last year, BuckleScript rebranded to ReScript.

Reason & BuckleScript are now united as ReScript.

Syntax comparison

OCaml

type tree = Leaf of int | Node of tree * tree

let rec exists_leaf test tree =
  match tree with
  | Leaf v -> test v
  | Node (left, right) ->
      exists_leaf test left
      || exists_leaf test right

let has_even_leaf tree =
  exists_leaf (fun n -> n mod 2 = 0) tree

Reason

type tree =
  | Leaf(int)
  | Node(tree, tree);

let rec exists_leaf = (test, tree) =>
  switch (tree) {
  | Leaf(v) => test(v)
  | Node(left, right) =>
	  exists_leaf(test, left) || exists_leaf(test, right)
  };

let has_even_leaf = tree =>
	exists_leaf(n => n mod 2 == 0, tree);

ReScript

type rec tree =
  | Leaf(int)
  | Node(tree, tree)

let rec existsLeaf = (test, tree) =>
  switch tree {
  | Leaf(v) => test(v)
  | Node(left, right) =>
		existsLeaf(test, left) || existsLeaf(test, right)
  }

let hasEvenLeaf = tree =>
	existsLeaf(n => mod(n, 2) == 0, tree)

Type soundness

The ReScript docs state that the ReScript type system is sounder than TypeScript’s. What does this mean in practice?

Most type systems make a guess at the type of a value and show you a type in your editor that's sometime incorrect. We don't do that.

Quick points:

Why is TypeScript unsound?

TypeScript doesn’t prioritize soundness, it prioritizes completeness (which is totally fine!).

const nums: number[] = []
const n = nums[0]

const adder = (a: number, b: number) => a + b

// What does this output?
console.log(adder(n, 2))

playground link

Meanwhile, ReScript will actually throw a compiler error when indexing an array, since there may not be an item at that index.

open Belt

// Note: no annotations needed
let nums = []
let n = nums[0]

let adder = (a, b) => a + b

adder(n, 2)
// Compiler error!
// [E] Line 7, column 6:
//
// This has type: option<'a>
//   Somewhere wanted: int

We can fix the compiler error by wrapping the array access with a switch statement, defaulting the value to 0.

open Belt

let nums = []
let n = switch nums[0] {
| Some(v) => v
| None => 0
}

let adder = (a: int, b: int) => a + b

adder(n, 2)

This is just one of many examples of TypeScript’s unsoundness. Here’s another:

const a: Record<string, string> = {
  id: 'd4346fda-7480-4e47-a51a-786a431b3272',
}

const fn = (t: { id?: number }) => {
  // Runtime type is `string`
  // Type is `number | undefined` ❌
  console.log(t.id)
}

// Expected error here but got none
fn(a)

playground link

The ReScript version outputs a compiler error:

let a = Js.Dict.empty()
Js.Dict.set(a, "id", "d4346fda-7480-4e47-a51a-786a431b3272")

let fn = (t: Js.Dict.t<option<int>>) => {
  Js.log(Js.Dict.get(t, "id"))
}

// This has type: Js.Dict.t<string> (defined as Js_dict.t<string>)
//  Somewhere wanted:
//    Js.Dict.t<option<int>> (defined as Js_dict.t<option<int>>)
//
//  The incompatible parts:
//  string vs option<int>
fn(a)

Variant and Option

TypeScript tackles variants with string literals:

type Animal = 'cat' | 'dog'

const fn = (animal: Animal) => {
  switch (animal) {
    case 'cat':
      console.log("I'm a cat")
      break
    case 'dog':
      console.log("I'm a dog")
      break
  }
}

Compare this to ReScript’s approach, where variants have dedicated constructors:

type animal = Dog | Cat

let fn = animal => {
  switch animal {
  | Dog => Js.log("I'm a dog")
  | Cat => Js.log("I'm a cat")
  }
}

This gives ReScript variants a lot more flexibility because the constructors can take arguments:

type account =
  | None
  | Instagram(string)
  | Facebook(string, int)

let myAccount = Facebook("Josh", 26)
let friendAccount = Instagram("Jenny")

let process = acc => switch acc {
| Instagram(name) => ...
| Facebook(name, age) => ...
| None => ...
}

process(myAccount)

Option

An Option is just a special type of Variant.

Example:

// Annotation for demonstration only
let licenseNumber: option<int> =
  if personHasACar {
    Some(5)
  } else {
    None
  }

Use pattern matching to handle both cases:

switch licenseNumber {
| None =>
  Js.log("The person doesn't have a car")
| Some(number) =>
  Js.log("The person's license number is " ++ Js.Int.toString(number))
}

Belt

The ReScript standard library.

let someNumbers = [1, 1, 4, 2, 3, 6, 3, 4, 2]

let greaterThan2UniqueAndSorted =
  someNumbers
  ->Belt.Array.keep(x => x > 2)
  // convert to and from set to make values unique
  ->Belt.Set.Int.fromArray
  ->Belt.Set.Int.toArray // output is already sorted

Js.log2("result", greaterThan2UniqueAndSorted)

When should I use Belt?

When should I not use Belt?

Personally, I like to replace the JS standard library with Belt entirely using this bsconfig setting:

"bsc-flags": ["-open Belt"]

Fibonacci demo

Setup:

git clone https://github.com/rescript-lang/rescript-project-template fib-demo
cd fib-demo
yarn

Problem: f0 = 0, f1 = 1, fn = f{n-1} + f{n-2} for n > 1

Recursive process solution

ReScript code:

let rec fib = n => {
  if n <= 2 {
    1
  } else {
    fib(n - 1) + fib(n - 2)
  }
}

JavaScript output:

function fib(n) {
  if (n <= 2) {
    return 1
  } else {
    return (fib((n - 1) | 0) + fib((n - 2) | 0)) | 0
  }
}

Iterative process (still using recursion!) solution

ReScript code:

let fib = n => {
  let rec iter = (a, b, counter) => {
    if counter == 0 {
      b
    } else {
      iter(a + b, a, counter - 1)
    }
  }

  iter(1, 0, n)
}

JavaScript output:

function fib(n) {
  var _a = 1
  var _b = 0
  var _counter = n
  while (true) {
    var counter = _counter
    var b = _b
    var a = _a
    if (counter === 0) {
      return b
    }
    _counter = (counter - 1) | 0
    _b = a
    _a = (a + b) | 0
    continue
  }
}

Note that the iterative solution introduces tail call optimization!

React demo

Setup:

npx create-react-app a-todo-list

Then follow the rescript-react installation instructions

Full project: https://github.com/mgmarlow/rescript-cra

Should I use ReScript?

Yes if...

References


Thanks for reading! Send your comments to [email protected].