Hippocampus: A brain region that helps people to remember by linking different parts of a memory together.

Rescriptocampus: Notes that helps me to remember ReScript.

ReScript

  • ReScript covers only a curated subet of JavaScript
  • ReScript emphasizes
    • sound and reliable type system
    • plain data + functions over classes
    • clean pattern matching over fragile ifs and virtual dispatch
    • proper data modeling over string abuse

let Binding

Rescript

let a = 10
let b = 10
let c = a + b

let x = 1 and y = 2
Js.log(x + y) // 3

// block scope
// z = 3
let z = {
  let x = 1
  let y = 2
  x + y
}

// shadow
let x = 1
let x = 2
let x = 3
Js.log(x) // 3

OCaml

let z = let x = 1 and y = 2 in x + y ;;
let z = let x = 1 in let y = 2 in x + y;;

Mutable binding

Rescript

let x = ref (10)
x := x.contents + 1
x.contents = x.contents + 1 /* x is 12 */


type box<'a> = {mutable contents: 'a}
let box = v => {contents: v}

let r = box(10)
r.contents = r.contents + 1

OCaml

let x = ref 10 in
x := !x + 1 ;;
x.contents <- x.contents + 1;;
!x ;; (* x is 12 *)

Type

Rescript type system is strong, static, sound, fast and inferred.

Rescript

Type Inference
let a = 10
let f = 10.
let add = (a, b) => a + b
Type annotation
let a: int = 10
let f: float = 10.
let add: (int, int) => int = (a, b) => a + b
Js.log(add(a, a)) // 20
// or
let add = (a: int, b: int): int => a + b
Js.log(add(a, a)) // 20
Type alias
type kg = float
let weight: kg = 88.

type pair = (string, int)
let kv: pair = ("Key", 100)
Js.log(kv) // [ 'key', 100 ]
Mutually recursive type
type rec person = {
  name: string,
  friends: list<person>,
}

type rec room = {building: building}
and building = {rooms: array<room>}
Generic type
type gpair<'a, 'b> = ('a, 'b)
let x: gpair<string, int> = ("Counter", 128)
Js.log(x) // [ 'Counter', 128 ]

type result<'a, 'e> = 
    | Ok('a) 
    | Error('e)
let v = Ok(100)
let e = Error("Not Found Error")
Type escape hatch
external convertToString: char => string = "%identity"
let ch = 'a'
let str = convertToString(ch) ++ "bc"
Js.log(str) // 97bc

OCaml

(* type inference *)
let a = 10 ;;
let b = ("hello", 123) ;;
let add a b = a + b ;;

(* type annotation *)
let add (a:int) (b:int) : int = a + b ;;

(* type alias *)
type kg = float ;;
let weight : kg = 88.3 ;;
type pair = (string * int)
let p : pair = ("hello", 123) ;;

(* mutually recursive types *)
type person = {
  name: string;
  friends: person list;
}

type room = {building: building}
and building = {rooms: room array}

(* type paramater *)
type ('a, 'b) t = ('a * 'b) ;;
let p : (string, int) t = ("hello", 123) ;; 

type ('a, 'b) either = Left of 'a | Right of 'b ;;

Primitive Types

Rescript

  • unit
  • bool
  • int
  • float
  • string
  • char

JavaScript

  • undefined
  • null
  • boolean
  • number
  • bigint
  • string
  • symbol

OCaml

  • unit
  • bool
  • int
  • float
  • string
  • char

Unit

Rescript

  • has a single value ()
  • compiles to JS's undefined
let x = ()
Js.log(x) // undefined in js

OCaml

let x = () ;;
Unit.equal x x ;; (* true *)

String

Rescript

let name = "Ryan"

/* string concatenation */
let name = name ++ " Zan"

/* string interpolation */
let greeting1 = `Hello ${name}`
let greeting2 = j`Hola $name`

/* multiline string */
let greeting3 = `hello
${name},
sup?`

/* Js.String2 module to deal with String type */
let replaced = "testing".Js.String2.replate("t", "R")
let len = Js.String2.length("abc")
let sub = "hello world"->Js.String2.substring(~from=0, ~to_=5) // hello

OCaml

let name = "Ryan" ;;

(* string concatenation *)
let name = name ^ " Zan" ;;

(* string interpolation *)
let greeting = Printf.sprintf "hello, %s" name ;;

(* multiline string *)
let greeting3 = {|hello
Baloo
sup?|} ;;
(* same as *)
let greeting3 = "hello\nBaloo\nsup?"

(* wrapped long line *)
let wrapline = "hello \
Baloo \
sup?" ;;
(* same as *)
let wrapline = "hello Baloo sup?"

(* String, StringLabels module to deal with string *)
String.sub "Hello World" 0 5 (* hello *)

Char

Rescript

  • Char doesn't support Unicode or UTF-8 and is therefore not recommended.
let ch = 'a'

// char to string
Char.escaped(ch)

OCaml

let ch = 'a' ;;
(* convert char to string *)
let s = String.make 1 ch ;;
let s = Char.escaped ch ;;
let s = Printf.sprintf "%c" ch ;;
(* convert string to char *)
let ch = String.get "a" 0 ;;

Boolean

Rescript

true, false /* boolean value */
&& || !     /* logical and, or, not */
<= >= < >   /* structural comparison */
==, !=      /* structural equality/inequality */
(1,1) == (1,1)  /* true */
(1,1) != (1,1) /* false */

===, !==    /* referential equality/inequality */
(1,1) === (1,1)  /* false */
(1,1) !== (1,1) /* true */

OCaml

true, false   (* boolean value *)
&& || not     (* logical and, or, not *)
<= >= < >     (* structural comparison *)

=, <>         (* structural equality/inequality *)
(1,1) = (1,1)  (* true *)
(1,1) <> (1,1) (* false *)

==, !=    (* referential equality/inequality *)
(1,1) == (1,1)  (* false *)
(1,1) != (1,1)  (* true *)

Integer

Rescript

  • 32-bits, truncated when necessary
  • Much smaller range than JS numbers. Use float if bigger number needed.
  • Js.Int Doc
  • Js.Int Source
let n = 1_234_456
let m = n + n * n - n / n
let x = mod(10, 3) // remainder

Js.log(Js.Int.max) //  2147483647
Js.log(Js.Int.min) // -2147483648
Js.log(Js.Int.toFloat(10))

OCaml

let n = 1_234_456 ;;
let m = n + n * n - n / n ;;
let x = 10 mod 3 ;; 
let x = (mod) 10 3 ;;

Int.add 1 1 (* 2 *)
Int.of_float 10.2 (* 10 *)

Float

Rescript

let n = 1_23.45_56
let m = n +. n *. n -. n /. n
Js.log(m) // 15363.74077136


Js.Float.fromString("123.23")->Js.log // 123.23

OCaml

let n = 1_23.45_56 ;;
let m = n +. n *. n -. n /. n ;; (* 15363.74077136 *)

Regular Expression

Rescript

let re = %re("/^ryan.*myat$/i") // starts with ryan .. ends with myat
Js.log(Js.Re.test_(re, "Ryanzanthumyat")) // true

OCaml

(* using Re package *)
let re = Re.Perl.re {|^ryan.*myat$|} |> Re.no_case |> Re.compile ;;
Re.execp re "RyanzanthumyaT" (* true *)

Conversion

Rescript
open Js

// string to bool
bool_of_string("true")->log // true
bool_of_string("false")->log // false
bool_of_string_opt("maybe")->log // undefined
// error
// bool_of_string("maybe")->log

string_of_bool(true)->log // true
string_of_bool(false)->log // false
// string to int
int_of_string("100")->log // 100
int_of_string_opt("abc100")->log // undefined

int_of_string("100_000")->log // 100000
int_of_string("010")->log // 10

// hex literal
int_of_string("0xFF_FFF")->log // 1048575
int_of_string("0xFFFF_FFFF")->log // -1 - overflow

// binary literal
int_of_string("0b100")->log // 4
int_of_string("0b100_000")->log // 32

// octal literal
int_of_string("0o100")->log // 64
int_of_string("0o100_000")->log // 32768

// error
// int_of_string(" 111")->log // error
// int_of_string("111 ")->log // error

// int to string
1_000->Int.toString->log // 1000
0xff_fff->Int.toString->log // 104875
0b1_100->Int.toString->log // 12
0o1_100->Int.toString->log // 576

// string to float
float_of_string("1.1")->log // 1.1
float_of_string("  10. ")->log // 10
float_of_string("1.1e+4")->log // 11000
float_of_string_opt("a10.")->log // undefined
float_of_string(" 1.11 ")->log // 1.11

// float to string
1.1->Float.toString->log // 1.1

// int to float
(float_of_int(10) +. 10.)->log // 20
10->Int.toFloat->log // 10

// float to int
int_of_float(1.111)->log //

// char to string
Char.escaped('a')->log

// char to int
Char.code('a')->log
// int to char
(Char.chr(65) == 'A')->log

if else

Rescript

if statements are expression.

let max = (x, y) =>
  if x > y {
    x
  } else {
    y
  }

let f = (x, y) =>
  x + if y > 0 {
    y
  } else {
    0
  }
f(11, 0)->Js.log // 11

let rec range = (x, y) => {
  if x > y {
    list{}
  } else {
    list{x, ...range(x + 1, y)}
  }
}
range(1, 10)->Js.log

// unit aka () is return for implicit else branch
let a = if true {
  ()
}
/* error here cause if branch returns integer whereas else branch returns unit
let a = if true {
  10
}
*/
// ternary operator
let abs = (x: float) => x >= 0. ? x : x *. -1.
abs(-99.9)->Js.log

OCaml

OCaml doesn't have ternary operator.

let max x y = if x > y then x else y ;;

loops

Rescript

for i in 1 to 10 {
  i->Js.log
}

for i in 10 downto 1 {
  i->Js.log
}

let i = ref(1)
while i.contents <= 10 {
  i->Js.log
  incr(i)
}

/* breaking loop */
exception Break_the_loop
let break_loop = () => {
  try {
    for i in 1 to 100 {
      if i > 10 {
        raise(Break_the_loop)
      }
      i->Js.log
    }
  } catch {
  | Break_the_loop => ()
  }
}
break_loop()

let break = ref(false)
let i = ref(10)
while !break.contents {
  if i.contents <= 20 {
    i->Js.log
    incr(i)
  } else {
    break := true
  }
}

OCaml

for i = 1 to 10 do print_int i done ;;

for i = 10 downto 1 do print_int i done ;;

let i = ref 1 in
while !i <= 10 do 
  print_int !i;
  incr i
done ;;

exception Break_the_loop
let break_loop = 
try 
 for i = 1 to 100 do
   if i <= 10 then print_int i
   else raise Break_the_loop
 done;
 with Break_the_loop -> () ;;

Function

Basic

open Js
let add = (a, b) => a + b
log(add(1, 2))

let add_float: (float, float) => float = (x, y) => x +. y
log(add_float(1., 2.))

Recursive function

open Js
let rec neverTerminate = () => neverTerminate()

let fold = (l, acc, f) => {
  let rec loop = (l, acc) => {
    switch l {
    | list{} => acc
    | list{h, ...t} => loop(t, f(acc, h))
    }
  }
  loop(l, acc)
}
fold(list{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 0, add)->log // 55

Mutually recursive function

open Js
let rec isOdd = n =>
  if n == 0 {
    false
  } else {
    isEven(n - 1)
  }
and isEven = n =>
  if n == 0 {
    true
  } else {
    isOdd(n - 1)
  }
isOdd(10)->log
isOdd(11)->log

Curried vs uncuried function

// Functions in Rescript are curried by default.
open Js
let mul = (x, y) => x * y
let curried_mul = mul(10)
curried_mul(10)->log

// uncurried
let mulU = (. x, y) => x * y
mulU(. 10, 10)->log

// with type annotation
let addU: (. float, float) => float = (. x, y) => x +. y
addU(. 1., 2.)->log

// with placeholders
let add = (x, y, z) => x + y + z
let addX = add(_, 2, 3)
let addY = add(1, _, 3)
let addZ = add(1, 2, _)
addX(1)->Js.log // 6
addY(2)->Js.log // 6
addZ(3)->Js.log // 6
10->addX->Js.log // 15
10->addY->Js.log // 14
10->addZ->Js.log // 13

let addXY = add(_, _, 3)
addXY(2)->Js.log // 7
10->addXY->Js.log // 23

let addXYZ = add(_, _, _)
addXYZ(10)->Js.log // 30
100->addXYZ->Js.log // 300

ignore() function to ignore the return value of a function

mySideEffect()->Promise.catch(handleError)->ignore
Js.Global.setTimeout(myFunc, 1000)->ignore

Label arguments

open Js
let range = (~start, ~end) => Belt.Array.makeBy(end - start, x => x + start)
range(~start=0, ~end=10)->log

/* alias */
let range = (~start as s, ~end as e) => Belt.Array.makeBy(e - s, x => x + s)
range(~start=0, ~end=10)->log

Labels and type inference

labeled and optional arguments cannot be inferred completely

open Js
let bump = (~step=?, x) =>
  switch step {
  | Some(s) => x + s
  | None => x
  }

/* 
this will get compiler error, bump's signature should be provided

let bumpIt = (bump, x) => bump(~step=2, x)
bumpIt(bump, 10)->log
*/

let bumpIt = (bump: (~step: int=?, int) => int, x) => bump(~step=2, x)
bumpIt(bump, 10)->log


/* paramaters order */
let add = (~x, ~y, ()) => x + y
let f = g => g(~x=1, ~y=2, ())
f(add)->log // 3

// this won't work, paramaters should be applied in the same order as declaration site
let f = g => g(~y=1, ~x=2, ())

Optional Arguments(Rescript)

  • labeled arguments can be made optional
  • optional arguments can be omitted when calling the function
  • if omitted, the optional argument is set None
  • if provided, the optional argument will be wrapped with a Some
  • whenever you have an optional argument, you need to ensure that there's also at least one positional argument (aka non-labeled, non-optional argument) after it. If there's none, provide a dummy unit (aka ()) argument.

Optional arguments

open Js
let concat = (~sep=?, x, y) => {
  let sep = switch sep {
    | Some(s) => s
    | None => ""
  }
  x ++ sep ++ y
}
concat(~sep=",", "hello", "world")->log // hello,world
concat("hello", "world", ~sep=",")->log // hello,world

Need to add "()" if optional argument is the last

open Js
let concat = (x, y, ~sep=?, ()) => {
  let sep = switch sep {
  | Some(s) => s
  | None => ""
  }
  x ++ sep ++ y
}
concat(~sep=":", "hello", "world", ())->log // hello:world
concat("hello", "world", ~sep=":", ())->log // hello:world

With function type signature and paramater type annotation

open Js
let concat: (string, string, ~sep: string=?, unit) => string = (
  x,
  y,
  ~sep: option<string>=?,
  (),
) => {
  let sep = switch sep {
  | Some(s) => s
  | None => ""
  }
  x ++ sep ++ y
}
concat("hello", "world", ~sep="-", ())->log // hello-world

Passing value of option type to optional paramater

open Js
let separator = Some("/")
concat("hello", "world", ~sep=?separator, ())->log // hello/world
let sep = None
concat("hello", "world", ~sep?, ())->log // helloworld

Optioanl paramater with default value

if default value is provided, the optional argument is not wrapped in an option type.

open Js
let concat = (~sep=", ", x, y) => x ++ sep ++ y
concat("hello", "world")->log // hello, world
concat("hello", "world", ~sep="<>")->log // hello<>world

Partial Application

open Js
let concat = (x, y, ~sep=?, ()) => {
  let sep = switch sep {
  | Some(s) => s
  | None => ""
  }
  x ++ sep ++ y
}

let concatP = concat("Hello")
concatP("World", ~sep="~", ())->log
let concatP = concat("Hello", "World")
concatP(~sep="@", ())->log
let concatP = concatP(~sep="/\\")
concatP()->log
let concatP = concat(~sep="&")
concatP("Hello", "World", ())->log

let test: (
  ~x: int=?,
  ~y: int=?,
  unit,
  ~z: int=?,
  unit,
) => (option<int>, option<int>, option<int>) = (~x=?, ~y=?, (), ~z=?, ()) => (x, y, z)
test(~z=3)()()->log // [ undefined, undefined, 3]
test()()->log // [ undefined, undefined, undefined ]
test(~x=1, ~y=2, (), ())->log // [ 1, 2, undefined ]
test(~y=2, (), ~z=3, ())->log // [ undefined, 2, 3 ]
test((), (), ~y=2, ~x=1, ~z=3)->log // [ 1, 2, 3 ]

Optional Arguments(OCaml)

Run the following code at sketch.sh

Optional arguments

let concat ?sep x y =
  let sep = match sep with None -> "" | Some s -> s in
  x ^ sep ^ y ;;
concat ~sep:"," "Hello" "World";;

Need to add "()" if optional argument is the last

let concat x y ?sep () =
  let sep = match sep with None -> "" | Some s -> s in
  x ^ sep ^ y ;;
concat ~sep:"," "hello" "world" () ;;

With function type signature and paramater type annotation

(* val concat: string -> string -> ?sep:string -> unit -> string *)
let concat (x: string) (y: string) ?(sep:string option) () =
  let sep = match sep with None -> "" | Some s -> s in
    x ^ sep ^ y ;;
concat ~sep:"," "hello" "world" () ;;

Passing value of option type to optional paramater

let concat ?sep x y =
  let sep = match sep with None -> "" | Some s -> s in
  x ^ sep ^ y ;;
let separator = Some("/") in
concat ?sep:separator "hello" "world" ;;
let sep = None in
concat ?sep "hello" "world" ;;

Optional paramater with default value

let bump ?(step=1) x = x + step ;;  
bump 10;;

Partial application

let test ?x ?y () ?z () = (x, y, z) ;;
test () () ~x:1 ~y:2 ~z:3 ;;
test ()() ;;
test ~x:1 ~y:2 () () ;;

Rescript Function Cheatsheet

Anonymous function

(x, y) => x + y 
((x, y) => x + y)(1, 2)->Js.log // 3

/* with type annotation */
(x: int, y: int): int => x + y 
((x: int, y: int): int => x + y)(1, 2)->Js.log // 3

Bind a function to a name

let add = (x, y) => x + y
add(1, 2)->Js.log

/* with function signature */
let add: (int, int) => int = (x, y) => x + y
add(1, 2)->Js.log

/* with paramater type annotation */
let add = (x: int, y: int): int => x + y
add(1, 2)->Js.log 

/* with both function signature and paramater type annotation */
let add: (int, int) => int = (x: int, y: int): int => x + y
add(1, 2)->Js.log

Labeled Arguments

let add = (~first, ~second) => first + second
add(~first=2, ~second=2)->Js.log
add(~first=(2: int), ~second=(2: int))->Js.log

/* with punning */
let first = 100
let second = 200
add(~first, ~second)->Js.log
add(~first: int, ~second: int)->Js.log

/* with function signature */
let add: (~first: int, ~second: int) => int = (~first, ~second) => first + second
add(~first=2, ~second=2)->Js.log

/* with paramater type annotation */
let add = (~first: int, ~second: int): int => first + second
add(~first=2, ~second=2)->Js.log

/* with both function signature and paramater type annotation */
let add: (~first: int, ~second: int) => int = (~first: int, ~second: int): int => first + second
add(~first=2, ~second=2)->Js.log

Labeled Arguments with alias

let add = (~first as x, ~second as y) => x + y
add(~first=2, ~second=2)->Js.log

/* with function signature */
let add: (~first: int, ~second: int) => int = (~first as x, ~second as y) => x + y
add(~first=2, ~second=2)->Js.log

/* with paramater type annotation */
let add = (~first as x: int, ~second as y: int): int => x + y
add(~first=2, ~second=2)->Js.log

/* with both function signature and paramater type annotation */
let add: (~first: int, ~second: int) => int = (~first as x: int, ~second as y: int): int => x + y
add(~first=2, ~second=2)->Js.log

labeled arguments with default value

let add = (~first=111, ~second=222, ()) => first + second
add()->Js.log
let add = (~first as x=11, ~second as y=22, ()) => x + y
add()->Js.log

Optional arguments

let bump = (~step=?, x) =>
  switch step {
  | Some(s) => x + s
  | None => x
  }
bump(~step=10, 1)->Js.log
bump(~step=(10: int), 1)->Js.log
bump(~step=?Some(111), 1)->Js.log
bump(~step=?(Some(111): option<int>), 1)->Js.log
bump(~step=?None, 1)->Js.log

/* with signature and type annotation */
let bump: (~step: int=?, int) => int = (~step: option<int>=?, x: int): int =>
  switch step {
  | Some(s) => x + s
  | None => x
  }
bump(~step=10, 1)->Js.log
bump(~step=(10: int), 1)->Js.log
bump(~step=?Some(111), 1)->Js.log
bump(~step=?(Some(111): option<int>), 1)->Js.log
bump(~step=?None, 1)->Js.log

/* with alias */
let bump = (~step as s=?, x) =>
  switch s {
  | Some(i) => x + i
  | None => x
  }
bump(~step=11, 1)->Js.log
bump(~step=(11: int), 1)->Js.log

/* with punning */
let step = 1111
bump(~step, 1)->Js.log
let step = Some(1111)
bump(~step?, 1)->Js.log

Array

  • same as JavaScript Array
  • ramdom access
  • dynamically resized, updated

Rescript

/*
need to open Belt to use subscript operator
Array subscript operator ([]) is just a sugar over Belt.Array.get and Belt.Array.set
get: (array<'a>, int) => option<'a>
set: (array<'a>, int, 'a) => bool
*/
open Belt

let arr = ["a", "b"]
let a : option<string> = arr[0]
(arr[1] = "z") -> ignore
arr[1]->Js.log

shellshort

let shellShort = (arr, n) => {
    let interval = ref (n / 2)
    while interval.contents > 0 {
        for i in interval.contents to n - 1 {
            let temp = Array.getExn(arr, i)
            let j = ref(i)
            while j.contents >= interval.contents && Array.getExn(arr,j.contents - interval.contents) > temp {
                Array.setExn (arr, j.contents, Array.getExn (arr,j.contents - interval.contents))
                j := j.contents - interval.contents 
            }
            Array.setExn(arr, j.contents, temp)
        }
        interval := interval.contents / 2 
    }
}

let nums = [ 1, 2, 10, 11, 23, 45, 93, 20, 30, 93]
shellShort(nums, Array.length( nums))
Array.forEach(nums, Js.log)
/* Belt.Array.blit */
let arr1 = [1, 2, 3, 4, 5]
let arr2 = [10, 20, 30, 40, 50]
Belt.Array.blit(~src=arr1, ~srcOffset=1, ~dst=arr2, ~dstOffset=0, ~len = 3)
arr2->Js.log // [2, 3, 4, 40, 50]

OCaml

let arr = [| 1; 2; 3|];;
print_int arr.(0) ;;
arr.(0) <- 10 ;;
arr |> Array.iter print_int ;;

let shell_sort arr n =
  let interval = ref (n / 2) in
  while !interval > 0 do
    for i = !interval to n - 1 do
     let temp = arr.(i) in
     let j = ref i in
     while !j >= !interval && arr.(!j - !interval) > temp do
       arr.(!j) <- arr.(!j - !interval);
       j := !j - !interval;
     done;
     arr.(!j) <- temp;
    done;
    interval := !interval / 2;
  done
;; 
let nums = [| 1; 2; 34; 11; 34; 29; 93; 293; 2020; 22; 43; 2; 0; 11|];;
shell_sort nums (Array.length nums);;
nums |> Array.iter (Printf.printf "%d ") ;;

List

Rescript

  • immutable

  • fast at prepending items

  • fast at getting the head

  • slow at everything else

  • Use Array for

    • accessing random element
    • better interop with JavaScript
    • better memory usage and performance
let numList = list{1, 2, 3}
let numList2 = list{0, ...numList}

/* multiple spread is not allowed */
let numList2 = list{0, numList, ...numList2}

let foldLeft = (lst, acc, f) => {
  let rec loop = (l, acc) => {
    switch l {
    | list{} => acc
    | list{h, ...t} => loop(t, f(acc, h))
    }
  }
  loop(lst, acc)
}

let nums = Js.List.init(10, (. x) => x + 1)
foldLeft(nums, 0, (x, y) => x + y)->Js.log

let letters = Js.List.init(26, (. x) => String.make(1, Char.chr(x + 65)))
foldLeft(letters, "", (x, y) => x ++ "," ++ y)->Js.log

let foldRight = (lst, acc, f) => {
  let rec loop = (l, acc) => {
    switch l {
    | list{} => acc
    | list{h, ...t} => f(h, loop(t, acc))
    }
  }
  loop(lst, acc)
}
foldRight(letters, "", (x, y) => x ++ "," ++ y)->Js.log

Tuple

Rescript

  • immutable
  • ordered
  • fixed-sized at creation time
  • heterogeneous
let pair: (string, int) = ("Number", 1)
Js.log(pair) // ['Number', 1]
let (n, _) = pair
Js.log(n) // Number
let (x, y, z) = (1, 2, 3)

OCaml

let pair = ("Number", 1) ;;
let (n, _) = pair ;;
let (x, y, z) = (1, 2, 3) ;;

Record

Rescript

  • Immutable by default
  • Have fixed fields - not extensible,
  • Nominal typing
Type declaration, mutable/immutable update, destructuring
module Model = {
  type person = {
    name: string,
    age: float,
  }
}

let ryan: Model.person = {name: "Ryan", age: 6.5}
let zan = {Model.name: "Zan", age: 6.5}

// destructuring
let show = ({Model.name: name, age}) => {
  Js.log(j`Name: $name, Age : $age`)
}

// immutable update
let joe = {...zan, name: "Joe"}
Js.log(joe)

// mutable update
module MutableModel = {
  type person = {
    name: string,
    mutable age: float,
  }
}
let ryan: MutableModel.person = {name: "Ryan", age: 6.5}
ryan.age = 7.
Optional record fields
module OpModel = {
  type person = {
    name: string,
    nickName?: string,
    age: float,
  }
}

// None , if optional field is not set
let p1: OpModel.person = {name: "Ryan", age: 6.5}
Js.log(p1.nickName == None) // true

// use ? to set value
let p1: OpModel.person = {name: "Ryan", nickName: ?Some("Horn"), age: 6.5}
Js.log(p1.nickName) // Horn

// no need to use ? in immutable update
let p2 = {...p1, nickName: "kcin"}
Js.log(p2.nickName) // kcin

// if pattern match directly on optional record field, it's an optional
let hasNickName = switch p2.nickName {
	| Some(_) => true
	| None => false
}
Js.log(hasNickName) // true

// if matching on the field as part of general record structure, it's non-optional value
let nick = switch p2 {
	| {nickName: n} if String.length(n) > 0 => n
	| _ => ""
}
Js.log(nick) // "kcin"

// check if optional record field has value or not
let hasNickName = switch p2 {
	| {nickName: ?None} => false
	| {nickName: ?Some(_)} => true
}
Js.log(hasNickName) // true

// destructuring
let show = ({OpModel.name: name, age, _}) => {
  Js.log(j`=> { Name: $name, Age : $age }`)
}
show(p2) // => { Name: Ryan, Age: 6.5 }

// paramaterized record
module ParamaterizedModel = {
  type point<'a> = {x: 'a, y: 'a}
}
let p1: ParamaterizedModel.point<int> = {
  x: 1,
  y: 2,
}
Js.log(p1) // { x : 1, y : 2}
OCaml

My Note on OCaml record
OCaml doesn't have optional record field feature.

type car = {
  mutable color : string ;
  mutable weight : float ;
  kind : string ;
} ;;

let c1 = { color = "black" ; weight = 100.; kind = "sport"} ;;

let c2 = { c1 with color = "pink"} ;;

let show { color = c; weight = w; _ } = Printf.sprintf "color=%s wight=%F" c w ;;
show c1 ;;
show c2 ;;

(* mutate color field *)
c1.color <- "blue" ;;
show c1 ;;

Object

Rescript
  • Structural typing
  • No type declaration needed
  • Not supporting updates unless it comes from JS site
  • No support for pattern matching
// optional type declaration
type person = {"name": string, "age": int}
let ryan = {"name": "Ryan", "age": 6.5}
Js.log(ryan)
Js.log(ryan["age"])

// spread object type but not object values
type point2D = {"x": float, "y": float}
type pooint3D = {...point2D, "z": float}

// binding to Dom
@val external document : 'a = "document"
let loc = document["location"]
let href = loc["href"]
OCaml
let ryan = object
    val name : string = "Ryan"
    val mutable age : float = 6.5
    method get_age = age
    method set_age n = age <- n
    method to_string = Printf.sprintf "Name = Ryan, Age = %f" age
end ;;

let show o = print_string o#to_string ;;
show ryan ;;

Variant

Rescript

type rec json =
  | Assoc(list<(string, json)>)
  | Bool(bool)
  | Float(float)
  | Int(int)
  | List(list<json>)
  | String(string)
  | Null

let boy = Assoc(list{("name", String("Ryan")), ("age", Float(6.5))})
Paramaterised Variant
type maybe<'a> = Nothing | Just('a)

// list
type rec list<'a> = Nil | Cons('a, list<'a>)

let lst = Cons(1, Cons(2, Cons(3, Nil)))
let length = l => {
  let rec loop = (l, acc) => {
    switch l {
    | Nil => acc
    | Cons(_, t) => loop(t, acc + 1)
    }
  }
  loop(l, 0)
}
Js.log(length(lst)) // 3

// peano number
type rec peano = Zero | Succ(peano)

let count = p => {
  let rec loop = (p, a) => {
    switch p {
    | Zero => a
    | Succ(t) => loop(t, a + 1)
    }
  }
  loop(p, 0)
}

Js.log(count(Succ(Succ(Succ(Succ(Zero)))))) // 4
Variant with inline vs free standing records
type circle = {radius: float, x: float, y: float}
let c = {radius: 1.1, x: 10., y: 20.2}
type shape =
  | Square(float)
  | Rectangle({width: float, height: float})
  | Circle(circle)
let display_shape = s => {
  open Js.Float
  switch s {
  | Circle({radius, x, y}) =>
    Js.log(
      `radius:${toString(radius)}, x:${toString(x)}, y:${toString(y)}`,
    )
  | Square(x) => Js.log(`Square of ${toString(x)}`)
  | Rectangle({width, height}) =>
    Js.log(`width:${toString(width)}, height:${toString(height)}`)
  }
}
display_shape(Rectangle({width: 1.1, height: 2.2}))
display_shape(Circle(c))

/* 
 Limitation of inline record.
 The following code will get compile time error - 
 'This form is not allowed as the type of the inlined record could escape.'
*/
let get_shape = s => {
  switch s {
  | Rectangle(r) => Some(r)
  | _ => None
  }
}
Variant with multiple arguments vs tuple
type point = Point(float, float)
let p = Point(1.1, 2.2)
let Point(x, y) = p

/* varint with tuple can be grabbed as a whole */
type point = Point((float, float))
let p = Point((1.1, 2.2))
let Point(p') = p
Js.log(p') // [ 1.1, 2.2]
OCaml

My Note Here

Example(blang)

Example from RealWorldOCaml

type rec expr<'a> =
  | Base('a)
  | Const(bool)
  | And(list<expr<'a>>)
  | Or(list<expr<'a>>)
  | Not(expr<'a>)

let and_ = l => {
    if Belt.List.some(l, (x) => switch x { 
        | Const(false) => true 
        | _ => false }) { 
        Const(false) 
    }
    else {
        switch Belt.List.keep(l, x => switch x { 
            | Const(true) => false 
            | _ => true}) {
                | list{} => Const(true)
                | list{x} => x
                | l => And(l)
        }
    }
}

let or_ = l => {
   open Belt
   if List.some(l, x => switch x { | Const(true) => true | _ => false}) { Const(true) }
   else {
       switch List.keep(l, x => switch x { | Const(false) => false | _ => true }) {
           | list{} => Const(false)
           | list{x} => x
           | l => Or(l)
       }
   }
}

let not_ = (expr) => {
    switch expr {
        | Const(x) => Const(!x)
        | e => Not(e)
    }
}

let rec simplify = (expr) => {
    open Belt
    switch expr {
        | (Base(_) | Const(_)) as x => x
        | And(l) => and_ (List.map(l, x => simplify(x)))
        | Or(l) => or_ (List.map(l, x => simplify(x)))
        | Not(e) => not_ (simplify(e))
    }
}

let rec eval = (expr, base_eval) => {
  let eval' = expr => eval(expr, base_eval)
  switch expr {
  | Base(base) => base_eval(base)
  | Const(b) => b
  | And(exprs) => Belt.List.every(exprs, eval')
  | Or(exprs) => Belt.List.some(exprs, eval')
  | Not(expr) => !eval'(expr)
  }
}

type mail_field = To | From | CC | Date | Subject
type mail_predicate = {field: mail_field, contains: string}
let test = (field, contains) => Base({field, contains})

let base_eval = ({field, contains}) => {
  switch field {
  | To => contains == "Ryan"
  | Subject => contains == "Maths"
  | _ => true
  }
}

let expr = And(list{
        Or(list{test(To, "Ryan"), test(Subject, "Maths"), Const(false)}), 
        Const(true), 
        Not(Const(true))
    })

let result = eval(simplify(expr), base_eval)
Js.log(result) // false

Polymorphic Variant

Rescript
  • explicit type declaration is optional
  • type will be inferred automatically
  • structural typing
  • have leading hash e.g. #A
  • > at the beginning of the variant type means the type is open to combination with other variant types
  • > means lower bound, < means upper bound. If the same set of tags are both an upper and a lower bound, we end up with an exact polymorphic variant type, which has neither marker.
Basic
let b = #Red
let l = #"aria-hidden"
let x = #"I am ❌"
Js.log(b) // Red
Js.log(l) // aria-hidden
Type Declaration (optional)
type color = [#red | #green | #blue]
let r: color = #red
Inline
let show = (v: [ #int(int) | #float(float) | #string(string) ]) => {
  switch v {
  | #int(i) => Js.log(i)
  | #float(f) => Js.log(f)
  | #string(s) => Js.log(s)
  }
}
show(#float(1.1))
structual sharing
type bool_or_char = [#bool(bool) | #char(char)]
let bc: bool_or_char = #bool(true)

let display = x => {
  switch x {
  | #bool(b) => Js.log(b)
  | #char(c) => Js.log(c)
  | #unit => Js.log("unit")
  }
}
display(bc)
display(#unit)
display(#char('a'))
lower bound poly variant with array
let three = #Int(3)
let four = #Float(4.)
let nan = #Not_a_number
Js.log([three, four, nan])

// list is immutable, so both open & close poly variant are allowed
let lst: list<[> #Int(int) | #Float(float) | #Not_a_number]> = list{three, four, nan}
Js.log(lst)
let lst: list<[< #Int(int) | #Float(float) | #Not_a_number]> = list{three, four, nan}
Js.log(lst)

// Rescript won't compile the following line 
let arr: array<[> #Int(int) | #Float(float) | #Not_a_number]> = [three, four, nan] // compilation error


let arr: array<[< #Int(int) | #Float(float) | #Not_a_number]> = [three, four, nan]
Js.log(arr)
Combine Types and Pattern match
type abc = [#a | #b | #c]
type def = [#d | #e | #f]
let show = x => {
  switch x {
  | #...abc => Js.log("abc")
  | #...def => Js.log("def")
  }
}
show(#a)
show(#f)
Constraints lower bound
type basic_color<'a> = [> #Red | #Green | #Blue] as 'a
type advanced_color = basic_color<[#Red | #Green | #Blue | #Pink]>
let bc: basic_color<[> #Red | #Green | #Blue]> = #Blue
let ac: advanced_color = bc
Js.log(ac)
Constraints upper bound
type xyz<'t> = [< #A | #B | #C] as 't
type omega = xyz<[#A]>
let x: xyz<[< #A | #B | #C]> = #A
let o: omega = x
Js.log(o)
Subtyping
type choice = [ #Yes | #No | #Maybe ]
let yes = #Yes
let ch1 : choice = yes :> choice

let color : [> #Red ] = #Green
Js.log(color) // green

OCaml

My note on OCaml poly-variant

Extensible Variant

Rescript

module Expr = {
  type t = ..

  type t += Int(int)
  type t += Float(float)
}

type Expr.t += String(string)

let toString = e => {
  switch e {
  | Expr.Int(i) => Js.log(i)
  | Expr.Float(f) => Js.log(f)
  | String(s) => Js.log(s)
  | _ => Js.log("other")
  }
}

toString(Expr.Int(10))
toString(Expr.Float(3.14))
toString(String("Hello"))
exceptios are extensible variant
exception ConnectionError(string)
// is equivalent to
type exn += ConnectionError(string)

OCaml

My Notes here

module Expr = struct
    type t = ..
    
    type t += Int of int
    
    type t += Float of float 
      
end ;;

type Expr.t += String of string ;;

let to_string = function
    | Expr.Int x -> Int.to_string x
    | Expr.Float x -> Float.to_string x
    | String x -> x 
    | _ -> "?" ;;

Exception

Rescript

/* rescript exception */
exception Empty(string)

type rec series<'a> = [#Nil | #Cons('a, series<'a>)]

let getHeadExn = s => {
  switch s {
  | #Nil => raise(Empty("list is empty"))
  | #Cons(h, _) => h
  }
}

let showFirstItem = s => {
  try {
    let hd = getHeadExn(s)
    Js.log(hd)
  } catch {
  | Empty(e) => Js.log(e)
  }
}

let s = #Cons(0, #Cons(1, #Cons(2, #Cons(3, #Nil))))
showFirstItem(s) // 0
showFirstItem(#Nil) // list is empty

/* exception can be pattern-matched */
switch getHeadExn(#Nil) {
| #Cons(h, _) => Js.log(h)
| exception Empty(e) => Js.log(e)
}

Catching Rescript Exception from JS

Raising an exception from Rescript
exception BadArgument({myMessage: string})

let myTest = () => {
  raise(BadArgument({myMessage: "Oops!"}))
}
Catching it from JavaScript
// after importing `myTest`...
try {
  myTest()
} catch (e) {
  console.log(e.myMessage) // "Oops!"
  console.log(e.Error.stack) // the stack trace
}

OCaml Exception

exception Empty of string ;;
type 'a series = [ `Nil | `Con of 'a * 'a series ] ;;

let se = `Con (1, `Con(2, `Con(3, `Con (4, `Con (5, `Nil))))) ;; 

let hd = function
    | `Nil -> raise (Empty "list is empty") 
    | `Con (h, _) -> h ;;  

let show_hd s =
  try
    let fst = hd s in
    print_int fst
  with
   | Empty e -> print_string e ;;
   
show_hd se ;;
show_hd `Nil ;;

match hd `Nil with | `Con(h, _) -> print_int h | exception Empty e -> print_string e ;;

JS Exception

Catching a JS Exception in Rescript

@new external make: float => Js.Array2.t<int> = "Array"
try {
  let a = make(111111111111111111111111111.1)
} catch {
| Js.Exn.Error(obj) =>
  switch Js.Exn.message(obj) {
  | Some(e) => Js.log(e)
  | None => Js.log("No message")
  }
}
// Invalid array length

Raise a JS Exception from Rescript

Raising from Rescript site
let myTest = () => {
  Js.Exn.raiseError("Hello!")
}
Catching it from JS side
// after importing `myTest`...
try {
  myTest()
} catch (e) {
  console.log(e.message) // "Hello!"
}

Option, Null, Undefined

option

Interoperate with JS undefined and null

  • Never, EVER, pass a nested option value (e.g. Some(Some(Some(5)))) into the JS side.

  • Never, EVER, annotate a value coming from JS as option<'a>. Always give the concrete, non-polymorphic type.

  • Use Js.Nullable.null or Js.Nullable.undefined if expecting null or undefined from JS side

// consuming JS null 
@module("MyConstant") external myId: Js.Nullable.t<string> = "myId"

// passing Nullable from Rescript to JS
@module("MyIdValidator") external validate: Js.Nullable.t<string> => bool = "validate"
let personId: Js.Nullable.t<string> = Js.Nullable.return("abc123")
let result = validate(personId)

Pipe

Rescript

  • pipe "->" is just a syntactic sugar not like "|>" which is an infix operator
  • pipe "->" is for data-first APIs
// [ 2, 3, 1 ] cause JS.Array is designed for data-last APIs
[1]->Js.Array.concat([2, 3])->Js.log 

// [ 1, 2, 3 ] cause JS.Array2 is designed for data-first APIs
[1]->Js.Array2.concat([2, 3])->Js.log 

pipe-placeholder


let add = (x, y, z) => x + y + z
let addX = add(_, 2, 3)
let addY = add(1, _, 3)
let addZ = add(1, 2, _)
10->addX->Js.log // 15
10->addY->Js.log // 14
10->addZ->Js.log // 13

let addXY = add(_, _, 3)
10->addXY->Js.log // 23

let addXYZ = add(_, _, _)
100->addXYZ->Js.log // 300

open Belt
let filter = (f, arr) => {
  let r = []
  for i in 0 to Array.length(arr) - 1 {
    let item = Array.getExn(arr, i) 
    if f(item) {
        Belt.Array.push(r, item)
    }
  }
  r
}

let nums = [1, 2, 3, 4, 5, 9 , 10, 1001]
filter(x => mod(x, 2) > 0, nums)->Js.log

// using pipe-placeholder
nums -> filter(x => mod(x, 2) > 0, _ ) -> Js.log

with labeled-arguments

let add = (x, ~y, ~z, ()) => x + y + z
10 -> add(10,~y=_,~z=10, ())->Js.log // 30
10 -> add(10,~y=10,~z=_, ())->Js.log // 30
10 -> add(10,~y=_,~z=_, ())->Js.log // 30
10 -> add(_,l~y=_,~z=_, ())->Js.log // 30
() -> add(10,~y=10,~z=10, _)->Js.log // 30

Lazy

  • defer and cache
  • can't re-trigger the computation after the first Lazy.force
  • it's not shared data type between Rescript and JavaScript
let rec fib = n => {
  switch n {
  | 0 | 1 => n
  | _ => fib(n - 1) + fib(n - 2)
  }
}

/* using Lazy.force */
let lfib = lazy(fib(20))
lfib->Lazy.force->Js.log

/* pattern match with switch */
switch lfib {
    | lazy(result) => Js.log(result)
}

/* pattern match with let binding */
let lazy(n) = lfib
n->Js.log

/* with exception handling */
let n = try {
        Lazy.force(lazy(fib(22)))
    } catch {
     |Some_error => 0
    }

Patterns

Variable pattern: binding the name to the value

let (x, y, z) = (1, 2, 3)
let (x, _, _) = (1, 2, 3)
let is_sorted = nums => {
  switch nums {
  | (x, y, z) if (x <= y && y <= z) || (x >= y && y >= z) => true
  | _ => false
  }
}
is_sorted((1, 2, 3))->Js.log
is_sorted((3, 2, 1))->Js.log
is_sorted((3, 11, 1))->Js.log

Constant pattern

exception Invalid_argument(string)
let isOn = x => {
  switch x {
  | "On" => true
  | "Off" => false
  | _ => raise(Invalid_argument("isOn"))
  }
}
isOn("On")->Js.log

Alias pattern

let sortPair = ((x, y) as p) =>
  if x < y {
    p
  } else {
    (y, x)
  }
sortPair((1, 2))->Js.log

let greaterOne = ((x1, y1) as p1, (x2, y2) as p2) =>
  if x1 > x2 || y1 > y2 {
    p1
  } else {
    p2
  }
greaterOne((11, 2), (2, 2))->Js.log

type rect = {width: float, height: float}
let r = {width: 1.1, height: 2.2}
let {width} = r
let {width: w} = r

Or pattern

let isVowel = v =>
  switch v {
  | 'a' | 'e' | 'i' | 'o' | 'u' => true
  | _ => false
  }
isVowel('a')->Js.log

let rects = [{width: 1.1, height: 2.2}, {width: 10.1, height: 2.2}, {width: 2.1, height: 2.2}]
let smallRects = rects => {
  rects->Belt.Array.keep(x =>
    switch x {
    | {width: 1.1 | 2.1} => true
    | _ => false
    }
  )
}
smallRects(rects)->Js.log

rects
->Belt.Array.keep(x => {
  switch x {
  | {width: w} if w > 10. => true
  | _ => false
  }
})
->Js.log

Variant pattern

type kg = Kg(float)
let weight = Kg(100.)
let Kg(w) = weight
let test = (Kg(w)) => Js.log(w)

type rec expr = Int(int) | Add(expr, expr)
let rec eval = expr =>
  switch expr {
  | Int(i) => i
  | Add(e1, e2) => eval(e1) + eval(e2)
  }
eval(Add(Int(10), Int(20)))->Js.log

type either<'a, 'b> = Left('a) | Right('b)
let left = Left(10)
let Left(l) | Right(l) = left
let choice = (Left(x) | Right(x)) => Js.log(x)
choice(Right(100))

Polymorphic variant pattern

let check = s =>
  switch s {
  | #Ok(x) => x
  | #Error(e) => e
  }
check(#Ok("hello"))->Js.log
check(#Error(404))->Js.log

Polymorphic variant abbreviation pattern

type color = [#Red(int) | #Green(int) | #Blue(int)]
type more_color = [color | #Pink(int) | #Gray(int)]
type much_more_color = [more_color | #Slate(int)]

let display = c =>
  switch c {
  | #...color => Js.log("Basic Color ")
  | #...more_color => Js.log("Advanced Color")
  | #...much_more_color => Js.log("Fantastic Color")
  }
display(#Green(10))
display(#Slate(10))
display(#Gray(10))

tuple pattern

let choose = ((0, x, _) | (_, _, x)) => x
choose((0, 2, 3))->Js.log // 2
choose((1, 2, 3))->Js.log // 3
let choose = ((x, None) | (_, Some(x))) => x
choose((1, None))->Js.log // 1
choose((1, Some(10)))->Js.log // 10

record pattern

type person = {name: string}
let blonde = {name: "Blondie"}
let tuco = {name: "Tuco"}
let eyes = {name: "Eyes"}
type gunslinger =
  | Good(person)
  | Bad(person)
  | Ugly(person)
  | TheJazz({name: string})

let who = gs =>
  switch gs {
  | Good({name: n}) => Js.log(n)
  | Bad({name: n}) => Js.log(n)
  | Ugly({name: n}) => Js.log(n)
  | TheJazz({name: "Jazz" | "Taltos"} as j) => Js.log(j.name)
  | _ => Js.log("NPC")
  }
who(TheJazz({name: "Jazz"}))
who(TheJazz({name: "Taltos"}))
who(Good(blonde))

Optional record field pattern

type person = {
    name: string,
    nickName?: string,
    age: float,
}
let p1 = { name : "jack", age ; 14 }
let hasNickName = switch p1 {
	| {nickName: ?None} => false
	| {nickName: ?Some(_)} => true
}

array pattern

let arr = Belt.Array.makeBy(3, x => Belt.Array.makeBy(3, _ => (x + 1) * 2))
let dig = arr =>
  switch arr {
  | [[x, _, _], [_, y, _], [_, _, z]] => (x, y, z)
  | _ => (0, 0, 0)
  }
dig(arr)->Js.log

range pattern

let classify = ch =>
  switch ch {
  | 'A' .. 'Z' => "uppercase"
  | 'a' .. 'z' => "lowercase"
  | '0' .. '9' => "digit"
  | _ => "other"
  }
classify('H')->Js.log
classify('3')->Js.log

lazy pattern

let l = Lazy.from_fun(() => 1000 * 100)
let lazy r = l
Js.log(r)

switch Lazy.from_fun(() => 1000 * 1000) {
| lazy l => Js.log(l)
}

switch Some(Lazy.from_fun(() => 5000 * 1000)) {
| Some(lazy l) => Js.log(l)
| None => Js.log(0)
}

exception pattern

switch Belt.List.headExn(list{}) {
| exception _ => Js.log("empty error")
| n => Js.log(n)
}

module destructuring pattern

module M = {
    type t = { name : string}
    let godot = { name : "godot"}
}
let gname = ({M.name : n}) => n
gname(M.godot)->Js.log

OCaml

My OCaml Notes here

Module

  • Module name must begin with an uppercase letter
  • Every .res file is a module. The convention is to capitalize file names.
  • Every .resi file is a signature.

Rescript

Module definition

module M = {
  let x = 17
  let y = 25
  %%private(let z = 10)
}

Module definition with signature

module Point: {
  let x: int
  let y: int
} = {
  let x = 10
  let y = 20
  let z = 40
}

Empty module

module E = {}

Module nesting

module X = {
  let x = 10
  module Y = {
    let y = 11
  }
  module Z = {
    let z = 12
  }
}
X.Y.y->Js.log
X.Z.z->Js.log

Extending module

module M1 = {
  let one = 1
}

module M2 = {
  let two = 2
}

module M3 = {
  include M1
  include M2
  let three = 3
}
open Js.Int
Js.log(`${M3.one->toString}, ${M3.two->toString}, ${M3.three->toString}`)

#### Opening module
open M3
one->Js.log
two->Js.log

Overriding open to turn off shadow warnings

module A = {
  let a = 1
}
module Foo = {
  module A = {
    let a = "one"
  }
}
open A
open! Foo.A
a->Js.log

Destructuring module

/* types cannot be extracted with module destructuring */
module Point2D = {
  type t = (float, float)
  let make = (x, y) => (x, y)
  let getX = ((x, _)) => x
  let getY = ((_, y)) => y
  let add = ((x1, y1), (x2, y2)) => (x1 +. x2, y1 +. y2)
  let substract = ((x1, y1), (x2, y2)) => (x1 -. x2, y1 -. y2)
}

let {make, add, substract} = module(Point2D)
let p1 = make(1.1, 2.2)
let p2 = make(10.1, 20.2)
add(p1, p2)->Js.log
substract(p2, p1)->Js.log

OCaml

My OCaml Notes here

Module Type

Rescript

Module type declaration

module type T = {
  let x: int
}

module M = {
  let x = 10
}

module MT1: T = M // same as module MT1 = (M : T)
MT1.x->Js.log

Sealing module with Module Type

module M2: T = {
  let x = 10
}
module MT2: T = M2
MT2.x->Js.log

module Z = {
  let z = 11.11
}
module M3: module type of Z = {
  let z = 22.22
}

Sharing constraint with "with" operator

module type TPair = {
  type t
  let pair: t
}

module Pair: TPair = {
  type t = (float, float)
  let pair: t = (11., 22.)
}

let p = Pair.pair // this is ok
let p': Pair.t = Pair.pair // this is ok
/* this will throw error cause TPair.t is abstrat type and not compitable with (float, float) */
// let p: (float, float) = Pair.pair

// use "with" operator
module APair: TPair with type t = (int, int) = {
  type t = (int, int)
  let pair = (1, 2)
}
let pp: (int, int) = APair.pair

module type TP = TPair with type t = (float, float)
module TPPair: TP = {
  type t = (float, float)
  let pair = (1.1, 2.2)
}
let pt: (float, float) = TPPair.pair

Destructive Substitution (:=) replace abstract type with the concrete type provided in constraint

module type List = {
  type item
  type t
  let data: t
}

module MList: List with type item := int and type t := list<int> = {
  type item = int
  type t = list<item>
  let data: t = Belt.List.makeBy(10, x => x + 1)
}
let lst: list<int> = MList.data

Module constraint with the "with" operator

module type TXY = {
  type x
  type y
}

module type MType = {
  module A: TXY
}

module B = {
  type x = int
  type y = int
  let x = 101
}

module C: MType with module A = B = {
  module A = {
    type x = int
    type y = int
    let x = B.x
  }
}
C.A.x->Js.log // 101
realworld code from darklang editor
module Belt = {
  include (Belt: module type of Belt with module Map := Belt.Map and module Result := Belt.Result)

  module Result = {
    include Belt.Result

    let pp = (
      okValueFormatter: (Format.formatter, 'okValue) => unit,
      errValueFormatter: (Format.formatter, 'errValue) => unit,
      fmt: Format.formatter,
      value: t<'okValue, 'errValue>,
    ) => {
      switch value {
      | Ok(value) =>
        Format.pp_print_string(fmt, "Ok")
        okValueFormatter(fmt, value)
      | Error(value) =>
        Format.pp_print_string(fmt, "Error")
        errValueFormatter(fmt, value)
      }
    }
  }

  module Map = {
    include (Belt.Map: module type of Belt.Map with module String := Belt.Map.String)

    module String = {
      include Belt.Map.String

      let pp = (
        valueFormatter: (Format.formatter, 'value) => unit,
        fmt: Format.formatter,
        map: t<'value>,
      ) => {
        Format.pp_print_string(fmt, "{ ")
        Belt.Map.String.forEach(map, (key, value) => {
          Format.pp_print_string(fmt, key)
          Format.pp_print_string(fmt, ": ")
          valueFormatter(fmt, value)
          Format.pp_print_string(fmt, ",  ")
        })
        Format.pp_print_string(fmt, "}")
        ()
      }
    }
  }
}

Extendng module type

module type TCounter = {
  type t
  let counter: t
}
module type TUpDown = {
  include TCounter
  let up: t => t
  let down: t => t
}
module Counter: TUpDown with type t = int = {
  type t = int
  let counter = 0
  let up = c => c + 1
  let down = c => c - 1
}
let count = 0
Counter.up(count)->Js.log
Counter.down(count)->Js.log

Recovering the type of a module

module Pi = {
  let pi = 3.14
}

module type TMaths = {
  include module type of Pi
}

module Maths: TMaths = {
  let pi = 3.141576
}
Maths.pi->Js.log

Empty module type

Empty module type is mainly used to hide the implementation of a module.

/* foo.mli */
module type S = {}

/* foo.ml */
module type S = {
  type t
  let unsafe_op: t => t
}

module M: S = {
  type t = int
  let unsafe_op = x => x + x
}

module Make = (X: S) => {
  let safe_op = x => X.unsafe_op(x)
}

OCaml

My OCaml Notes here

Functor

Rescript

  • Functors use the module keyword
  • Functors take modules as arguments and return a module
  • Funcotrs require annotating arguments
  • Functors must start with a capital letter
module type X = {
  let x: int
}
module type Y = {
  let y: int
}

module MX = {
  let x = 10
}
module MY = {
  let y = 10
}

module type TXY = {
  let xy: int
}

module F = (M1: X, M2: Y): TXY => {
  let xy = M1.x + M2.y
}

module XY = F(MX, MY)
XY.xy->Js.log

sample code

module type POINT = {
  type item
  type t
  let make: (item, item) => t
  let getX: t => item
  let getY: t => item
  let add: (t, t) => t
  let substract: (t, t) => t
}

module type ADD_SUB = {
  type t
  let add: (t, t) => t
  let substract: (t, t) => t
}

module MakePoint = (MP: ADD_SUB): (POINT with type item := MP.t) => {
  type item = MP.t
  type t = (item, item)
  let make = (x, y) => (x, y)
  let getX = ((x, _)) => x
  let getY = ((_, y)) => y
  let add = ((x1, y1), (x2, y2)) => (MP.add(x1, x2), MP.add(y1, y2))
  let substract = ((x1, y1), (x2, y2)) => (MP.substract(x1, x2), MP.substract(y1, y2))
}

module Int = {
  type t = int
  let add = (x: t, y: t) => x + y
  let substract = (x: t, y: t) => x - y
}

module Float = {
  type t = float
  let add = (x: t, y: t) => x +. y
  let substract = (x: t, y: t) => x -. y
}

module IntPoint = MakePoint(Int)
let ip1 = IntPoint.make(10, 10)
let ip2 = IntPoint.make(11, 11)
IntPoint.add(ip1, ip2)->Js.log

module FloatPoint = MakePoint(Float)
let ip1 = FloatPoint.make(10., 10.)
let ip2 = FloatPoint.make(11., 11.)
FloatPoint.add(ip1, ip2)->Js.log

OCaml

My Ocaml Notes here

First-class Module

Rescript

Packing a module as a first-class value and unpacking it into a module

module type T = {
  let x: int
}
module M = {
  let x = 77
}
let packed = module(M: T)
module Unpacked = unpack(packed)

Passing first-class module to a function and unpacking it using pattern match

let show = (m: module(T)) =>
  switch m {
  | (module(UM: T)) => UM.x->Js.log
  }
show(packed)

Returning a first-class module from a function

let getM = (x: int): module(T) => {
  module(
    {
      let x = x
    }
  )
}

module UM = unpack(getM(10))
UM.x->Js.log

With locally abstract type

module type DOUBLE = {
  type t
  let double: t => t
}

let doubleList = (type a, nums: array<a>, mb: module(DOUBLE with type t = a)) => {
  module D = unpack(mb)
  Belt.Array.map(nums, D.double)
}

module DInt: DOUBLE with type t = int = {
  type t = int
  let double = x => x * 2
}

module DFloat: DOUBLE with type t = float = {
  type t = float
  let double = x => x *. 2.
}

doubleList([1, 2, 3], module(DInt))->Js.log
doubleList([4.3, 3.2, 32.2], module(DFloat))->Js.log

With module type having abstract type

module type Comparable = {
  type t
  let compare: (t, t) => int
}

type comparable<'a> = module(Comparable with type t = 'a)

let compare = (type a, x, y, c: comparable<a>) => {
  module C = unpack(c)
  C.compare(x, y)
}

module IntCmp: Comparable with type t = int = {
  type t = int
  let compare = (x, y) => x - y
}

module FloatCmp: Comparable with type t = float = {
  type t = float
  let compare = (x, y) => x > y ? 1 : y > x ? -1 : 0
}

compare(1, 2, module(IntCmp))->Js.log
compare(1., 2., module(FloatCmp))->Js.log

Functor as first-class module

module type Stringable = {
  type t
  let toString: t => string
}

module type MakePrinterType = (Item: Stringable) =>
{
  let print: Item.t => string
}

module MakePrinter = (Item: Stringable) => {
  let print = x => "Printer: " ++ Item.toString(x)
}

module StringableInt = {
  type t = int
  let toString = Js.Int.toString
}

module StringableFloat = {
  type t = float
  let toString = Js.Float.toString
}

let f = module(MakePrinter: MakePrinterType)
module IntPrinter = unpack(f)(StringableInt)
IntPrinter.print(45322)->Js.log

module FloatPrinter = MakePrinter(StringableFloat)
FloatPrinter.print(112.211)->Js.log

OCaml

My OCaml Notes here

Decorator & Extension Points

Decorator

  • To annotate a piece of code to express extra functionality
@inline let nodeEnv = "prod"
let env = nodeEnv

/* JS output */
let env = "prod"
@val external process: 'a = "process"

@inline
let mode = "development"

if (process["env"]["mode"] === mode) {
  Js.log("Dev-only code here!")
}

/* JS output */
if (process.env.mode === "development") {
  console.log("Dev-only code here!");
}

@unboxed

To unwrap (aka unbox) the JS object wrappers from the output for records with a single field and variants with a single constructor and single payload.

type coordinates = {x: float, y: float}
@unboxed type localCoordinates = Local(coordinates)
@unboxed type worldCoordinates = World(coordinates)

let renderDot = (World(coordinates)) => {
  Js.log3("Pretend to draw at:", coordinates.x, coordinates.y)
}

let toWorldCoordinates = (Local(coordinates)) => {
  World({
    x: coordinates.x +. 10.,
    y: coordinates.x +. 20.,
  })
}

let playerLocalCoordinates = Local({x: 20.5, y: 30.5})

renderDot(playerLocalCoordinates->toWorldCoordinates)

Extension Point

  • Extension points are attributes that don't annotate an item; they are the item.
  • Extension points start with %. A standalone extension point starts with %%.
let add = %raw("(a,b) => a + b")
add(2,2)

let pattern = %re("/^[A-Z]/g")

let f = (x, y) => {
  %debugger
  x + y
}

// standalone extension aka top level raw
%%raw(`import '../style/main.css'`)

%%raw(`
    var message = "hello";
    function greet(m) {
      console.log(m)
    }
`)

Binding

external is like a let binding, but:

  • The right side of = isn't a value; it's the name of the JS value you're referring to.
  • The type for binding is mandatory.
  • Can only exist at the top level of a file or module.

Binding to JS global functions - Js.Global

Refs

Illegal identifier

Illegal identifiers can be created by prefixing \.

let \"orange 🍊" = "🍊"
\"orange 🍊"->Js.log

type element = {
  \"aria-label": string
}

let myElement = {
  \"aria-label": "close"
}

let label = myElement.\"aria-label"

let calculate = (~\"Props") => {
  \"Props" + 1
}

Global JS Values

@val, @scope

type immediateId
@val external setImmediate: (float => unit, float) => immediateId = "setImmediate"
@val external clearImmediate: immediateId => unit = "clearImmediate"
// usage
let id = setImmediate(x => Js.log(x), 101.101)
clearImmediate(id)

@val @scope("Math")
external pi : float = "PI"
pi->Js.log
// or
@val external py : float = "Math.PI"
py->Js.log

@val @scope(("window", "localStorage"))
external clear: unit => unit = "clear"
clear()
// js => window.localStorage.clear()
// Or 
@val external localStorageClear: unit => unit = "localStorage.clear"
localStorageClear()
// js => localStorage.clear()

@val external is: ('a, 'b) => bool = "Object.is"
is(1,2)->Js.log
// Or
@val @scope("Object")
external is: ('a, 'b) => bool = "is"
is(Js.Float._NaN, Js.Float._NaN)->Js.log

Special global values %external

switch %external(__DEV__) {
| Some(_) => Js.log("dev mode")
| None => Js.log("production mode")
}

switch %external(__filename) {
| Some(f) => Js.log(f)
| None => Js.log("non-node environment")
};

Import from/Export to JS

Rescript supports 2 JS import/export formats.

  • Common JS: require("MyFile") and module.exports = ...
  • ES6 modules: import * from MyFile and export let ...

It can be configured via bsconfig.json

{
  "package-specs": {
    "module": "commonjs",
    "in-source": true
  }
}

"module": "es6 resolves node_modules using relative paths.

Export to JavaScript

  • named exporting is already there
  • for default export, just let default = 123.

A JavaScript default export is really just syntax sugar for a named export implicitly called default.

Binding to default or named export

JavaScript

/* utils.js */

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

function max(x, y) {
  return x > y ? x : y;
}

function min(x, y) {
  return x < y ? x : y;
}

const lang = {
  name: 'Rescript',
  type: 'FP',
  version: 10.2,
};

export default lang;
export { lang, fib, max, min };

Rescript Binding

type lang = {
  name: string,
  @as("type") type_: string,
  version: float,
}

@module("./utils") external lang: lang = "default"
@module("./utils") external fib: int => int = "fib"
@module("./utils") external min: (int, int) => int = "min"
@module("./utils") external max: (int, int) => int = "min"

/* usage */
lang.name->Js.log
fib(10)->Js.log
min(1,2)->Js.log
max(2,3)->Js.log

Binding to default or named exported JS Class

Binding to default exported JS class

JavaScript
/* rectangle.js */
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  get area() {
    return this.calcArea();
  }

  calcArea() {
    return this.width * this.height;
  }
}
export default Rectangle;
Rescript Binding
type rect = {
  area: float,
}
@new @module("./rectangle") external createRect: (float, float) => rect = "default"
@send external calcArea: rect => float = "calcArea"

/* usage */
let r = createRect(1.1, 2.2)
r.area->Js.log
r->calcArea->Js.log

Binding to named exported JS class

JavaScript
/* rectangle.js */
class Rectangle {
  constructor(width, height) {
    this.width = width;
    this.height = height;
  }

  get area() {
    return this.calcArea();
  }

  calcArea() {
    return this.width * this.height;
  }
}
export { Rectangle };
Rescript Binding
type rect
@new @module("./rectangle")  external createRect: (float, float) => rect = "Rectangle"
@get external area: rect => float = "area"
@send external calcArea: rect => float = "calcArea"

/* usage */
let r = createRect(1.1, 2.2)
r->area->Js.log
r->calcArea->Js.log

Bind to JS Object

References

Bind using Rescript record

JavaScript
/* languages.js */
const rescript = {
  name: 'Rescript',
  type: 'FP',
  version: 10.2,
};

const ocaml = {
  name: 'OCaml',
  type: 'FP',
  version: 5.0,
};

const rust = {
  name: 'Rust',
  type: 'ML Family',
  version: 1.5,
};

export { rescript, ocaml, rust };
Rescript Binding
type lang = {
  name: string,
  @as("type") type_: string,
  version: float,
}
@module("./languages") external rescript: lang = "rescript"
@module("./languages") external ocaml: lang = "ocaml"
@module("./languages") external rust: lang = "rust"

/* usage */
rescript.name->Js.log
ocaml.name->Js.log
rust.name->Js.log

Bind JS array to Rescript record

JavaScript
/* languages.js */
const rescript = {
  name: 'Rescript',
  type: 'FP',
  version: 10.2,
};

const ocaml = {
  name: 'OCaml',
  type: 'FP',
  version: 5.0,
};

const rust = {
  name: 'Rust',
  type: 'ML Family',
  version: 1.5,
};

let langs = [ocaml, rust, rescript];

export { langs };

Rescript Binding
type lang = {
  name: string,
  @as("type") type_: string,
  version: float,
}

type langMap = {
  @as("0") ocaml: lang,
  @as("1") rust: lang,
  @as("2") rescript: lang,
}

@module("./languages") external langs: langMap = "langs"

/* usage */
langs.ocaml.name->Js.log
langs.rust.name->Js.log
langs.rescript.name->Js.log

Bind using Rescript object

JavaScript
/* languages.js */
const rescript = {
  name: 'Rescript',
  type: 'FP',
  version: 10.2,
};
export { rescript };
Rescript Binding
type langobj = {
  "name": string,
  "type": string,
  "version": float,
}
@module("./languages") external rescript: langobj = "rescript"

/* usage */
rescript["name"]->Js.log

Bind with getter/setter

Rescript Binding
@set external setValue: (Dom.element, string) => unit = "value"
@get external getValue: Dom.element => string = "value"

/* @get_index/@set_index to access dynamic property or an index */
type t
@new external create: array<float> => t = "Int32Array"
@get_index external get: (t, int) => float = ""
@set_index external set: (t, int, float) => unit = ""

/* usage */
let arr = create([1.,2.,3.])
arr->get(0)->Js.log
arr->set(0, 77.7)

Bind to JS Function

Labeled Arguments

JavaScript

/* play.js */
function drawCircle(x, y, color) {
  const s1 = `drawing circle at (${x},${y})`;
  return color ? `${s1} with ${color} color.` : s1;
}
export { drawCircle };

Rescript Binding

@module("./play") external drawCircle: 
(~x: float, ~y: float, ~color: string=?, unit) => string = "drawCircle"

/* usage */
drawCircle(~x=11.11, ~y=12.12, ())->Js.log
drawCircle(~x=11.11, ~y=12.12, ~color=?Some("Red"), ())->Js.log
drawCircle(~x=11.11, ~y=12.12, ~color="Blue", ())->Js.log

Object Method

JavaScript

/* play.js */
let funcObj = {
  id: (x) => x,
};

export { funcObj };

Rescript Binding

type fobj 
@module("./play") external funcObj : fobj = "funcObj"
@send external id : (fobj, int) => int = "id" 

/* usage */
funcObj-> id (25)->Js.log

Variadic Function

JavaScript

/* play.js */
function sum(...args) {
  return Array.from(args).reduce((x, y) => x + y);
}
export { sum };

Rescript Binding

@module("./play") @variadic external sumInts : array<int> => int = "sum"
@module("./play") @variadic external joinStrings : array<string> => string = "sum"

/* usage */
sumInts([1,2,3,4,5])->Js.log
joinStrings(["abc", "def", "xyz"])->Js.log

Polymorphic Variant (@unwrrap)

JavaScript

/* play.js */
function config(option) {
  if (typeof option === 'string') {
    return `set ${option}`;
  }
  if (typeof option === 'number') {
    return `set ${option}`;
  }
  if (typeof option === 'object') {
    return `set ${option.option}`;
  }
  if (typeof option === 'function') {
    return `set ${option()}`;
  }

  throw new Error('unexpected value');
}

export { config };

Rescript Binding

/* usage */
type optType = {option: string}
@module("./play")
external config: @unwrap
[
  | #Int(int)
  | #String(string)
  | #Object(optType)
  | #Function(unit => string)
  | #Null(unit) /* this is just for testing error catching */
] => string = "config"

/* usage */
config(#Int(10))->Js.log
config(#String("ten"))->Js.log
config(#Object({option: "obj option"}))->Js.log
config(#Function(() => "func option"))->Js.log
try {
  config(#Null())->Js.log
} catch {
| Js.Exn.Error(obj) =>
  switch Js.Exn.message(obj) {
  | Some(e) => Js.log(e)
  | None => ()
  }
}

Constrain Arguments

JavaScript

/* play.js */
function openWith(browserName) {
  if (browserName === 'chrome') {
    return 'opening with chrome ...';
  }
  if (browserName === 'safari') {
    return 'opening with safari ...';
  }
  if (browserName === 'firefox') {
    return 'opening with firefox ...';
  }
  if (browserName === 'edge') {
    return 'opening with edge ...';
  }
}

function queueWith(state) {
  if (state === 0) {
    return 'task is in the queue with low priority.';
  }
  if (state === 6) {
    return 'task is in the queue with high priority.';
  }
  if (state === 7) {
    return 'task is in the queue with top proirity.';
  }
  throw new Error('Unexpected state');
}

export { openWith, queueWith }

Rescript Binding

@module("./play") external openWith: (
    @string[
        | #chrome
        | #safari
        | #firefox
        | @as("edge") #IE
    ]
) => string = "openWith"

@module("./play") external queueWith:(
    @int[
        |#low
        |@as(6) #high
        |#top
    ]
) => string = "queueWith"

/* usage */
openWith(#chrome) -> Js.log
openWith(#safari) -> Js.log
openWith(#firefox) -> Js.log
openWith(#IE) -> Js.log

queueWith(#low)->Js.log
queueWith(#high)->Js.log
queueWith(#top)->Js.log

Special-case: Event Listeners

JavaScript

/* play.js */
class Game {
  #eventMap;
  #timeOutId;

  constructor() {
    this.#eventMap = new Map();
  }

  on(eventName, event) {
    this.#eventMap.set(eventName, event);
    return this;
  }

  play() {
    console.log('play...');
    clearTimeout(this.#timeOutId);
    this.#eventMap.get('play')?.();
    this.#timeOutId = setTimeout(
      () => this.#eventMap.get('completed')?.('You won!'),
      2000
    );
  }

  exit() {
    clearTimeout(this.#timeOutId);
    this.#eventMap.get('exit')?.('Bye');
  }
}

export { Game };

Rescript Binding

type game
@new @module("./play") external createGame: unit => game = "Game"
@send external play: game => unit = "play" 
@send external exit: game => unit = "exit" 
@send external on: (game, @string [
    |#play (unit => unit)
    |#completed (string => unit)
    |#exit (string => unit)
]  ) => game = "on"

/* usage */
let myGame = createGame()
myGame
    ->on(#play (() => Js.log("let's start...")))
    ->on(#completed (x => Js.log(x)))
    ->on(#exit (x => Js.log(x)))
    ->ignore
myGame->play
myGame->exit

Fixed Arguments

Special-case: Event Listeners

JavaScript

/* play.js */
class Game {
  #eventMap;
  #timeOutId;

  constructor() {
    this.#eventMap = new Map();
  }

  on(eventName, event) {
    this.#eventMap.set(eventName, event);
    return this;
  }

  play() {
    console.log('play...');
    clearTimeout(this.#timeOutId);
    this.#eventMap.get('play')?.();
    this.#timeOutId = setTimeout(
      () => this.#eventMap.get('completed')?.('You won!'),
      2000
    );
  }

  exit() {
    clearTimeout(this.#timeOutId);
    this.#eventMap.get('exit')?.('Bye');
  }
}

export { Game };

Rescript Binding

type game
@new @module("./play") external createGame: unit => game = "Game"
@send external gameOnPlay: (game, @as("play") _, unit => unit) => unit = "on"
@send external gameOnCompleted: (game, @as("completed") _, string => unit) => unit = "on"
@send external gameOnExit: (game, @as("exit") _, string => unit) => unit = "on"

/* usage */
let myGame = createGame()
myGame->gameOnPlay(()=>Js.log("lets start the game ..."))
myGame->gameOnCompleted(x=>Js.log(x))
myGame->gameOnExit(x=>Js.log(x))
myGame->play
myGame->exit

Ignore Arguments

From Docs

You can also explicitly "hide" external function parameters in the JS output, which may be useful if you want to add type constraints to other parameters without impacting the JS side:

Rescript

@val external doSomething: (@ignore 'a, 'a) => unit = "doSomething"

doSomething("this only shows up in ReScript code", "test")

JavaScript Output

doSomething("test");

Curry and Uncurry

uncurry syntax

type timerId
@val external setTimeOut: (((. unit) => unit), int) => timerId = "setTimeOut"

setTimeOut((.) => Js.log("hello"), 100)->Js.log

extra solution (@uncurry)

  • you're using external
  • the external function takes in an argument that's another function
  • you want the user not to need to annotate the call sites with the dot
@val external setTimeOut:(@uncurry (unit => unit), int) => timerId = "setTimeOut"

@send external keep: (array<int>, @uncurry (int => bool)) => array<int> = "filter"
[1,2,3]->keep(x => mod(x,2) === 0)->Js.log

Modeling this-based callbacks

JavaScript

x.onload = function(v) {
  console.log(this.response + v)
}

Rescript Binding

type x
@val external x: x = "x"
@set external setOnload: (x, @this ((x, int) => unit)) => unit = "onload"
@get external resp: x => int = "response"
setOnload(x, @this ((o, v) => Js.log(resp(o) + v)))

Bind null or undefined return value to Option

JavaScript

function getValue(key) {
  if (key) {
    return 'value';
  } else {
    return undefined;
  }
}
export { getValue };

Rescript

@module("./play") @return(nullable) external getValue: string => option<string> = "getValue"

/* usage */
let v = getValue("")
switch v {
  | Some(v') => Js.log(v')
  | None => Js.log("undefined")
}

type doc
type elm
@val external document: doc = "document"
@send @return(nullable) external getElementById: (doc, string) => option<elm> = "getElementById"

/* usage */
let element = getElementById(document, "lesson")
switch element {
    |Some(e) => Js.log(e)
    |None => Js.log("Not Found")
}

JSON

Generate Converters & Helpers

Use @deriving to

  • Automatically generate functions that convert between ReScript's internal and JS runtime values (e.g. variants).
  • Convert a record type into an abstract type with generated creation, accessor and method functions.
  • Generate some other helper functions, such as functions from record attribute names.

@deriving(accessors)

@deriving(accessors) with variants

  • create accessor functions for variant constructors
  • payload-less constructors generate plain integers
@deriving(accessors)
type rec json =
  | Bool(bool)
  | Float(float)
  | Int(int)
  | String(string)
  | Null
  | Undefined

/* usage */
let b : json = bool(true)
b->Js.log // {TAG: 0, _0: true}
let f : json = float(1.1)
f->Js.log // {TAG: 1, _0: 1.1}
let i : json = int(10)
i->Js.log // {TAG: 2, _0: 10}
let s : json = string("hello")
s->Js.log // {TAG: 3, _0: 'hello'}
null->Js.log // 0 
undefined->Js.log // 1 

@deriving(accessors) with records

  • create accessors with field names
@deriving(accessors)
type lang = {
  name: string,
  \"type": string,
  version: float,
}

let langs = [
    { name: "ReScript", \"type" : "fp", version : 10.2},
    { name: "OCaml", \"type" : "fp", version : 5.0}
]

/* usage */
Belt.Array.map(langs, name) -> Js.log // ['ReScript', 'OCaml']
Belt.Array.map(langs, \"type") -> Js.log // ['fp', 'fp']
Belt.Array.map(langs, version) -> Js.log // [10.2, 5.0]

@deriving(jsConverter)

  • create converter functins the allow back and forth conversion between JS integer enum and ReScript variant values
@deriving(jsConverter)
type letter =
  | @as(1) A
  | B
  | @as(4) E
  | F

letterToJs(A)->Js.log // 1
letterToJs(B)->Js.log // 2
letterToJs(E)->Js.log // 4
letterToJs(F)->Js.log // 5

// print A 
switch letterFromJs(1) {
    |Some(A)  => Js.log("A")
    |_ => ()
}

// print None
switch letterFromJs(3) {
    |None => Js.log("None") 
    | _ => ()
}

@deriving({jsConverter: newType})

  • create converter functins the allow back and forth conversion between generated abstract type and ReScript variant values
  • newType hides the fact that JS enum are ints
@deriving({jsConverter: newType})
type letter =
  | @as(1) A
  | B
  | @as(4) E
  | F

/* usage */

letterToJs(A)->Js.log // 1
letterToJs(B)->Js.log // 2
letterToJs(E)->Js.log // 4
letterToJs(F)->Js.log // 5

let f = letterFromJs(letterToJs(F))
f->Js.log // print 3 here, that's OK we don't care the value as long as we can check the type
(f === F)->Js.log // true

@obj

Use @obj on an external binding to create a function that, when called, will evaluate to a JS object with fields corresponding to the function's parameter labels.

@obj
external route: (
  ~\"type": string,
  ~path: string,
  ~action: list<string> => unit,
  ~options: {..}=?,
  unit,
) => _ = ""

/* usage */

let homeRoute = route(~\"type"="GET", ~path="/", ~action=_ => Js.log("Home"), ())
homeRoute->Js.log // {type: 'GET', path: '/', action: ƒ}

@deriving(abstract)

Go and read here 😄

ReScriptReact (V3)

This learning notes is based on ReScriptReact V3.

React.res

type element

@val external null: element = "null"

external float: float => element = "%identity"
external int: int => element = "%identity"
external string: string => element = "%identity"

external array: array<element> => element = "%identity"

type componentLike<'props, 'return> = 'props => 'return

type component<'props> = componentLike<'props, element>

/* this function exists to prepare for making `component` abstract */
external component: componentLike<'props, element> => component<'props> = "%identity"

@module("react")
external createElement: (component<'props>, 'props) => element = "createElement"

@module("react")
external cloneElement: (element, 'props) => element = "cloneElement"

@variadic @module("react")
external createElementVariadic: (component<'props>, 'props, array<element>) => element =
  "createElement"

@module("react") @deprecated("Please use JSX syntax directly.")
external jsxKeyed: (component<'props>, 'props, string) => element = "jsx"

@module("react") @deprecated("Please use JSX syntax directly.")
external jsx: (component<'props>, 'props) => element = "jsx"

@module("react") @deprecated("Please use JSX syntax directly.")
external jsxs: (component<'props>, 'props) => element = "jsxs"

@module("react") @deprecated("Please use JSX syntax directly.")
external jsxsKeyed: (component<'props>, 'props, string) => element = "jsxs"

type ref<'value> = {mutable current: 'value}

module Ref = {
  @deprecated("Please use the type React.ref instead")
  type t<'value> = ref<'value>

  @deprecated("Please directly read from ref.current instead") @get
  external current: ref<'value> => 'value = "current"

  @deprecated("Please directly assign to ref.current instead") @set
  external setCurrent: (ref<'value>, 'value) => unit = "current"
}

@module("react")
external createRef: unit => ref<Js.nullable<'a>> = "createRef"

module Children = {
  @module("react") @scope("Children")
  external map: (element, element => element) => element = "map"
  @module("react") @scope("Children")
  external mapWithIndex: (element, @uncurry (element, int) => element) => element = "map"
  @module("react") @scope("Children")
  external forEach: (element, element => unit) => unit = "forEach"
  @module("react") @scope("Children")
  external forEachWithIndex: (element, @uncurry (element, int) => unit) => unit = "forEach"
  @module("react") @scope("Children")
  external count: element => int = "count"
  @module("react") @scope("Children")
  external only: element => element = "only"
  @module("react") @scope("Children")
  external toArray: element => array<element> = "toArray"
}

module Context = {
  type t<'props>

  @obj
  external makeProps: (
    ~value: 'props,
    ~children: element,
    unit,
  ) => {"value": 'props, "children": element} = ""

  @get
  external provider: t<'props> => component<{"value": 'props, "children": element}> = "Provider"
}

@module("react")
external createContext: 'a => Context.t<'a> = "createContext"

@module("react")
external forwardRef: (@uncurry ('props, Js.Nullable.t<ref<'a>>) => element) => component<
  'props,
> = "forwardRef"

@module("react")
external memo: component<'props> => component<'props> = "memo"

@module("react")
external memoCustomCompareProps: (
  component<'props>,
  @uncurry ('props, 'props) => bool,
) => component<'props> = "memo"

module Fragment = {
  @obj
  external makeProps: (~children: element, ~key: 'key=?, unit) => {"children": element} = ""
  @module("react")
  external make: component<{
    "children": element,
  }> = "Fragment"
}

module StrictMode = {
  @obj
  external makeProps: (~children: element, ~key: 'key=?, unit) => {"children": element} = ""
  @module("react")
  external make: component<{
    "children": element,
  }> = "StrictMode"
}

module Suspense = {
  @obj
  external makeProps: (
    ~children: element=?,
    ~fallback: element=?,
    ~key: 'key=?,
    unit,
  ) => {"children": option<element>, "fallback": option<element>} = ""
  @module("react")
  external make: component<{
    "children": option<element>,
    "fallback": option<element>,
  }> = "Suspense"
}

module Experimental = {
  module SuspenseList = {
    type revealOrder
    type tail
    @obj
    external makeProps: (
      ~children: element=?,
      ~revealOrder: [#forwards | #backwards | #together]=?,
      ~tail: [#collapsed | #hidden]=?,
      unit,
    ) => {"children": option<element>, "revealOrder": option<revealOrder>, "tail": option<tail>} =
      ""

    @module("react")
    external make: component<{
      "children": option<element>,
      "revealOrder": option<revealOrder>,
      "tail": option<tail>,
    }> = "SuspenseList"
  }
}

/* HOOKS */

/*
 * Yeah, we know this api isn't great. tl;dr: useReducer instead.
 * It's because useState can take functions or non-function values and treats
 * them differently. Lazy initializer + callback which returns state is the
 * only way to safely have any type of state and be able to update it correctly.
 */
@module("react")
external useState: (@uncurry (unit => 'state)) => ('state, ('state => 'state) => unit) =
  "useState"

@module("react")
external useReducer: (
  @uncurry ('state, 'action) => 'state,
  'state,
) => ('state, 'action => unit) = "useReducer"

@module("react")
external useReducerWithMapState: (
  @uncurry ('state, 'action) => 'state,
  'initialState,
  @uncurry ('initialState => 'state),
) => ('state, 'action => unit) = "useReducer"

@module("react")
external useEffect: (@uncurry (unit => option<unit => unit>)) => unit = "useEffect"
@module("react")
external useEffect0: (@uncurry (unit => option<unit => unit>), @as(json`[]`) _) => unit =
  "useEffect"
@module("react")
external useEffect1: (@uncurry (unit => option<unit => unit>), array<'a>) => unit = "useEffect"
@module("react")
external useEffect2: (@uncurry (unit => option<unit => unit>), ('a, 'b)) => unit = "useEffect"
@module("react")
external useEffect3: (@uncurry (unit => option<unit => unit>), ('a, 'b, 'c)) => unit =
  "useEffect"
@module("react")
external useEffect4: (@uncurry (unit => option<unit => unit>), ('a, 'b, 'c, 'd)) => unit =
  "useEffect"
@module("react")
external useEffect5: (@uncurry (unit => option<unit => unit>), ('a, 'b, 'c, 'd, 'e)) => unit =
  "useEffect"
@module("react")
external useEffect6: (
  @uncurry (unit => option<unit => unit>),
  ('a, 'b, 'c, 'd, 'e, 'f),
) => unit = "useEffect"
@module("react")
external useEffect7: (
  @uncurry (unit => option<unit => unit>),
  ('a, 'b, 'c, 'd, 'e, 'f, 'g),
) => unit = "useEffect"

@module("react")
external useLayoutEffect: (@uncurry (unit => option<unit => unit>)) => unit = "useLayoutEffect"
@module("react")
external useLayoutEffect0: (
  @uncurry (unit => option<unit => unit>),
  @as(json`[]`) _,
) => unit = "useLayoutEffect"
@module("react")
external useLayoutEffect1: (@uncurry (unit => option<unit => unit>), array<'a>) => unit =
  "useLayoutEffect"
@module("react")
external useLayoutEffect2: (@uncurry (unit => option<unit => unit>), ('a, 'b)) => unit =
  "useLayoutEffect"
@module("react")
external useLayoutEffect3: (@uncurry (unit => option<unit => unit>), ('a, 'b, 'c)) => unit =
  "useLayoutEffect"
@module("react")
external useLayoutEffect4: (@uncurry (unit => option<unit => unit>), ('a, 'b, 'c, 'd)) => unit =
  "useLayoutEffect"
@module("react")
external useLayoutEffect5: (
  @uncurry (unit => option<unit => unit>),
  ('a, 'b, 'c, 'd, 'e),
) => unit = "useLayoutEffect"
@module("react")
external useLayoutEffect6: (
  @uncurry (unit => option<unit => unit>),
  ('a, 'b, 'c, 'd, 'e, 'f),
) => unit = "useLayoutEffect"
@module("react")
external useLayoutEffect7: (
  @uncurry (unit => option<unit => unit>),
  ('a, 'b, 'c, 'd, 'e, 'f, 'g),
) => unit = "useLayoutEffect"

@module("react")
external useMemo: (@uncurry (unit => 'any)) => 'any = "useMemo"

@module("react")
external useMemo0: (@uncurry (unit => 'any), @as(json`[]`) _) => 'any = "useMemo"

@module("react")
external useMemo1: (@uncurry (unit => 'any), array<'a>) => 'any = "useMemo"

@module("react")
external useMemo2: (@uncurry (unit => 'any), ('a, 'b)) => 'any = "useMemo"

@module("react")
external useMemo3: (@uncurry (unit => 'any), ('a, 'b, 'c)) => 'any = "useMemo"

@module("react")
external useMemo4: (@uncurry (unit => 'any), ('a, 'b, 'c, 'd)) => 'any = "useMemo"

@module("react")
external useMemo5: (@uncurry (unit => 'any), ('a, 'b, 'c, 'd, 'e)) => 'any = "useMemo"

@module("react")
external useMemo6: (@uncurry (unit => 'any), ('a, 'b, 'c, 'd, 'e, 'f)) => 'any = "useMemo"

@module("react")
external useMemo7: (@uncurry (unit => 'any), ('a, 'b, 'c, 'd, 'e, 'f, 'g)) => 'any = "useMemo"

/* This is used as return values */
type callback<'input, 'output> = 'input => 'output

@module("react")
external useCallback: (@uncurry ('input => 'output)) => callback<'input, 'output> = "useCallback"

@module("react")
external useCallback0: (
  @uncurry ('input => 'output),
  @as(json`[]`) _,
) => callback<'input, 'output> = "useCallback"

@module("react")
external useCallback1: (@uncurry ('input => 'output), array<'a>) => callback<'input, 'output> =
  "useCallback"

@module("react")
external useCallback2: (@uncurry ('input => 'output), ('a, 'b)) => callback<'input, 'output> =
  "useCallback"

@module("react")
external useCallback3: (
  @uncurry ('input => 'output),
  ('a, 'b, 'c),
) => callback<'input, 'output> = "useCallback"

@module("react")
external useCallback4: (
  @uncurry ('input => 'output),
  ('a, 'b, 'c, 'd),
) => callback<'input, 'output> = "useCallback"

@module("react")
external useCallback5: (
  @uncurry ('input => 'output),
  ('a, 'b, 'c, 'd, 'e),
) => callback<'input, 'output> = "useCallback"

@module("react")
external useCallback6: (
  @uncurry ('input => 'output),
  ('a, 'b, 'c, 'd, 'e, 'f),
) => callback<'input, 'output> = "useCallback"

@module("react")
external useCallback7: (
  @uncurry ('input => 'output),
  ('a, 'b, 'c, 'd, 'e, 'f, 'g),
) => callback<'input, 'output> = "useCallback"

@module("react")
external useContext: Context.t<'any> => 'any = "useContext"

@module("react") external useRef: 'value => ref<'value> = "useRef"

@module("react")
external useImperativeHandle0: (
  Js.Nullable.t<ref<'value>>,
  @uncurry (unit => 'value),
  @as(json`[]`) _,
) => unit = "useImperativeHandle"

@module("react")
external useImperativeHandle1: (
  Js.Nullable.t<ref<'value>>,
  @uncurry (unit => 'value),
  array<'a>,
) => unit = "useImperativeHandle"

@module("react")
external useImperativeHandle2: (
  Js.Nullable.t<ref<'value>>,
  @uncurry (unit => 'value),
  ('a, 'b),
) => unit = "useImperativeHandle"

@module("react")
external useImperativeHandle3: (
  Js.Nullable.t<ref<'value>>,
  @uncurry (unit => 'value),
  ('a, 'b, 'c),
) => unit = "useImperativeHandle"

@module("react")
external useImperativeHandle4: (
  Js.Nullable.t<ref<'value>>,
  @uncurry (unit => 'value),
  ('a, 'b, 'c, 'd),
) => unit = "useImperativeHandle"

@module("react")
external useImperativeHandle5: (
  Js.Nullable.t<ref<'value>>,
  @uncurry (unit => 'value),
  ('a, 'b, 'c, 'd, 'e),
) => unit = "useImperativeHandle"

@module("react")
external useImperativeHandle6: (
  Js.Nullable.t<ref<'value>>,
  @uncurry (unit => 'value),
  ('a, 'b, 'c, 'd, 'e, 'f),
) => unit = "useImperativeHandle"

@module("react")
external useImperativeHandle7: (
  Js.Nullable.t<ref<'value>>,
  @uncurry (unit => 'value),
  ('a, 'b, 'c, 'd, 'e, 'f, 'g),
) => unit = "useImperativeHandle"

module Uncurried = {
  @module("react")
  external useState: (@uncurry (unit => 'state)) => ('state, (. 'state => 'state) => unit) =
    "useState"

  @module("react")
  external useReducer: (
    @uncurry ('state, 'action) => 'state,
    'state,
  ) => ('state, (. 'action) => unit) = "useReducer"

  @module("react")
  external useReducerWithMapState: (
    @uncurry ('state, 'action) => 'state,
    'initialState,
    @uncurry ('initialState => 'state),
  ) => ('state, (. 'action) => unit) = "useReducer"

  type callback<'input, 'output> = (. 'input) => 'output

  @module("react")
  external useCallback: (@uncurry ('input => 'output)) => callback<'input, 'output> =
    "useCallback"

  @module("react")
  external useCallback0: (
    @uncurry ('input => 'output),
    @as(json`[]`) _,
  ) => callback<'input, 'output> = "useCallback"

  @module("react")
  external useCallback1: (@uncurry ('input => 'output), array<'a>) => callback<'input, 'output> =
    "useCallback"

  @module("react")
  external useCallback2: (@uncurry ('input => 'output), ('a, 'b)) => callback<'input, 'output> =
    "useCallback"

  @module("react")
  external useCallback3: (
    @uncurry ('input => 'output),
    ('a, 'b, 'c),
  ) => callback<'input, 'output> = "useCallback"

  @module("react")
  external useCallback4: (
    @uncurry ('input => 'output),
    ('a, 'b, 'c, 'd),
  ) => callback<'input, 'output> = "useCallback"

  @module("react")
  external useCallback5: (
    @uncurry ('input => 'output),
    ('a, 'b, 'c, 'd, 'e),
  ) => callback<'input, 'output> = "useCallback"

  @module("react")
  external useCallback6: (
    @uncurry ('input => 'output),
    ('a, 'b, 'c, 'd, 'e, 'f),
  ) => callback<'input, 'output> = "useCallback"

  @module("react")
  external useCallback7: (
    @uncurry ('input => 'output),
    ('a, 'b, 'c, 'd, 'e, 'f, 'g),
  ) => callback<'input, 'output> = "useCallback"
}

type transitionConfig = {timeoutMs: int}

@module("react")
external useTransition: (
  ~config: transitionConfig=?,
  unit,
) => (callback<callback<unit, unit>, unit>, bool) = "useTransition"

@set
external setDisplayName: (component<'props>, string) => unit = "displayName"

@get @return(nullable)
external displayName: component<'props> => option<string> = "displayName"

Basic Elements

Creating elements from string, int, float, array & Null element React.null

let {string, int, float, array} = module(React)
let strElement = <p key="hello"> {"Hello"->string} </p>
let intElement = <p key="t5"> {25->int} </p>
let floatElement = <p key="pi"> {3.14->float} </p>
let elements = [strElement, intElement, floatElement]

let isTired = false

let basicElement = <div> {"I'm a basic element. 😁"->string} </div>

@react.component
let make = () => {
  <div>
    <div> {elements->array} </div>
    {switch isTired {
    | true => <p> {"Omg!"->string} </p>
    | false => React.null
    }}
    basicElement
  </div>
}

Component

A component is ('props) => React.element or React.component<'props>

Component definition in RescriptReact

type componentLike<'props, 'return> = 'props => 'return
type component<'props> = componentLike<'props, element>

JSX component interface

  • A component is a module with:
    • make : props => React.element
    • makeProps: 'a => props
module Movie = {
  type props = {"title": string, "desc": string}
  
  @obj
  external makeProps: (~title: string, ~desc: string, ~children: React.element=?, unit) => props 
  = ""
  
  let make = (props: props) => {
    <div>
      <h1> {props["title"]->React.string} </h1>
      <p> {props["desc"]->React.string} </p>
    </div>
  }
}

@react.component
let make = () => {
  <div>
    <Movie title="Dark" desc="The best sifi movie." />
  </div>
}

@react.component Decorator

  • @react.component decorator helps to create component more easily
module Movie = {
  @react.component
  let make = (~title: string, ~desc: string) => {
    <div>
      <h1> {title->React.string} </h1>
      <p> {desc->React.string} </p>
    </div>
  }
}

@react.component
let make = () => {
  <div>
    <Movie title="Looper" desc="The best sifi movie." />
  </div>
}

No need to write let make = (~title: string, ~desc: string, unit) cause @react.component helps to add it.

Low level APIs to create components

The following APIs are used by JSX itself to create elements.

React.createElement: (React.component<'props>, 'props) => React.elements

React.createElementVariadic: 
(React.component<'props>, 'props, array<React.element>) => React.element

React.cloneElement: (React.element, 'props) => React.element

React.createElement

module Movie = {
  @react.component
  let make = (~title: string, ~desc: string) => {
    <div>
      <h1> {title->React.string} </h1>
      <p> {desc->React.string} </p>
    </div>
  }
}

@react.component
let make = () => {
  <div>
    {React.createElement(Movie.make, {"title": "Lopper", "desc": "One of the best sifi movies."})}
  </div>
}

React.createElementVariadic

module Stack = {
    @react.component
    let make = (~width: option<string>=?, ~height: option<string>=?, ~children: option<React.element>=?) => {
        let ms = ReactDOM.Style.make
        let style = switch (width, height) {
            |(Some(w), Some(h)) => ms(~width=w, ~height=h, ())
            |(Some(w), None) => ms(~width=w, ~height="auto", ())
            |(None, Some(h)) => ms(~width="auto",~height=h, ())
            |(None, None) => ms(~width="auto", ~height="auto", ())
        }
       <div className="flex flex-col flex-wrap" style>
       { switch children { |Some(c) => c |None => React.null } }
       </div> 
    }
}

module Movie = {
  @react.component
  let make = (~title: string, ~desc: string) => {
    <div>
      <h1> {title->React.string} </h1>
      <p> {desc->React.string} </p>
    </div>
  }
}

@react.component
let make = () => {
  <div>
    {React.createElementVariadic(Stack.make, {"width": Some("100%"), "height": Some("50%") , "children": None}, [
        <div>{"My List"->React.string}</div>,
        <Stack width="50%" height="50%">
            <div>{"Scifi Movies"->React.string}</div> 
            <Movie title="Dark" desc="The best scifi series I've ever watched"/>
        </Stack> 
    ])}
  </div>
}

React.cloneElement

Useful to add additional attributes to the original element - like data- attributes

let hello = <div className="text-slate-900"> {"Hello"->React.string} </div>
@react.component
let make = () => {
  <div>
    {React.cloneElement(hello, 
     {"className": "text-red-900 text-xl", "data-theme": "red"})}
  </div>
}

generated <div class="text-red-900 text-xl" data-theme="red">Hello</div>

Misc

Optional Props

// Greeting.res
@react.component
let make = (~name: option<string>=?) => {
  let greeting = switch name {
    | Some(name) => "Hello " ++ name ++ "!"
    | None => "Hello stranger!"
  }
  <div> {React.string(greeting)} </div>
}

let name = Some("Andrea")

<Greeting ?name /> // <== just ?name

Special Props key and ref

  • key and ref are special props, cannot define them like ~key or ~ref

Invalid Prop Names

  • Reserved keywords like type should be used like type_
  • aria-* should be like ariaLabel
  • data- attributes can be added using React.cloneElement

Children

children is treated as a React.Element.

Component that must have children

module Comp = {
 @react.component
 let make = (~children: React.element) => { ...... }
}

Component that doesn't have children

module Comp = {
 @react.component
 let make = () => { ...... }
}

Component with optional children

module Comp = {
 @react.component
 let make = (~children: option<React.element>=?) => { ...... }
}

Component Naming

// File.res

// will be named `File` in dev tools
@react.component
let make = ...

// will be named `File$component` in dev tools
@react.component
let component = ...

module Nested = {
  // will be named `File$Nested` in dev tools
  @react.component
  let make = ...
};

Arrays and Keys

Keys help React identify which elements have been changed, added, or removed throughout each render.

type movie = {title: string, desc: string}
let movies = [{title: "Dark", desc: "scifi"}, {title: "Looper", desc: "scifi"}]

module Movie = {
  @react.component
  let make = (~title: string, ~desc: string) => {
    <div>
      <h1> {title->React.string} </h1>
      <p> {desc->React.string} </p>
    </div>
  }
}

@react.component
let make = () => {
  <div>
    {
        movies
        ->Belt.Array.map(({title, desc}) => <Movie key={title} title desc />)
        ->React.array
    }
  </div>
}

Refs and the DOM

React.ref: type t<'value> = { mutable current: 'value }

When to use Refs:

  • Managing state that should not trigger any re-render.
  • Managing focus, text selection, or media playback.
  • Triggering imperative animations.
  • Integrating with third-party DOM libraries.

Managing state that should not trigger an re-render

  • clicking will not update click_count span althought it updates document title.
@val external document: Dom.element = "document"
@set external setTitle: (Dom.element, string) => unit = "title"

@react.component
let make = () => {
  let clicks = React.useRef(0)
  let onClick = _ => {
      clicks.current = clicks.current + 1
      setTitle(document, Js.Int.toString(clicks.current))          
  }
  <div>
    <button className="flex flex-col" type_="button" onClick>
      <span name="click_count"> {clicks.current->React.int} </span>
      <span>{"click me"->React.string}</span>
    </button>
  </div>
}

Adding Ref to DOM Element

@send external select: Dom.element => unit = "select"
@send external focus: Dom.element => unit = "focus"

@react.component
let make = () => {
  let inputRef = React.useRef(Js.Nullable.null)
  
  let onClick = _ => {
    switch inputRef.current->Js.Nullable.toOption {
    | Some(e) => select(e)
    | None => ()
    }
  }
  
  let focus = _ => {
    switch inputRef.current->Js.Nullable.toOption {
    | Some(e) => focus(e)
    | None => ()
    }
  }
  
  <div className="flex flex-col p-4 gap-2">
    <input type_="text" ref={ReactDOM.Ref.domRef(inputRef)} className="w-full border-400" />
    <button type_="button" className="p-1 bg-200" onClick={focus}> 
        {"Focus"->React.string} 
    </button>
    <button type_="button" className="p-1 bg-200" onClick>
        {"Select Text"->React.string}
    </button>
  </div>
}

Manage a list of Refs using a ref callback

  • React.Ref.callbackDomRef()
@send external scrollIntoView: (Dom.element, {..}) => unit = "scrollIntoView"

let getCatList = () => {
  let cats = []
  for i in 0 to 10 {
    cats->Belt.Array.push({
      "id": i,
      "url": "https://placekitten.com/250/200?image=" ++ Js.Int.toString(i),
    })
  }
  cats
}

module CatList = {
  let cats = getCatList()
  @react.component
  let make = () => {
    let itemsRef = React.useRef(Belt.MutableMap.Int.make())
    let scrollToId = id => {
      let m = itemsRef.current
      switch Belt.MutableMap.Int.get(m, id) {
      | Some(e) =>
        scrollIntoView(
          e,
          {
            "behavior": "smooth",
            "block": "nearest",
            "inline": "center",
          },
        )
      | None => ()
      }
    }
    <>
      <div className="flex flex-col p-4 items-start justify-center">
        <nav className="flex gap-2 pb-2">
          <button type_="button" className="p-2 bg-slate-200" onClick={_ => scrollToId(1)}>
            {"Tome"->React.string}
          </button>
          <button type_="button" className="p-2 bg-slate-200" onClick={_ => scrollToId(5)}>
            {"Remy"->React.string}
          </button>
          <button type_="button" className="p-2 bg-slate-200" onClick={_ => scrollToId(9)}>
            {"Jack"->React.string}
          </button>
        </nav>
      </div>
      <div className="flex p-4 overflow-x-auto">
        <ul className="flex gap-2">
          {cats
          ->Belt.Array.map(cat => {
            <li
              className="flex-none"
              ref={ReactDOM.Ref.callbackDomRef(element => {
                let m = itemsRef.current
                switch element->Js.Nullable.toOption {
                | Some(e) => Belt.MutableMap.Int.set(m, cat["id"], e)
                | None => Belt.MutableMap.Int.remove(m, cat["id"])
                }
              })}>
              <img src={cat["url"]} className="w-[20rem] h-[16rem]" />
            </li>
          })
          ->React.array}
        </ul>
      </div>
    </>
  }
}

@react.component
let make = () => {
  <CatList />
}

Forwarding Refs

@send external select: Dom.element => unit = "select"
@send external focus: Dom.element => unit = "focus"

module CoolInput = {
  @react.component
  let make = (~inputRef: ReactDOM.domRef) => {
    <div className="w-full border-[1px] border-300 rounded bg-100 p-2 text-lg">
      <input type_="text" ref={inputRef} className="w-full border-0 bg-50" />
    </div>
  }
}

@react.component
let make = () => {
  let inputRef = React.useRef(Js.Nullable.null)

  let onClick = _ => {
    switch inputRef.current->Js.Nullable.toOption {
    | Some(e) => select(e)
    | None => ()
    }
  }

  let focus = _ => {
    switch inputRef.current->Js.Nullable.toOption {
    | Some(e) => focus(e)
    | None => ()
    }
  }

  <div className="flex flex-col p-4 gap-2">
    <CoolInput inputRef={ReactDOM.Ref.domRef(inputRef)} />
    <button type_="button" className="p-1 bg-200" onClick={focus}> {"Focus"->React.string} </button>
    <button type_="button" className="p-1 bg-200" onClick> {"Select Text"->React.string} </button>
  </div>
}
module FancyInput = {
  @react.component
  let make = React.forwardRef((~className=?, ~children, ref_) =>
    <div>
      <input
        type_="text"
        ?className
        ref=?{Js.Nullable.toOption(ref_)->Belt.Option.map(
          ReactDOM.Ref.domRef,
        )}
      />
      children
    </div>
  )
}

@send external focus: Dom.element => unit = "focus"

@react.component
let make = () => {
  let input = React.useRef(Js.Nullable.null)

  let focusInput = () =>
    input.current
    ->Js.Nullable.toOption
    ->Belt.Option.forEach(input => input->focus)

  let onClick = _ => focusInput()

  <div>
    <FancyInput className="fancy" ref=input>
      <button onClick> {React.string("Click to focus")} </button>
    </FancyInput>
  </div>
}

Context

  • Context provides a way to share values between components in a component tree
  • Drawback: it renders all the components under the provider each time the context state changes
    • Should use React.memo for components within the provider
module FunContext = {
  let context = React.createContext("")

  module Provider = {
    let provider = React.Context.provider(context)
    @react.component
    let make = (~value, ~children) => {
      React.createElement(provider, {"value": value, "children": children})
    }
  }
}

module Component1 = {
  @react.component
  let make = () => {
    let fun = React.useContext(FunContext.context)
    <div className="flex flex-col items-center justify-center">
      <div> {Js.Date.now()->React.float} </div>
      {React.string(fun)}
    </div>
  }
  let make = React.memo(make) // Foo won't render when provider state changes
}
module Component2 = {
  @react.component
  let make = () => {
    <div className="flex flex-col items-center justify-center">
      <div> {Js.Date.now()->React.float} </div>
      <Component1 />
    </div>
  }
  let make = React.memo(make) // Foo won't render when provider state changes
}
module Component3 = {
  @react.component
  let make = () => {
    <div className="flex flex-col items-center justify-center">
      <div> {Js.Date.now()->React.float} </div>
      <Component2 />
    </div>
  }
  let make = React.memo(make) // Component 3 will be rerendered cause of the state changes
}

module Foo = {
  @react.component
  let make = () => {
    <div className="flex items-center justify-center">
      <span> {"Foo "->React.string} </span>
      <div> {Js.Date.now()->React.float} </div>
    </div>
  }
  let make = React.memo(make) // Foo won't render when provider state changes
}

@react.component
let make = () => {
  let (fun, setFun) = React.useState(_ => "fun")
  <FunContext.Provider value={fun}>
    <div className="flex flex-col items-center justify-center">
      <Component3 />
      <button type_="button" className="bg-300 p-2" onClick={_ => setFun(prev => prev ++ prev)}>
        {"Have Fun"->React.string}
      </button>
    </div>
    <Foo />
  </FunContext.Provider>
}

Styling

Inline style


let style1 = ReactDOM.Style.make(
    ~backgroundColor="blue",
    ~fontSize="2rem",
    ~color=?Some("yellow"),
    ~padding="0.5rem 1rem",
    (),
)

let roundedStyle = ReactDOM.Style.make(~borderRadius="50px 50px", ())

let combined = ReactDOM.Style.combine(style1, roundedStyle)

let unsafeStyle =
ReactDOM.Style.make(~color="red", ~padding="10px", ())
 ->ReactDOM.Style.unsafeAddProp(
  "-webkit-animation-name",
  "moveit")

Global CSS

// in a CommonJS setup
%%raw("require('./styles/main.css')")

// or with ES6
%%raw("import './styles/main.css'")

CSS Modules

// {..} means we are handling a JS object with an unknown
// set of attributes
@module external styles: {..} = "./styles.module.css"

// Use the obj["key"] syntax to access any classname within our object
let app = <div className={styles["root"]} />

Resources