Exploring ReasonML
Please support this book: buy it or donate
(Ad, please don’t block.)

10 Functions

This chapter explores how functions work in ReasonML.

10.1 Defining functions

An anonymous (nameless) function looks as follows:

(x) => x + 1;

This function has a single parameter, x, and the body x + 1.

You can give that function a name by binding it to a variable:

let plus1 = (x) => x + 1;

This is how you call plus1:

# plus1(5);
- : int = 6

10.1.1 Functions as parameters of other functions (high-order functions)

Functions can also be parameters of other functions. To demonstrate this feature, we briefly use lists, which are explained in their own chapter. Lists are, roughly, singly linked lists and similar to immutable arrays.

The list function List.map(func, list) takes list, applies func to each of its elements and returns the results in a new list. For example:

# List.map((x) => x + 1, [12, 5, 8, 4]);
- : list(int) = [13, 6, 9, 5]

Functions that have functions as parameters or results are called higher-order functions. Functions that don’t are called first-order functions. List.map() is a higher-order function. plus1() is a first-order function.

10.1.2 Blocks as function bodies

A function’s body is an expression. Given that scope blocks are expressions, the following two definitions for plus1 are equivalent.

let plus1 = (x) => x + 1;

let plus1 = (x) => {
    x + 1
};

10.2 Single parameters without parentheses

If a function has a single parameter and that parameter is defined via an identifier, you can omit the parentheses:

let plus1 = x => x + 1;

10.3 Recursive bindings via let rec

Normally, you can only refer to let-bound values that already exist. That means that you can’t define mutually recursive and self-recursive functions.

10.3.1 Defining mutually recursive functions

Let’s examine mutually recursive functions first. The following two functions even and odd are mutually recursive (this is an example, not how you’d actually implement these functions). You must use the special let rec to define them:

let rec even = (x) =>
  if (x <= 0) {
    true;
  } else {
    odd(x - 1);
  }
and odd = (x) =>
  if (x <= 0) {
    false;
  } else {
    even(x - 1);
  };

Notice how and connects multiple let rec entries that all know each other. There is no semicolon before the and. The semicolon at the end indicates that let rec is finished.

Let’s use these functions:

# even(11);
- : bool = false
# even(2);
- : bool = true
# odd(11);
- : bool = true
# odd(2);
- : bool = false

10.3.2 Defining self-recursive functions

You also need let rec for functions that call themselves recursively, because when the recursive call is made, the binding does not exist, yet. For example:

let rec factorial = (x) =>
  if (x <= 2) {
    x
  } else {
    x * factorial(x - 1)
  };

factorial(3); /* 6 */
factorial(4); /* 24 */

10.4 Terminology: arity

The arity of a function is how many (positional) parameters it has. The arity of factorial() is 1. The following adjectives describe functions with arities from 0 to 2:

Beyond arity 3, we talk about 4-ary functions, 5-ary functions etc. Functions whose arity can vary are called variadic functions. These are also called varargs in some programming languages.

10.5 The types of functions

Functions are the first time that we get in contact with complex types: types built by combining other types. Let’s use rtop to determine the types of a two functions.

10.5.1 Types of first-order functions

First, a function add():

# let add = (x, y) => x + y;
let add: (int, int) => int = <fun>;

Therefore, the type of add is:

(int, int) => int

The arrow indicates that add is a function. Its parameters are two ints. Its result is a single int.

The notation (int, int) => int is also called the (type) signature of add. It describes the types of its inputs and its outputs.

10.5.2 Types of higher-order functions

Second, a higher-order function callFunc():

# let callFunc = (f) => f(1) + f(2);
let callFunc: ((int) => int) => int = <fun>;

You can see that the parameter of callFunc is itself a function and has the type (int) => int.

This is how callFunc() is used:

# callFunc(x => x);
- : int = 3
# callFunc(x => 2 * x);
- : int = 6

10.5.3 Type annotations and type inference

Type annotations are optional in ReasonML, but they improve the accuracy of type checking. The most extreme is to annotate everything:

let add = (x: int, y: int): int => x + y;

We have provided type annotations for both parameters x and y and for the result of the function (the last : int before the arrow).

You can omit the annotation for the return type and ReasonML will infer it (deduce it from the type of the parameters):

# let add = (x: int, y: int) => x + y;
let add: (int, int) => int = <fun>;

However, type inference is more sophisticated than that. It doesn’t only work top-down, it can also infer the types of the parameters from the int-only plus operator (+):

# let add = (x, y) => x + y;
let add: (int, int) => int = <fun>;

If you want, you can also annotate only some of the parameters:

let add = (x, y: int) => x + y;

10.5.4 Type annotations: best practice

The coding style I prefer for functions is to annotate all parameters, but to let ReasonML infer the return type. Apart from improving type checking, annotations for parameters are also good documentation.

10.6 There are no functions without parameters

ReasonML doesn’t have nullary functions, but you can use it without ever noticing that.

Recall that () is roughly similar to null in many C-style languages. It is the only element of type unit. When calling functions, omitting parameters is the same as passing the unit value as a single parameter. That is, the following two expressions are equivalent.

func()
func(())

The following example demonstrates this phenomenon: If you call a unary function without parameters, rtop underlines () and complains about that expression having the wrong type. It does not complain about not enough parameters being provided (it doesn’t partially apply either – details later).

# let id = (x: int) => x;
let id: (int) => int = <fun>;
# id();
Error: This expression has type unit but
an expression was expected of type int

If you define a function that has no parameters, ReasonML adds a parameter for you, whose type is unit:

# let f = () => 123;
let f: (unit) => int = <fun>;

10.6.1 Why no nullary functions?

Why doesn’t ReasonML have nullary functions? That is due to ReasonML always performing partial application (explained in detail later): If you don’t provide all of a function’s parameters, you get a new function from the remaining parameters to the result. As a consequence, if you could actually provide no parameters at all, then func() would be the same as func and neither would actually call func.

10.7 Destructuring function parameters

Destructuring can be used wherever variables are bound to values. That is, it also works in parameter definitions. Let’s look at a function that adds the components of a tuple:

let addComponents = ((x, y)) => x + y;
let tuple = (3, 4);
addComponents(tuple); /* 7 */

The double parentheses around x, y indicate that addComponents is a function with a single parameter, a tuple whose components are x and y. It is not a function with the two parameters x and y. Its type is:

addComponents: ((int, int)) => int

When it comes to type annotations, you can either annotate the components:

# let addComponents = ((x: int, y: int)) => x + y;
let addComponents: ((int, int)) => int = <fun>;

Or you can annotate the whole parameter:

# let addComponents = ((x, y): (int, int)) => x + y;
let addComponents: ((int, int)) => int = <fun>;

10.8 Labeled parameters

So far, we have only used positional parameters: the position of an actual parameter at the call site determines what formal parameter it is bound to.

But ReasonML also supports labeled parameters. Here, labels are used to associate actual parameters with formal parameters.

As an example, let’s examine a version of add that uses labeled parameters:

let add = (~x, ~y) => x + y;
add(~x=7, ~y=9); /* 16 */

In this function definition, we used the same name for the label ~x and the parameter x. You can also use separate names, e.g. ~x for the label and op1 for the parameter:

/* Inferred types */
let add = (~x as op1, ~y as op2) =>
  op1 + op2;

/* Specified types */
let add = (~x as op1: int, ~y as op2: int) =>
  op1 + op2;

At call sites, you can abbreviate ~x=x as just ~x:

let x = 7;
let y = 9;
add(~x, ~y);

One nice feature of labels is that you can mention labeled parameters in any order:

# add(~x=3, ~y=4);
- : int = 7
# add(~y=4, ~x=3);
- : int = 7

10.8.1 Compatibility of function types with labeled parameters

There is one unfortunate caveat to being able to mention labeled parameters in any order: function types are only compatible if labels are mentioned in the same order.

Consider the following three functions.

let add = (~x, ~y) => x + y;
let addxy = (add: ((~x: int, ~y: int) => int)) => add(5, 2);
let addyx = (add: ((~y: int, ~x: int) => int)) => add(5, 2);

addxy works as expected with add:

# addxy(add);
- : int = 7

However, with addyx, we get an error, because the labels are in the wrong order:

# addyx(add);
Error: This expression has type
(~x: int, ~y: int) => int
but an expression was expected of type
(~y: int, ~x: int) => int

10.9 Optional parameters

In ReasonML, only labeled parameters can be optional. In the following code, both x and y are optional.

let add = (~x=?, ~y=?, ()) =>
  switch (x, y) {
  | (Some(x'), Some(y')) => x' + y'
  | (Some(x'), None) => x'
  | (None, Some(y')) => y'
  | (None, None) => 0
  };

Let’s examine what the relatively complicated code does.

Why the () as the last parameter? That is explained in the next section.

What does the switch expression do? If you declare a parameter as optional, it always has the type option(t), where t is whatever type the actual values have. option is a variant (which is explained in a separate chapter). For now, I’ll give a brief preview. The definition of option is:

type option('a) = None | Some('a);

It is used as follows:

In other words, option wraps values and the switch expression in the example unwraps them.

10.9.1 With optional parameters, you need at least one positional parameter

Why does add have a parameter of type unit (an empty parameter, if you will) at the end?

let add = (~x=?, ~y=?, ()) =>
  ···

The reason has to do with partial application and is explained in more detail later. In a nutshell, two things are in conflict here:

To resolve this conflict, ReasonML fills in all defaults for missing optional parameters when it encounters the first positional parameter. Before it encounters a positional parameter, it still waits for the missing optional parameters. That is, you need a positional parameter to trigger the call and since add() doesn’t have one, we added an empty one.

The advantage of this slightly weird approach is that you get the best of both worlds: you get partial application and optional parameters.

10.9.2 Type annotations for optional parameters

When you annotate optional parameters, they must all have option(···) types:

let add = (~x: option(int)=?, ~y: option(int)=?, ()) =>
  ···

The type signature of add is:

(~x: int=?, ~y: int=?, unit) => int

It’s unfortunate that the definition differs from the type signature in this case. But it is the same as for parameters with default values (which are explained next) where it makes sense. The idea is to hide the implementation detail of how optional parameters are handled.

10.9.3 Parameter default values

Handling missing parameters can be cumbersome:

let add = (~x=?, ~y=?, ()) =>
  switch (x, y) {
  | (Some(x'), Some(y')) => x' + y'
  | (Some(x'), None) => x'
  | (None, Some(y')) => y'
  | (None, None) => 0
  };

In this case, all we want is for x and y to be zero if they are omitted. ReasonML has special syntax for this:

let add = (~x=0, ~y=0, ()) => x + y;

10.9.4 Type annotations with parameter default values

If there are default values, type annotations are more intuitive (no option()):

let add = (~x: int=0, ~y: int=0, ()) =>
  x + y;

The type signature of add is:

(~x: int=?, ~y: int=?, unit) => int

10.9.5 Passing option values to optional parameters (advanced)

Internally, optional parameters are received as elements of the option type (None or Some(x)). Until now, you could only pass those values by either providing or omitting parameters. But there is also a way to pass those values directly. Before we get to use cases for this feature, let’s try it out first, via the following function.

let multiply = (~x=1, ~y=1, ()) => x * y;

multiply has two optional parameters. Let’s start by providing ~x and omitting ~y, via elements of option:

# multiply(~x = ?Some(14), ~y = ?None, ());
- : int = 14

The syntax for passing option values is:

~label = ?expression

If expression is a variable whose name is label, you can abbreviate: the following two syntaxes are equivalent.

~foo = ?foo
~foo?

So what is the use case? It’s one function forwarding an optional parameter to another function’s optional parameter. That way, it can rely on that function’s parameter default value and doesn’t have to define one itself.

Let’s look at an example: The following function square has an optional parameter, which is passes on to multiply’s two optional parameters:

let square = (~x=?, ()) => multiply(~x?, ~y=?x, ());

square does not have to specify a parameter default value, it can use multiply’s defaults.

10.10 Partial application

Partial application is a mechanism that makes functions more versatile: If you omit one or more parameters at the end of a function call f(···), f returns a function that maps the missing parameters to f’s final result. That is, you apply f to its parameters in multiple steps. The first step is called a partial application or a partial call.

Let’s see how that works. We first create a function add with two parameters:

# let add = (x, y) => x + y;
let add: (int, int) => int = <fun>;

Then we partially call the binary function add to create the unary function plus5:

# let plus5 = add(5);
let plus5: (int) => int = <fun>;

We have only provided add’s first parameter, x. Whenever we call plus5, we provide add’s second parameter, y:

# plus5(2);
- : int = 7

10.10.1 Why is partial application useful?

Partial application lets you write more compact code. To demonstrate how, we’ll work with a list of numbers:

# let numbers = [11, 2, 8];
let numbers: list(int) = [11, 2, 8];

Next, we’ll use the standard library function List.map. List.map(func, myList) takes myList, applies func to each of its elements and returns them as a new list.

We use List.map to add 2 to each element of numbers:

# let plus2 = x => add(2, x);
let plus2: (int) => int = <fun>;
# List.map(plus2, numbers);
- : list(int) = [13, 4, 10]

With partial application we can make this code more compact:

# List.map(add(2), numbers);
- : list(int) = [13, 4, 10]

Let’s compare the two versions more directly:

List.map(x => add(2, x), numbers)
List.map(add(2), numbers)

Which version is better? That depends on your taste. The first version is – arguably – more self-descriptive, the second version is more concise.

Partial application really shines with the pipe operator (|>) for function composition (which is explained later).

10.10.2 Partial application and labeled parameters

So far, we have only see partial application with positional parameters, but it works with labeled parameters, too. Consider, again, the labeled version of add:

# let add = (~x, ~y) => x + y;
let add: (~x: int, ~y: int) => int = <fun>;

If we call add with only the first labeled parameter, we get a function that maps the second parameter to the result:

# add(~x=4);
- : (~y: int) => int = <fun>

Providing only the second labeled parameter works analogously.

# add(~y=4);
- : (~x: int) => int = <fun>

That is, labels don’t impose an order here. That means that partial application is more versatile with labels, because you can partially apply any labeled parameter, not just the last one.

10.10.2.1 Partially application and optional parameters

How about optional parameters? The following version of add has only optional parameters:

# let add = (~x=0, ~y=0, ()) => x + y;
let add: (~x: int=?, ~y: int=?, unit) => int = <fun>;

If you mention only the label ~x or only the label ~y, partial application works as before, with one difference: The additional positional parameter of type unit must also still be filled in.

# add(~x=3);
- : (~y: int=?, unit) => int = <fun>
# add(~y=3);
- : (~x: int=?, unit) => int = <fun>

However, as soon as you mention the positional parameter, there is no more partial application; the defaults are now filled in:

# add(~x=3, ());
- : int = 3
# add(~y=3, ());
- : int = 3

Even if you take one or two intermediate steps, the () is always the final signal to evaluate. One intermediate step looks as follows.

# let plus5 = add(~x=5);
let plus5: (~y: int=?, unit) => int = <fun>;
# plus5(());
- : int = 5

Two intermediate steps:

# let plus5 = add(~x=5);
let plus5: (~y: int=?, unit) => int = <fun>;
# let result8 = plus5(~y=3);
let result8: (unit) => int = <fun>;
# result8(());
- : int = 8

10.10.3 Currying (advanced)

Currying is one technique for implementing partial application for positional parameters. Currying a function means transforming it from a function with an arity of 1 or more to a series of unary function calls.

For example, take the binary function add:

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

To curry add means to convert it to the following function:

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

Now we have to invoke add as follows:

# add(3)(1);
- : int = 4

What have we gained? Partial application is easy now:

# let plus4 = add(4);
let plus4: (int) => int = <fun>;
# plus4(7);
- : int = 11

And now the surprise: all functions in ReasonML are automatically curried. That’s how it supports partial application. You can see that if you look at the type of the curried add:

# let add = x => y => x + y;
let add: (int, int) => int = <fun>;

On other words: add(x, y) is the same as add(x)(y) and the following two types are equivalent:

(int, int) => int
int => int => int

Let’s conclude with a function that curries binary functions. Given that currying functions that are already curried is meaningless, we’ll curry a function whose single parameter is a pair.

let curry2 = (f: (('a, 'b)) => 'c) => x => y => f((x, y));

Let’s use curry2 with a unary version of add:

# let add = ((x, y)) => x + y;
let add: ((int, int)) => int = <fun>;
# curry2(add);
- : (int, int) => int = <fun>

The type at the end tells us that we have created a binary function.

10.11 The reverse-application operator (|>)

The operator |> is called reverse-application operator or pipe operator. It lets you chain function calls: x |> f is the same as f(x). That may not look like much, but it is quite useful when combining function calls.

10.11.1 Example: piping ints and strings

Let’s start with a simple example. Given the following two functions.

let times2 = (x: int) => x * 2;
let twice = (s: string) => s ++ s;

If we use them with traditional function calls, we get:

# twice(string_of_int(times2(4)));
- : string = "88"

First we apply times2 to 4, then string_of_int (a function in the standard library) to the result, etc. The pipe operator lets us write code that is closer to the description that I have just given:

let result = 4 |> times2 |> string_of_int |> twice;

10.11.2 Example: piping lists

With more complex data and currying, we get a style that is reminiscent of chained method calls in object-oriented programming.

For example, the following code works with a list of ints:

[4, 2, 1, 3, 5]
|> List.map(x => x + 1)
|> List.filter(x => x < 5)
|> List.sort(compare);

These functions are explained in the chapter on lists. For now, it is enough to have a rough idea of how they work.

The three computational steps are:

# let l0 = [4, 2, 1, 3, 5];
let l0: list(int) = [4, 2, 1, 3, 5];
# let l1 = List.map(x => x + 1, l0);
let l1: list(int) = [5, 3, 2, 4, 6];
# let l2 = List.filter(x => x < 5, l1);
let l2: list(int) = [3, 2, 4];
# let l3 = List.sort(compare, l2);
let l3: list(int) = [2, 3, 4];

We see that in all of these functions, the primary parameter comes last. When we piped, we first filled in the secondary parameters via partial application, creating a function. Then the pipe operator filled in the primary parameter, by calling that function.

The primary parameter is similar to this or self in object-oriented programming languages.

10.12 Tips for designing function signatures

These are a few tips for designing the type signatures of functions:

The idea behind these rules is to make code as self-descriptive as possible: The primary (or only) parameter is described by the name of the function, the remaining parameters are described by their labels.

As soon as a function has more than one positional parameter, it usually becomes difficult to tell what each parameter does. Compare, for example, the following two function calls. The second one is much easier to understand.

blit(bytes, 0, bytes, 10, 10);
blit(~src=bytes, ~src_pos=0, ~dst=bytes, ~dst_pos=10, ~len=10);

I also like optional parameters, because they enable you to add more parameters to functions without breaking existing callers. That helps with evolving APIs.

Source of this section: Sect. “Suggestions for labeling” in the OCaml Manual.

10.13 Single-argument match functions

ReasonML provides an abbreviation for unary functions that immediately switch on their parameters. Take, for example the following function.

let divTuple = (tuple) =>
  switch tuple {
  | (_, 0) => (-1)
  | (x, y) => x / y
  };

This function is used as follows:

# divTuple((9, 3));
- : int = 3
# divTuple((9, 0));
- : int = -1

If you use the fun operator to define divTuple, the code becomes shorter:

let divTuple =
  fun
  | (_, 0) => (-1)
  | (x, y) => x / y;

10.14 (Advanced)

All remaining sections are advanced.

10.15 Operators

One neat feature of ReasonML is that operators are just functions. You can use them like functions if you put them in parentheses:

# (+)(7, 1);
- : int = 8

And you can define your own operators:

# let (+++) = (s, t) => s ++ " " ++ t;
let ( +++ ): (string, string) => string = <fun>;
# "hello" +++ "world";
- : string = "hello world"

By putting an operator in parentheses, you can also easily look up its type:

# (++);
- : (string, string) => string = <fun>

10.15.1 Rules for operators

There are two kinds of operators: infix operators (between two operands) and prefix operators (before single operands).

The following operator characters can be used for both kinds of operators:

! $ % & * + - . / : < = > ? @ ^ | ~

Infix operators:

First character Followed by operator characters
= < > @ ^ ❘ & + - * / $ % 0+
# 1+

Additionally, the following keywords are infix operators:

* + - -. == != < > || && mod land lor lxor lsl lsr asr

Prefix operators:

First character Followed by operator characters
! 0+
? ~ 1+

Additionally, the following keywords are prefix operators:

- -.

Source of this section: Sect. “Prefix and infix symbols” in the OCaml Manual.

10.15.2 Precedences and associativities of operators

The following tables lists operators and their associativities. The higher up an operator, the higher its precedence is (the stronger it binds). For example, * has a higher precedence than +.

Construction or operator Associativity
prefix operator
. .( .[ .{
[ ] (array index)
#···
applications, assert, lazy left
- -. (prefix)
**··· lsl lsr asr right
*··· /··· %··· mod land lor lxor left
+··· -··· left
@··· ^··· right
=··· <··· >··· ❘··· &··· $··· != left
&& right
❘❘ right
if
let switch fun try

Legend:

Source of this table: Sect. “Expressions” in the OCaml manual

10.15.3 When does associativity matter?

Associativity matters whenever an operator is not commutative. With a commutative operator, the order of the operands does not matter. For example, plus (+) is commutative. However, minus (-) is not commutative.

Left associativity means that operations are grouped from the left. Then the following two expressions are equivalent:

x op y op z
(x op y) op z

Minus (-) is left-associative:

# 3 - 2 - 1;
- : int = 0

Right associativity means that operations are grouped from the right. Then the following two expressions are equivalent:

x op y op z
x op (y op z)

We can define our own right-associative minus operator. According to the operator table, if it starts with an @ symbol, it is automatically right-associative:

let (@-) = (x, y) => x - y;

If we use it, we get a different result than normal minus:

# 3 @- 2 @- 1;
- : int = 2

10.16 Polymorphic functions

Recall the definition of polymorphism: making the same operation work for several types. There are multiple ways in which polymorphism can be achieved. OOP languages achieve it via subclassing. Overloading is another popular kind of polymorphism.

ReasonML supports parametric polymorphism: so-called type variables indicate that any type can be filled in. (Such variables are universally quantified.) A function that uses type variables is called a generic function.

10.16.1 Example: id()

For example, id is the identity function that simply returns its parameter:

# let id = x => x;
let id: ('a) => 'a = <fun>;

The type for id that ReasonML infers is interesting: It can’t detect a type for x, so it uses the type variable 'a to indicate “any type”. Type variables always start with a straight apostrophe. ReasonML also infers that the return type of id is the same as the type of its parameter. That is useful information.

id is generic and works with any type:

# id(123);
- : int = 123
# id("abc");
- : string = "abc"

10.16.2 Example: first()

Let’s look another example: a generic function first for accessing the first component of a pair (a 2-tuple).

# let first = ((x, y)) => x;
let first: (('a, 'b)) => 'a = <fun>;

first uses destructuring to access the first component of that tuple. Type inference tells us that the return type is the same as the type of the first component.

We can use an underscore to indicate that we are not interested in the second component:

# let first = ((x, _)) => x;
let first: (('a, 'b)) => 'a = <fun>;

With a type-annotated component, first looks as follows:

# let first = ((x: 'a, _)) => x;
let first: (('a, 'b)) => 'a = <fun>;

10.16.3 Example: ListLabels.map()

As a quick preview, I’m showing the signature of another function that I explain properly in the chapter on lists.

ListLabels.map: (~f: ('a) => 'b, list('a)) => list('b)

10.16.4 Overloading vs. parametric polymorphism

Note how overloading and parametric polymorphism are different:

10.17 ReasonML does not support variadic functions

ReasonML does not support variadic functions (varargs). That is, you can’t define a function that computes the sum of an arbitrary number of parameters:

let sum = (x0, ···, xn) => x0 + ··· + xn;

Instead, you are forced to define one function for each arity:

let sum2(a: int, b: int) = a + b;
let sum3(a: int, b: int, c: int) = a + b + c;
let sum4(a: int, b: int, c: int, d: int) = a + b + c + d;

You have seen a similar technique with currying, where we couldn’t define a variadic function curry() and had to go with a binary curry2(), instead. You’ll occasionally see it in libraries, too.

An alternative to this technique is to use lists of ints.