NexusCS

ReasonML

Languages
ReasonML is a syntax extension for OCaml with first-class React support and JavaScript interop. This guide covers syntax, types, patterns, and interop.
ocaml
functional
react

Getting started

Introduction

ReasonML is a functional programming language with static types, pattern matching, and seamless JavaScript/React integration through Melange or ReScript.

Installation

# Using ReScript
npm install rescript

# Using Melange
opam install melange

Hello World

/* Basic printing */
Js.log("Hello, World!");

/* With string concatenation */
let name = "ReasonML";
Js.log("Hello, " ++ name ++ "!");

Compiling

# ReScript
npx rescript build

# Melange
dune build

Variables & Bindings

Let Bindings

/* Immutable by default */
let x = 42;
let message = "Hello";

/* Shadowing (not mutation) */
let x = 10;
let x = x + 5; /* x is now 15 */

/* Block scope */
{
  let inner = "scoped";
  Js.log(inner);
};
/* inner not accessible here */

Comparison with JavaScript

/* ReasonML */
let x = 10;
let x = 20; /* Creates new binding */
/* JavaScript */
const x = 10;
const x = 20; // Error!

let y = 10;
y = 20; // Mutation

Mutable References

/* Use ref for mutation */
let counter = ref(0);

/* Read with ^ */
Js.log(counter^); /* 0 */

/* Update with := */
counter := counter^ + 1;
Js.log(counter^); /* 1 */

Type System

Type Inference

/* Types are inferred */
let x = 42; /* int */
let name = "Alice"; /* string */
let isValid = true; /* bool */

/* Type annotations (optional) */
let age: int = 30;
let greet: string => string =
  (name) => "Hello, " ++ name;

Primitive Types

Type Example JavaScript
int 42 number
float 3.14 number
bool true, false boolean
string "hello" string
char 'a' N/A
unit () undefined

Type Parameters

/* Generic/polymorphic types */
let identity: 'a => 'a = (x) => x;

let first: ('a, 'b) => 'a =
  (x, _y) => x;

/* Type parameter names */
type result('value, 'error) =
  | Ok('value)
  | Error('error);

Type Aliases

/* Simple alias */
type userId = int;
type name = string;

/* With parameters */
type point('a) = ('a, 'a);
type coords = point(float);

/* Function types */
type mapper('a, 'b) = 'a => 'b;
type predicate('a) = 'a => bool;

Functions

Basic Syntax

/* Anonymous function */
let add = (x, y) => x + y;

/* With type annotation */
let multiply: (int, int) => int =
  (x, y) => x * y;

/* Multiple lines */
let greet = (name) => {
  let message = "Hello, " ++ name;
  Js.log(message);
  message; /* Return value */
};

Named Arguments

/* Define with ~ */
let greetPerson = (~name, ~age) => {
  "Hello " ++ name ++
  ", you are " ++ string_of_int(age)
};

/* Call with labels */
greetPerson(~name="Alice", ~age=30);
greetPerson(~age=25, ~name="Bob");

Optional Arguments

/* With default value */
let greet = (~name, ~greeting="Hello") =>
  greeting ++ ", " ++ name;

greet(~name="Alice");
/* "Hello, Alice" */

greet(~name="Bob", ~greeting="Hi");
/* "Hi, Bob" */

Partial Application

/* Curried functions */
let add = (x, y) => x + y;
let addFive = add(5);
addFive(3); /* 8 */

/* With named args */
let divide = (~dividend, ~divisor) =>
  dividend /. divisor;

let halve = divide(~divisor=2.0);
halve(~dividend=10.0); /* 5.0 */

Pattern Matching

Switch Expression

/* Basic matching */
let result = switch (value) {
| 0 => "zero"
| 1 => "one"
| 2 => "two"
| _ => "many" /* Catch-all */
};

/* With variables */
switch (x) {
| 0 => "nothing"
| n => "value: " ++ string_of_int(n)
};

Guards

/* When clauses */
switch (age) {
| n when n < 0 => "Invalid"
| n when n < 13 => "Child"
| n when n < 20 => "Teenager"
| _ => "Adult"
};

/* Multiple conditions */
switch (user) {
| {age, admin: true} when age > 18 =>
    "Admin"
| {age} when age > 18 => "User"
| _ => "Minor"
};

List Matching

/* Destructuring lists */
switch (myList) {
| [] => "empty"
| [x] => "one element"
| [x, y] => "two elements"
| [head, ...tail] => "many"
};

/* Pattern matching in lists */
switch (items) {
| [1, 2, ...rest] => rest
| _ => []
};

Record & Variant Matching

/* Record matching */
type person = {name: string, age: int};

switch (person) {
| {name: "Alice", age} => age
| {age: 18, name} => name
| {name, age: _} => name
};

/* Variant matching */
type color = Red | Green | Blue;

switch (color) {
| Red => "#FF0000"
| Green => "#00FF00"
| Blue => "#0000FF"
};

Data Structures

Records

/* Definition */
type person = {
  name: string,
  age: int,
  email: string,
};

/* Creation */
let alice = {
  name: "Alice",
  age: 30,
  email: "alice@example.com",
};

/* Access */
alice.name; /* "Alice" */
alice.age;  /* 30 */

/* Immutable update */
let olderAlice = {
  ...alice,
  age: 31,
};

Variants (Tagged Unions)

/* Simple variants */
type color =
  | Red
  | Green
  | Blue;

/* With constructor arguments */
type shape =
  | Circle(float)
  | Rectangle(float, float)
  | Triangle(float, float, float);

/* Using variants */
let area = (shape) =>
  switch (shape) {
  | Circle(r) => 3.14 *. r *. r
  | Rectangle(w, h) => w *. h
  | Triangle(a, b, c) => /* ... */ 0.0
  };

Option Type

/* Definition (built-in) */
type option('a) =
  | Some('a)
  | None;

/* Usage */
let findUser = (id) =>
  if (id > 0) {
    Some("Alice")
  } else {
    None
  };

/* Pattern matching */
switch (findUser(1)) {
| Some(name) => "Found: " ++ name
| None => "Not found"
};

Lists

/* Creation */
let empty = [];
let numbers = [1, 2, 3, 4, 5];
let strings = ["a", "b", "c"];

/* Cons operator :: */
let nums = [1, 2, 3];
let moreNums = [0, ...nums];

/* Operations */
List.length(numbers); /* 5 */
List.hd(numbers); /* 1 */
List.tl(numbers); /* [2,3,4,5] */

/* Map and filter */
List.map((x) => x * 2, numbers);
List.filter((x) => x > 2, numbers);

Arrays

/* Creation (mutable) */
let arr = [|1, 2, 3, 4, 5|];

/* Access */
arr[0]; /* 1 */

/* Mutation */
arr[0] = 10;

/* Operations */
Array.length(arr); /* 5 */
Array.map((x) => x * 2, arr);
Array.get(arr, 2); /* 3 */
Array.set(arr, 2, 99);

Lists vs Arrays

Feature List [1,2,3] Array `[ 1,2,3 ]`
Mutability Immutable Mutable
Access O(n) O(1)
Prepend O(1) with :: O(n)
Size Dynamic Fixed
Memory Linked list Contiguous

Modules

Module Definition

/* Inline module */
module Math = {
  let pi = 3.14159;
  let add = (x, y) => x + y;
  let multiply = (x, y) => x * y;
};

/* Usage */
Math.pi;
Math.add(2, 3);

Opening Modules

/* Global open */
open Belt.List;
map((x) => x * 2, [1, 2, 3]);

/* Local open */
Belt.List.(
  map((x) => x * 2, [1, 2, 3])
);

/* Local open with let */
let doubled = {
  open Belt.List;
  map((x) => x * 2, [1, 2, 3]);
};

Module Types (Signatures)

/* Define interface */
module type Comparable = {
  type t;
  let compare: (t, t) => int;
};

/* Implement */
module IntCompare: Comparable = {
  type t = int;
  let compare = (a, b) => a - b;
};

Functors

/* Module function */
module MakeSet =
  (Item: Comparable) => {
    type t = list(Item.t);

    let empty = [];
    let add = (item, set) => [item, ...set];
  };

/* Usage */
module IntSet = MakeSet(IntCompare);

Control Flow

If/Else

/* Expression (returns value) */
let result = if (condition) {
  "yes"
} else {
  "no"
};

/* Ternary style */
let message =
  if (age >= 18) { "Adult" } else { "Minor" };

/* Nested */
if (score >= 90) {
  "A"
} else if (score >= 80) {
  "B"
} else {
  "C"
};

For Loops

/* Ascending (to) */
for (i in 0 to 4) {
  Js.log(i); /* 0,1,2,3,4 */
};

/* Descending (downto) */
for (i in 4 downto 0) {
  Js.log(i); /* 4,3,2,1,0 */
};

/* With refs for mutation */
let sum = ref(0);
for (i in 1 to 10) {
  sum := sum^ + i;
};

While Loops

/* Basic while */
let i = ref(0);
while (i^ < 5) {
  Js.log(i^);
  i := i^ + 1;
};

/* Infinite loop with break */
while (true) {
  let input = getInput();
  if (input == "quit") {
    break; /* Not built-in, use recursion */
  };
};

Recursion

/* Recursive function */
let rec factorial = (n) =>
  if (n <= 1) {
    1
  } else {
    n * factorial(n - 1)
  };

/* Tail recursion */
let rec factorialTail = (n, acc) =>
  if (n <= 1) {
    acc
  } else {
    factorialTail(n - 1, n * acc)
  };

factorialTail(5, 1);

Pipe Operators

Pipe Last (|>)

/* Data flows left to right */
let result =
  [1, 2, 3, 4, 5]
  |> List.map((x) => x * 2)
  |> List.filter((x) => x > 5)
  |> List.fold_left((+), 0);

/* Without pipe */
List.fold_left(
  (+),
  0,
  List.filter(
    (x) => x > 5,
    List.map((x) => x * 2, [1,2,3,4,5])
  )
);

Pipe First (->)

/* Data as first argument */
let result =
  [1, 2, 3]
  ->Belt.List.map((x) => x * 2)
  ->Belt.List.keep((x) => x > 2);

/* Equivalent to */
Belt.List.keep(
  Belt.List.map([1, 2, 3], (x) => x * 2),
  (x) => x > 2
);

Choosing Pipe Operators

Operator Data Position Common With
|> Last argument Stdlib, List
-> First argument Belt, Js
/* Stdlib uses pipe last */
[1, 2, 3] |> List.map((x) => x * 2);

/* Belt uses pipe first */
[1, 2, 3] -> Belt.List.map((x) => x * 2);

React & JSX

React Component

/* Basic component */
@react.component
let make = () => {
  <div>
    {React.string("Hello")}
  </div>
};

/* With props */
@react.component
let make = (~name, ~age) => {
  <div>
    <h1>{React.string(name)}</h1>
    <p>{React.int(age)}</p>
  </div>
};

Props & Children

/* With children */
@react.component
let make = (~children) => {
  <div className="wrapper">
    {children}
  </div>
};

/* Optional props */
@react.component
let make = (~title=?, ~className="") => {
  <div className>
    {switch (title) {
     | Some(t) => <h1>{React.string(t)}</h1>
     | None => React.null
    }}
  </div>
};

Hooks

/* useState */
@react.component
let make = () => {
  let (count, setCount) =
    React.useState(() => 0);

  <button
    onClick={_ => setCount(_ => count + 1)}>
    {React.int(count)}
  </button>
};

/* useEffect */
@react.component
let make = (~userId) => {
  React.useEffect1(() => {
    fetchUser(userId);
    None; /* No cleanup */
  }, [userId]);

  <div />
};

Event Handlers

@react.component
let make = () => {
  let handleClick = (event) => {
    ReactEvent.Mouse.preventDefault(event);
    Js.log("Clicked!");
  };

  <button onClick={handleClick}>
    {React.string("Click me")}
  </button>
};

JavaScript Interop

External Bindings

/* Bind to global */
@mel.val
external alert: string => unit = "alert";

alert("Hello!");

/* Bind to module */
@mel.module("path")
external join: (string, string) => string = "join";

join("/usr", "local");

Object Methods

/* Method binding */
@mel.send
external push: (array('a), 'a) => unit = "push";

let arr = [|1, 2, 3|];
push(arr, 4);

/* Getter */
@mel.get
external length: array('a) => int = "length";

length(arr); /* 4 */

Module Import

/* Default import */
@mel.module("./utils")
external utils: {..} = "default";

/* Named import */
@mel.module("lodash")
external debounce: ('a => unit, int) => ('a => unit)
  = "debounce";

/* Multiple bindings */
@mel.module("fs")
external readFile: (string, (option(error), string) => unit) => unit
  = "readFile";

@mel.module("fs")
external writeFile: (string, string) => unit
  = "writeFile";

Raw JavaScript

/* Inline JS */
let add = [%mel.raw {|
  function(a, b) {
    return a + b;
  }
|}];

/* Template with variables */
let logValue = (x) => {
  [%mel.raw {|console.log($x)|}];
};

/* Unsafe type casting */
let unsafeObj: myType =
  [%mel.raw {|{name: "test", value: 42}|}];

Object Types

/* Object type */
type point = {
  .
  "x": float,
  "y": float,
};

let origin: point = {
  "x": 0.0,
  "y": 0.0,
};

/* Accessing JS objects */
let x = origin["x"];

/* Creating JS objects */
let makePoint = (x, y) => {
  {"x": x, "y": y}
};

Common Patterns

Result Type

/* Error handling without exceptions */
type result('a, 'e) =
  | Ok('a)
  | Error('e);

let divide = (a, b) =>
  if (b == 0) {
    Error("Division by zero")
  } else {
    Ok(a / b)
  };

/* Usage */
switch (divide(10, 2)) {
| Ok(result) => Js.log(result)
| Error(msg) => Js.log("Error: " ++ msg)
};

Belt.Option Helpers

open Belt.Option;

/* Map */
let doubled = map(Some(5), (x) => x * 2);
/* Some(10) */

/* GetWithDefault */
getWithDefault(None, 0); /* 0 */
getWithDefault(Some(5), 0); /* 5 */

/* FlatMap */
flatMap(Some(5), (x) =>
  if (x > 0) { Some(x * 2) } else { None }
);

Comparison Functions

/* String comparison */
"abc" == "abc"; /* true (structural) */
"abc" === "abc"; /* true (referential) */

/* Deep equality */
let listA = [1, 2, 3];
let listB = [1, 2, 3];
listA == listB; /* true */

/* Reference equality */
let arr1 = [|1, 2|];
let arr2 = [|1, 2|];
arr1 === arr2; /* false (different refs) */

String Operations

/* Concatenation */
"Hello" ++ " " ++ "World";

/* Interpolation (with helper) */
let name = "Alice";
{j|Hello $name!|j};

/* Conversion */
string_of_int(42); /* "42" */
int_of_string("42"); /* 42 */

Type System Advanced

Phantom Types

/* Type-level validation */
type validated;
type unvalidated;

type email('a) = string;

let validate: email(unvalidated) =>
  option(email(validated)) =
  (email) => {
    if (Js.Re.test_(%re("/.*@.*/"), email)) {
      Some(email)
    } else {
      None
    }
  };

/* Can't use unvalidated email */
let send: email(validated) => unit =
  (email) => Js.log("Sending to: " ++ email);

Polymorphic Variants

/* Open variant types */
type color = [
  | `Red
  | `Green
  | `Blue
];

let isWarm = (c) =>
  switch (c) {
  | `Red | `Orange => true
  | `Blue | `Green => false
  | _ => false
  };

/* Accepts more than defined */
isWarm(`Red);
isWarm(`Orange); /* Works! */

Recursive Types

/* Tree structure */
type tree('a) =
  | Leaf
  | Node(tree('a), 'a, tree('a));

let rec sum = (tree) =>
  switch (tree) {
  | Leaf => 0
  | Node(left, value, right) =>
      sum(left) + value + sum(right)
  };

Gotchas

Integer Division

/* Integer division (/) */
5 / 2; /* 2 (not 2.5!) */

/* Float division (/.) */
5.0 /. 2.0; /* 2.5 */

/* Operators are type-specific */
1 + 2;      /* int addition */
1.0 +. 2.0; /* float addition */

String Comparison

/* Structural equality (==) */
"hello" == "hello"; /* true */

/* Reference equality (===) */
"hello" === "hello"; /* true for literals */

/* For strings, usually use == */
let name = "Alice";
if (name == "Alice") {
  Js.log("Match");
};

List Performance

/* O(1) prepend */
let list = [1, 2, 3];
let newList = [0, ...list]; /* Fast */

/* O(n) append - SLOW! */
let slowAppend = list @ [4]; /* Avoid */

/* Use List.rev for accumulation */
let rec reverseMap = (f, list) =>
  switch (list) {
  | [] => []
  | [x, ...xs] =>
      reverseMap(f, xs) @ [f(x)] /* SLOW */
  };

Semicolon Rules

/* Semicolons separate statements */
let x = 1;
let y = 2; /* Required between bindings */

/* Last expression in block has no ; */
let result = {
  let a = 1;
  let b = 2;
  a + b /* No semicolon */
};

/* In sequences, use ; */
{
  Js.log("First");
  Js.log("Second"); /* Optional on last */
}

Comparison with JavaScript

Variables

/* ReasonML: Immutable by default */
let x = 10;
let x = 20; /* New binding (shadowing) */

/* Mutation requires ref */
let counter = ref(0);
counter := counter^ + 1;
/* JavaScript: Mutable by default */
let x = 10;
x = 20; // Mutation

/* Immutable requires const */
const y = 10;
// y = 20; // Error

Functions

/* ReasonML: Curried */
let add = (x, y) => x + y;
let addFive = add(5);
addFive(3); /* 8 */

/* Named arguments */
let greet = (~name, ~age) => "Hello";
greet(~age=30, ~name="Alice");
/* JavaScript: Not curried */
const add = (x, y) => x + y;
const addFive = (y) => add(5, y);
addFive(3); // 8

/* Destructuring for "named args" */
const greet = ({ name, age }) => "Hello";
greet({ age: 30, name: "Alice" });

Pattern Matching

/* ReasonML: Exhaustive */
switch (value) {
| Some(x) => x
| None => 0
}; /* Compiler ensures all cases */

/* Guards */
switch (age) {
| n when n < 18 => "Minor"
| _ => "Adult"
};
/* JavaScript: Switch (not exhaustive) */
switch (value) {
  case "some":
    return x;
  case "none":
    return 0;
  // Easy to forget cases
}

/* No guards, use if/else */
if (age < 18) {
  return "Minor";
} else {
  return "Adult";
}

Arrays & Lists

/* ReasonML: Separate types */
let list = [1, 2, 3]; /* Immutable linked list */
let arr = [|1, 2, 3|]; /* Mutable array */

List.hd(list); /* 1 */
arr[0]; /* 1 */
/* JavaScript: Only arrays */
const arr = [1, 2, 3]; // Mutable
arr[0]; // 1
arr.push(4); // Mutation

Also see