Docs / Language Manual / Function


Cheat sheet for the full function syntax at the end

Can you believe we haven't covered function until now?

Functions are declared with an arrow and return the expression.

let greet = (name) => "Hello " ++ name;

This declares a function and assigns to it the name greet, which you can call like so:

greet("world!"); /* "Hello world!" */

Multi-arguments functions have arguments separated by comma:

let add = (x, y, z) => x + y + z; add(1, 2, 3); /* 6 */

For longer functions, you'd surround the body with a block:

let greetMore = (name) => { let part1 = "Hello"; part1 ++ " " ++ name };

No Argument

A function always takes an argument; but sometimes, we'd use it for e.g. side-effects, and don't have anything to pass to it. In other languages, we'd conceptually pass "no argument". In Reason, every function takes an argument; here we'd conventionally pass it the value (), called "unit".

/* receive & destructure the unit argument */ let logSomething = () => { print_endline("hello"); print_endline("world") }; /* call the function with the value of type unit */ logSomething();

() is a totally normal value, the single possible value in unit. Reason gave it a special syntax out of convenience.

Labeled Arguments

Multi-arguments functions, especially those whose arguments are of the same type, can be confusing to call.

let addCoordinates = (x, y) => { /* use x and y here */ }; /* ... */ addCoordinates(5, 6); /* which is x, which is y? */

In OCaml/Reason, you can attach labels to an argument by prefixing the name with the ~ symbol:

let addCoordinates = (~x, ~y) => { /* use x and y here */ }; /* ... */ addCoordinates(~x=5, ~y=6);

Since we have currying (more on that below), we can provide the arguments in any order:

addCoordinates(~y=6, ~x=5);

The ~x part in the declaration means the function accepts an argument labeled x and can refer to it in the function body by the same name. You can also refer to the arguments inside the function body by a different name for conciseness:

let drawCircle = (~radius as r, ~color as c) => { setColor(c); startAt(r, r); /* ... */ }; drawCircle(~radius=10, ~color="red");

As a matter of fact, (~radius) is just a shorthand (called punning) for (~radius as radius).

Here's the syntax for typing the arguments:

let drawCircle = (~radius as r: int, ~color as c: string) => ...;


Reason functions can automatically be partially called:

let add = (x, y) => x + y; let addFive = add(5); let eleven = addFive(6); let twelve = addFive(7);

Actually, the above add is nothing but syntactic sugar for this:

let add = (x) => (y) => x + y;

OCaml optimizes this to avoid the unnecessary function allocation (2 functions here, naively speaking) whenever it can! This way, we get

  • Nice syntax

  • Currying for free (every function takes a single argument, actually!)

  • No performance cost

Optional Labeled Arguments

Labeled function arguments can be made optional during declaration. You can then omit them when calling the function.

/* radius can be omitted */ let drawCircle = (~color, ~radius=?, ()) => { setColor(color); switch (radius) { | None => startAt(1, 1) | Some(r_) => startAt(r_, r_) } };

When given in this syntax, radius is wrapped in the standard library's option type, defaulting to None. If provided, it'll be wrapped with a Some. So radius's type value is either None or Some(int) here.

Note: None | Some(foo) is a data structure type called variant, described earlier. This particular variant type is provided by the standard library. It's called option. Its definition: type option('a) = None | Some('a).

Note the unit () at the end of drawCircle. Writing this particular function without the unit () would lead to the following problem. Because radius and color are both labeled, the function can be curried, and it can be applied out-of-order, it's unclear what the following means:

let whatIsThis = drawCircle(~color);

Is whatIsThis a curried drawCircle function, waiting for the optional radius to be applied? Or did it finish applying because the radius is optional? To address this confusion, append a positional (aka non-labeled) argument to drawCircle (conventionally ()), and OCaml will, as a rule of thumb, presume the optional labeled argument is omitted when the positional argument is provided.

Because we don't supply the unit OCaml knows we want to curry the function.

let curriedFunction = drawCircle(~color);

Because we do supply the unit OCaml knows we deliberately omit the radius parameter, and the function is executed.

let circle = drawCircle(~color, ());

Signatures and Type Annotations

Functions with optional labeled arguments can be confusing when it comes to signature and type annotations. Indeed, the type of an optional labeled argument looks different depending on whether you're calling the function, or working inside the function body. Outside the function, a raw value is either passed in (int, for example), or left off entirely. Inside the function, the parameter is always there, but its value is an option (option(int)). This means that the type signature is different, depending on whether you're writing out the function type, or the parameter type annotation. The first being a raw value, and the second being an option.

If we get back to our previous example and both add a signature and type annotations to its argument, we get this:

let drawCircle: (~color: color, ~radius: int=?, unit) => unit = (~color: color, ~radius: option(int)=?, ()) => { setColor(color); switch (radius) { | None => startAt(1, 1) | Some(r_) => startAt(r_, r_) }; };

The first line is the function's signature, we would define it like that in an interface file (cf. Signatures). The function's signature describes the types that the outside world interacts with, hence the type int for radius because it indeed expects an int when called.

In the second line, we annotate the arguments to help us remember the types of the arguments when we use them inside the function's body, here indeed radius will be an option(int) inside the function.

So if you happen to struggle when writing the signature of a function with optional labeled arguments, try to remember this!

Explicitly Passed Optional

Sometimes, you might want to forward a value to a function without knowing whether the value is None or Some(a). Naively, you'd do:

let result = switch (payloadRadius) { | None => drawCircle(~color, ()) | Some(r) => drawCircle(~color, ~radius=r, ()) };

This quickly gets tedious. We provide a shortcut:

let result = drawCircle(~color, ~radius=?payloadRadius, ());

This means "I understand radius is optional, and that when I pass it a value it needs to be an int, but I don't know whether the value I'm passing is None or Some(val), so I'll pass you the whole option wrapper".

Optional with Default Value

Optional labeled arguments can also be provided a default value. In this case, they aren't wrapped in an option type.

let drawCircle = (~radius=1, ~color, ()) => { setColor(color); startAt(radius, radius) };

Recursive Functions

By default, a value can't see a binding that points to it, but including the rec keyword in a let binding makes this possible. This allows functions to see and call themselves, giving us the power of recursion.

let rec neverTerminate = () => neverTerminate();

In imperative languages, including JavaScript, recursive functions are rarely used. In Reason, recursive functions are common, and can typically replace an imperative loop.

A simple recursive function may look like this:

/** * Recursively check every item on the list until one equals the `item` * argument. If a match is found, return `true`, otherwise return `false`. */ let rec listHas = (list, item) => switch (list) { | [] => false | [a,] => a == item || listHas(rest, item) };

Using recursion with a switch statement is a powerful combination that can sometimes produce code simpler than a standard library function like fold or reduce. In fact, if you dig into the Reason standard library's source code, you'll see that its implementations of fold or reduce are actually just recursive functions and switch statements!

Tail Recursion

The reason why recursive functions are avoided in many languages, such as JavaScript, is those languages don't support "tail call optimization." This means that each time the function recurses, the previous call to the function is held in memory, which can lead to a stack overflow.

When the recursion happens in the tail position (meaning that the recursive call is the last step performed in the function), this can be optimized in a way that is stack safe. Fortunately for us, the Reason compiler is able to detect and perform this optimization.

For example, if you take our listHas function and compile it to JavaScript, you'll see that it removes the tail-calls by compiling into a while loop! Reason gives us the best of both worlds.

For writing for or while loops instead of recursive functions, see: Imperative Loops.

Mutually Recursive Functions

Mutually recursive functions start like a single recursive function using the rec keyword, and then are chained together with and:

let rec callSecond = () => callFirst() and callFirst = () => callSecond();

Note that there's no semicolon ending the first line and no let on the second line.

Tips & Tricks

Cheat sheet for the function syntaxes:


/* anonymous function. Listed for completeness only */ (x) => (y) => 1; /* sugar for the above */ (x, y) => 1; /* assign to a name */ let add = (x, y) => 1; /* labeled */ let add = (~first as x, ~second as y) => x + y; /* with punning sugar */ let add = (~first, ~second) => first + second; /* labeled with default value */ let add = (~first as x=1, ~second as y=2) => x + y; /* with punning */ let add = (~first=1, ~second=2) => first + second; /* optional */ let add = (~first as x=?, ~second as y=?) => switch (x) {...}; /* with punning */ let add = (~first=?, ~second=?) => switch (first) {...};

With Type Annotation

/* anonymous function */ (x: int) => (y: int): int => 1; /* sugar for the above */ (x: int, y: int): int => 1; /* assign to a name */ let add = (x: int, y: int): int => 1; /* labeled */ let add = (~first as x: int, ~second as y: int) : int => x + y; /* with punning sugar */ let add = (~first: int, ~second: int) : int => first + second; /* labeled with default value */ let add = (~first as x: int=1, ~second as y: int=2) : int => x + y; /* with punning sugar */ let add = (~first: int=1, ~second: int=2) : int => first + second; /* optional */ let add = (~first as x: option(int)=?, ~second as y: option(int)=?) : int => switch (x) {...}; /* with punning sugar */ /* note that the caller would pass an `int`, not `option int` */ /* Inside the function, `first` and `second` are `option int`. */ let add = (~first: option(int)=?, ~second: option(int)=?) : int => switch (first) {...};


/* anonymous application. Listed for completeness only */ add(x)(y); /* sugar for the above */ add(x, y); /* labeled */ add(~first=1, ~second=2); /* with punning sugar */ add(~first, ~second); /* application with default value. Same as normal application */ add(~first=1, ~second=2); /* explicit optional application */ add(~first=?Some(1), ~second=?Some(2)); /* with punning */ add(~first?, ~second?);

With Type Annotation

/* anonymous application */ add(x: int)(y: int); /* labeled */ add(~first=1: int, ~second=2: int); /* with punning sugar */ add(~first: int, ~second: int); /* application with default value. Same as normal application */ add(~first=1: int, ~second=2: int); /* explicit optional application */ add(~first=?Some(1): option(int), ~second=?Some(2): option(int)); /* with punning sugar */ add(~first: option(int)?, ~second: option(int)?);

Standalone Type Signature

/* first arg type, second arg type, return type */ type foo = int => int => int; /* sugar for the above */ type foo = (int, int) => int; /* labeled */ type foo = (~first: int, ~second: int) => int; /* labeled */ type foo = (~first: int=?, ~second: int=?, unit) => int;

In Interface Files

To annotate a function from the implementation file (.re):

let add: int => int => int; /* sugar for the above */ let add: (int, int) => int;

Same rules as the previous section, except replacing type foo = bar with let add: bar.

Don't confuse this with actually exporting a type in the interface file. let add: bar annotates an existing value bar from the implementation file. type foo = bar exports a type of the same shape from the implementation file.