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
- ReasonML Official Documentation - Official language guide and reference
- ReScript Documentation - ReScript (ReasonML successor) docs
- Melange Documentation - OCaml to JavaScript compiler
- ReasonReact - React bindings for Reason
- Belt Standard Library - Functional utilities optimized for JS
- GitHub Issue #180 - Original cheatsheet request