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.
- Powered by OCaml
- Functional feel
- More of a TypeScript competitor than alternatives like Elm, PureScript, or ClojureScript.
Why use it instead of TypeScript?
- Only cover a small, curated subset of JavaScript
- Types are sound (will not lie to you)
- Fast compilation
- Minimal type annotations
- Gradual adoption strategy
- Compiler optimizations
Emphasizes:
- Functions over classes
- Pattern matching over conditionals or virtual dispatches
- Data modeling (variants) over string abuse
- First class support for React
History
You may recognize this project under a different name, Reason.
- Used by Facebook Messenger
- Promised strong types w/ safe inference derived from OCaml
- Reason was just a syntax layer, BuckleScript was doing the real work
- BuckleScript was a fork of the OCaml compiler that output JS
- BuckleScript and Reason were two separate teams
Last year, BuckleScript rebranded to ReScript.
- Directly integrated Reason into BuckleScript
- Combined both teams under the same umbrella
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:
- No
undefined
ornull
. There’s interop available, but they don’t exist in idiomatic ReScript - Types are compiler-only constructs, they won’t affect your runtime performance
- Type checking is way faster than
tsc
- Almost no need for annotations
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))
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)
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.
- Recall:
undefined
andnull
don’t exist in ReScript - Potentially nonexistent values are still useful
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.
- Immutable data structures
- Safety by default (e.g. array access runtime safety, Belt functions never throw exceptions)
- Tree-shakable, good performance (citation needed)
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?
- You need an API that isn’t available in regular JS
- You want no compromises on type safety
When should I not use Belt?
- You want zero-cost abstractions
- You want semantics similar to JS
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...
- Your code doesn’t rely too heavily on third-party JS packages
- You want total type correctness
- You want compiler-defined performance optimizations
- You’re using React