ReasonML: lists and arrays

[2018-01-02] dev, reasonml
(Ad, please don’t block)

Table of contents for this series of posts: “What is ReasonML?


In this blog post, we look at two ReasonML data structures – lists and arrays:

  • Lists are an immutable data structure for sequences of elements that all have the same type. It is especially well-suited for being processed via pattern matching.
  • Arrays are a mutable data structure with random access whose elements all have the same type. It is especially well suited for large amounts of data and whenever you need random access.

The following table compares the two data structures.

Lists Arrays
Size small–medium small–large
Resizable? flexible fixed
Mutability immutable mutable
Elem types same same
Access via destructuring index
Fastest prepend/remove first read/write elems

Brief excursion: type signatures  

In this blog post, you’ll see many type signatures such as this one:

let map: ('a => 'b, list('a)) => list('b);

Type signatures may still seem cryptic to you. But you’ll profit from learning how to read them. Let’s explore what the type signature tells us about map – without us knowing what map is or does (which will be explained later). Insights:

  • let map: («parameters») => «result»;
    map is a function.
  • 'a => 'b
    map’s first parameter is a function from type 'a to type 'b. The leading apostrophe indicates that 'a and 'b are type variables: They accept any types. But once a variable has accepted a particular type, it henceforth only accepts that type.
  • list('a)
    map’s second parameter is a list whose elements are of type 'a.
  • list('b)
    map’s result is a list whose elements are of type 'b.

Standard library functionality for lists and arrays  

ReasonML currently has two standard modules for lists and arrays:

  • List with functions such as:
    let map: ('a => 'b, list('a)) => list('b);
    
  • Array with functions such as:
    let map: ('a => 'b, array('a)) => array('b);
    

Each function in those modules is also available in a labeled version, via the following two modules:

  • ListLabels with functions such as:
    let map: (~f: 'a => 'b, list('a)) => list('b);
    
  • ArrayLabels with functions such as:
    let map: (~f: 'a => 'b, array('a)) => array('b);
    

You can use a trick and “replace” the normal modules with their labeled versions by opening the module StdLabels:

open StdLabels;

This module has the submodules Array, Bytes, List and String which are aliases for the global ArrayLabels etc. Therefore, if you open it, you override the current implementations of Array etc.

Lists  

Lists in ReasonML are a typically functional data structure in that their type is defined recursively and that they are immutable. Let us explore what that means.

The structure of lists  

Lists via a self-recursive parameterized variant  

list is a self-recursive parameterized variant. If you were to define it yourself, this is what it would look like:

type mylist('a) =
  | Nil;
  | Cons('a, mylist('a))

The names Nil and Cons (“construct”) are historical and originated with the Lisp programming language. You nest Cons to create lists:

# let abc = Cons("a", Cons("b", Cons("c", Nil)));
let abc: mylist(string) = Cons("a", Cons("b", Cons("c", Nil)));

mylist has the type parameter 'a. It is passed on to Cons and its recursive use of mylist. That means two things: First, the elements of mylist can have any type. Second, they must all have the same type. In the previous interaction with ReasonML, you can see that it automatically inferred that for abc, 'a is string.

abc is a singly-linked list. In memory, it may look like this:

The two parts of a cons pair are called:

  • Head (or first): It is the first value of the current list.
  • Tail (or rest): It points to another list with the remaining elements.

Creating and pattern-matching ReasonML’s lists  

ReasonML has special syntax for lists. The two constructors are:

  • The empty list [].
  • The cons pair [head, ...tail].

Therefore, pattern matching works like this:

switch myList {
| [] => ···
| [head, ...tail] => ···
}

This is one way of recreating the list abc from the previous section:

# let abc = ["a", ...["b", ...["c", ...[]]]];
let abc: list(string) = ["a", "b", "c"];

You can see that rtop suggests the following, more compact, syntax, which is equivalent:

# let abc = ["a", "b", "c"];
let abc: list(string) = ["a", "b", "c"];

Let’s use pattern matching to compute the length of any list myList:

let rec len = (myList: list('a)) =>
  switch myList {
  | [] => 0
  | [_, ...tail] => 1 + len(tail)
  };

We recurse over the two constructors:

  • An empty list has length 0.
  • A non-empty list has this length: 1 plus the length of the tail.

The type parameter 'a makes function len generic. But we are never interested in the type of the elements, only in the structure of the list. The following interaction uses len with various lists:

# len([]);
- : int = 0
# len(["a", "b"]);
- : int = 2
# len([1, 2, 3, 4]);
- : int = 4

Printing lists  

ReasonML has no built-in support for printing complex data structures. But BuckleScript lets you use JavaScript’s console.log(). It’s best to use this function like this:

Js.log(Array.of_list(myList));

Before we print the list myList, we convert it to an array. But why? It leads to nicer output, because ReasonML lists are represented as nested 2-element arrays in JavaScript (an encoding of cons pairs).

More ways of creating lists  

The triple dot constructor is also called spread operator. This operator lets you prepend zero or more elements before an existing list:

# [...["a", "b"]];
- : list(string) = ["a", "b"]
# ["a", ...["b", "c"]];
- : list(string) = ["a", "b", "c"]
# ["a", "b", ...["c", "d"]];
- : list(string) = ["a", "b", "c", "d"]

Alas, this operator only works at the end. In JavaScript, you can use it anywhere, but in ReasonML, you can’t:

# [...["a", "b"], ...["c", "d"]];
Error: Syntax error

ReasonML has its own operator for concatenating lists:

# ["a", "b"] @ ["c", "d"];
- : list(string) = ["a", "b", "c", "d"]

Note that concatenating lists is comparatively slow, because you must prepend each element of the first operand to the second operand:

let rec append = (l1: list('a), l2: list('a)) =>
  switch l1 {
  | [] => l2
  | [head, ...tail] => [head, ...append(tail, l2)]
  };

(This implementation can be improved. We’ll see how in an upcoming blog post.)

This is how you use append:

# append([1,2,3], [4,5]);
- : list(int) = [1, 2, 3, 4, 5]

This is, once again, type inference at work:

  • First, ReasonML inferred the types of 1 etc. to be int.
  • Then it inferred the type of, e.g., the first input list to be list(int):
    # [1,2,3];
    - : list(int) = [1, 2, 3]
    
  • Then it checked that l1 and l2 had the same value for the type parameter 'a.
  • Lastly, it used the type data of l1 and l2 to infer the type list(int) of the result of append.

Examples: creating lists  

range() creates a list of ints:

/**
 * Compute a list of integers starting with `start`,
 * up to and excluding `end_`.
 */
let rec range = (start: int, end_: int) =>
  if (start >= end_) {
    [];
  } else {
    [start, ...range(start + 1, end_)];
  };

end is a keyword in ReasonML and therefore not a legal variable name. That’s why the parameter end_ has the underscore in its name. Let’s try out range():

# range(0, 0);
- : list(int) = []
# range(0, 1);
- : list(int) = [0]
# range(0, 5);
- : list(int) = [0, 1, 2, 3, 4]

fill() creates a list filled with the value ~element:

/**
 * Create a list of length `~length` where each
 * element is `~element`.
 */
let rec fill = (~element: 'a, ~length: int) =>
  if (length <= 0) {
    [];
  } else {
    [element, ...fill(~element, ~length=length-1)];
  };

ReasonML uses the type of ~element to infer the type of the result:

# fill("x", 4);
- : list(string) = ["x", "x", "x", "x"]
# fill(0, 3);
- : list(int) = [0, 0, 0]

Examples: reading lists  

Computing the sum of a list  

summarize() computes the total of all the ints in a list:

/**
 * Compute the sum of all the ints in the list `l`.
 */
let rec summarize = (l: list(int)) =>
  switch l {
  | [] => 0
  | [head, ...tail] => head + summarize(tail)
  };

summarize([]); /* 0 */
summarize([3]); /* 3 */
summarize([1, 2, 3]); /* 6 */

Accessing the n-th list element  

getElementAt() retrieves a list element by index:

/**
 * Get the list element at index `~index`.
 * The head of a list has the index 0,
 * the head of its tail the index 1, etc.
 */
let rec getElementAt = (~index: int, l: list('a)) =>
  switch l {
  | [] => None
  | [head, ...tail] =>
    if (index <= 0) {
      Some(head);
    } else {
      getElementAt(~index=index-1, tail);
    }
  };

We can eliminate the if-then-else expression if we use a when clause and an additional case for switch. The resulting flat structure is slightly easier to read:

let rec getElementAt = (~index: int, l: list('a)) =>
  switch l {
  | [] => None
  | [head, ..._] when index <= 0 => Some(head)
  | [head, ...tail] => getElementAt(~index=index-1, tail)
  };

A few things are noteworthy in this code:

  • Failure is handled via the variant type option:
    • None means we have failed.
    • Some(x) means we have succeeded, with the result x.
  • Index 0 refers to the current head (2nd case).
  • If we reach the end of the list [], we have failed. This first case of switch is triggered if either l is empty or if we reach its end before ~index is 0.

The standard library has ListLabels.nth() that works like getElementAt(), but it throws an exception for illegal indices, it does not use option.

Examples: changing lists  

Given that lists are immutable – how do you change them? To find an answer, consider that, so far, we have seen two kinds of algorithms:

  • Algorithms that read data from lists by recursing over their structures. For example: len(), summarize(), etc.
  • Algorithms that created lists by recursively building new structures. For example: range(), fill(), etc.

To change a list, we combine both approaches: We create a completely new list and include data from the existing list or derive data from it, as needed.

removeAll()  

The following is a first example of a function that changes an existing list.

/**
 * Remove all elements from the list `l` that are
 * equal to `~value`.
 */
let rec removeAll = (~value: 'a, l: list('a)) =>
  switch l {
  | [] => []
  | [head, ...tail] when head == value => removeAll(~value, tail)
  | [head, ...tail] => [head, ...removeAll(~value, tail)]
  };

The first case means that we are done. The third case makes an exact copy of the existing list. The second case removes elements that are equal to ~value.

This is removeAll() in action:

# removeAll(~value=9, [1,9,2,9,3]);
- : list(int) = [1, 2, 3]

replaceAll()  

replaceAll() replaces values:

/**
 * Inside the list `l`, remove all occurrences of the value `~value`
 * with the value `~with_`.
 */
let rec replaceAll = (~value: 'a, ~with_: 'a, l: list('a)) =>
  switch l {
  | [] => []
  | [head, ...tail] when head == value =>
    [with_, ...replaceAll(~value, ~with_, tail)]
  | [head, ...tail] =>
    [head, ...replaceAll(~value, ~with_, tail)]
  };

The first case means we are finished. The third case makes an exact copy. The second case makes a replacement.

We can make replaceAll() more compact via an internal helper function replaceOne():

let rec replaceAll = (~value: 'a, ~with_: 'a, l: list('a)) => {
  let replaceOne = (x) => if (x == value) with_ else x;
  switch l {
  | [] => []
  | [head, ...tail] =>
    [replaceOne(head), ...replaceAll(~value, ~with_, tail)]
  };
};

This is replaceAll() in action:

# replaceAll(~value=1, ~with_=11, [1, 2, 1, 3]);
- : list(int) = [11, 2, 11, 3]

drop()  

drop() removes list elements:

/**
 * Remove the first `~count` elements of `theList`.
 */
let rec drop = (~count, theList: list('a)) =>
  switch theList {
  | [] => []
  | l when count <= 0 => l
  | [_, ...tail] => drop(~count=count-1, tail)
  };

Let’s use drop():

# drop(~count=0, ["a", "b", "c", "d"]);
- : list(string) = ["a", "b", "c", "d"]
# drop(~count=2, ["a", "b", "c", "d"]);
- : list(string) = ["c", "d"]
# drop(~count=2, ["a", "b"]);
- : list(string) = []
# drop(~count=2, ["a"]);
- : list(string) = []
# drop(~count=2, []);
- : list('a) = []

For the last result of drop(), ReasonML can’t infer the element type and leaves the type parameter 'a unbound.

Standard library functions for lists  

ReasonML’s standard library is still in flux. Therefore, We are only looking at a few highlights here. You can read up on the rest (as it currently exists) via the documentation for ListLabels.

ListLabels.map()  

Signature:

let map: (~f: 'a => 'b, list('a)) => list('b);

map() takes a list with elements of type 'a, applies the function ~f to each element and returns the results in another list.

# ListLabels.map(~f=x => int_of_string(x), ["7", "15", "6"]);
- : list(int) = [7, 15, 6]

This function is a classic tool for processing lists of data.

mapi() is a version of map() that passes both the current element and the element’s index to the callback ~f. We can use mapi() to non-destructively update lists:

/**
 * Create a copy of `theList` whose element at index `~index`
 * is `~value`.
 */
let setElementAt = (~index: int, ~value: 'a, theList: list('a)) =>
  ListLabels.mapi(
    ~f=(i,x) => if (i==index) value else x,
    theList
  );

The parameter ~f passes on all elements unchanged, except for the element at index ~index.

This is setElementAt() in use:

# setElementAt(~index=1, ~value="|", ["a", "b", "c"]);
- : list(string) = ["a", "|", "c"]

ListLabels.filter()  

This is the function’s signature:

let filter: (~f: 'a => bool, list('a)) => list('a);

filter() applies the function ~f to each element of its positional parameter. If it returns true, the element is included in the result. If it returns false, it isn’t. It is used as follows.

# ListLabels.filter(~f=x=>x>5, [8, 4, 9, 7, 2]);
- : list(int) = [8, 9, 7]

ListLabels.for_all()  

Signature:

let for_all: (~f: 'a => bool, list('a)) => bool;

for_all() returns true if ~f returns true for each element of the list. For example:

# ListLabels.for_all(~f=x=>x>3, [4,5,6]);
- : bool = true
# ListLabels.for_all(~f=x=>x>3, [3,4,5,6]);
- : bool = false

for_all stops processing as soon as ~f returns false. Then the result is guaranteed to be false. for_all is named after the mathematical operator ∀.

ListLabels.exists() is related to for_all(): It returns true if its callbacks returns true for at least one of the elements of its list. exists is named after the mathematical operator ∃.

ListLabels.flatten()  

Signature:

let flatten: list(list('a)) => list('a);

flatten() converts a list of lists l, to a list, by concatenating the elements of l. That is, the following three expressions are equivalent:

flatten([l1, l2, l3])
ListLabels.append(l1, ListLabels.append(l2, l3))
l1 @ l2 @ l3

This is how flatten() is used:

# ListLabels.flatten([[1,2], [], [3,4,5]]);
- : list(int) = [1, 2, 3, 4, 5]

If you are wondering about arbitrarily nested lists, recall that, in ReasonML, all elements must have the same type. Therefore, if one list element is itself a list then all elements must be lists:

# ListLabels.flatten([[1,[2]], [], [3]]);
Error: This expression has type list('a)
but an expression was expected of type int
# ListLabels.flatten([[[1],[2]], [], [[3]]]);
- : list(list(int)) = [[1], [2], [3]]

Let’s continue by looking at use cases for flatten().

Use case: filtering and mapping at the same time  

flatten() lets you filter and map at the same time. As an example, consider the trying to extract the first elements of several lists stored in a list. You could:

  • First filter out empty lists (which don’t have first elements) via ListLabels.filter().
  • Then map each non-empty list to its head via ListLabels.map().

Or you could use flatten and do both at the same time:

module L = ListLabels;
let listFromHead = (l: list('a)) =>
  switch (l) {
  | [] => []
  | [head, ..._] => [head]
  };
let heads = (l: list(list('a))) =>
  L.flatten(L.map(~f=listFromHead, l));

First, we map each non-empty list to a list with its head and each empty list to an empty list. Then we flatten the result. This looks as follows:

# let l0 = [[1, 2], [], [3,4,5]];
let l0: list(list(int)) = [[1, 2], [], [3, 4, 5]];
# L.map(~f=listFromHead, l0);
- : list(list(int)) = [[1], [], [3]]
# let l1 = L.map(~f=listFromHead, l0);
let l1: list(list(int)) = [[1], [], [3]];
# L.flatten(l1);
- : list(int) = [1, 3]

These steps are equivalent to:

# heads([[1, 2], [], [3,4,5]]);
- : list(int) = [1, 3]

It is instructive to compare listFromHead to a function getHead that uses option to express failure:

let getHead = (l: list('a)) =>
  switch (l) {
  | [] => None
  | [head, ..._] => Some(head)
  };

That is, None expresses “l does not have a head”:

# getHead(["a", "b"]);
- : option(string) = Some("a")
# getHead([1, 2, 3]);
- : option(int) = Some(1)
# getHead([]);
- : option('a) = None

With listFromHead, we used the empty list instead of None and a one-element list instead of Some.

Use case: mapping to multiple values  

Let’s assume that we have created a list of people and their children:

type person = Person(string, list(string));
let persons = [
  Person("Daisy", []),
  Person("Della", ["Huey", "Dewey", "Louie"]),
  Person("Marcus", ["Minnie"])
];

If we want to collect the children in a list, ListLabels.map() almost gives us what we want, but not quite:

# ListLabels.map(~f=(Person(_, ch)) => ch, persons);
- : list(list(string)) = [[], ["Huey", "Dewey", "Louie"], ["Minnie"]]

Alas, this is a list of lists of strings, not a list of strings. We can fix this by applying ListLabels.flatten() to this nested list:

let collectChildren = (persons: list(person)) =>
  ListLabels.flatten(
    ListLabels.map(
        ~f=(Person(_, children)) => children,
        persons));

collectChildren(persons);
  /* ["Huey", "Dewey", "Louie", "Minnie"] */

Now we get the desired result:

# collectChildren(persons);
- : list(string) = ["Huey", "Dewey", "Louie", "Minnie"]

Use case: conditionally inserting values  

Sometimes, you create lists where some elements are added or omitted depending on a condition (cond in the following example):

let func = (cond: bool) => ListLabels.flatten([
  if (cond) ["a"] else [],
  [
    "b",
    "c"
  ]
]);

This is how func() is used:

# func(true);
- : list(string) = ["a", "b", "c"]
# func(false);
- : list(string) = ["b", "c"]

ListLabels.fold_left()  

Signature:

let fold_left: (~f: ('a, 'b) => 'a, ~init: 'a, list('b)) => 'a;

fold_left() works as follows:

  • Input: a list of type list('b) (last parameter)
  • Result: a value of type 'a

To compute the result, fold_left() depends on its parameter, the function ~f. It calls ~f for each element elem of its input list:

let nextIntermediateResult = f(intermediateResult, elem);

intermediateResult is what has already been computed. The first intermediate result is ~init. The last nextIntermediateResult is the result of fold_left().

Let’s look at a concrete example.

fold_left() by example: summarize()  

We have already encountered the function summarize() which computes the total of a list of ints. Let’s implement that function via fold_left():

let rec summarize = (l: list(int)) =>
  ListLabels.fold_left(~f=(r, elem) => r + elem, ~init=0, l);

To understand how summarize() works, consider the following expression:

summarize([1,2,3]) /* 6 */

To compute the result 6, the following steps are taken:

/* Parameter */
let f = (r, elem) => r + elem;
let init = 0;

/* Steps */
let result0 = f(init, 1); /* 1 */
let result1 = f(result0, 2); /* 3 */
let result2 = f(result1, 3); /* 6 */

result2 is the result of fold_left().

Another way of looking at fold_left()  

Another way of looking at fold_left() is that takes a binary operator ~f and turns it into an n-ary operator for lists. An example in mathematics for going from binary to n-ary is the binary operator + also existing as an n-ary version (the operator Σ). summarize() went from + to Σ. It could also be written like this:

# ListLabels.fold_left(~f=(+), ~init=0, [1, 2, 3]);
- : int = 6

I find fold_left easiest to understand if it works in this mode – with an ~f that is commutative (order of parameters doesn’t matter). But there is much you can do with it – read on for an example.

A more complex example: finding values  

The function contains() uses it to find a value in a list:

let contains = (~value: 'a, theList: list('a)) => {
  let f = (found, elem) => found || elem == value;
  fold_left(~f, ~init=false, theList);
};

Converting lists to arrays via iteri()  

Signature:

let iteri: (~f: (int, 'a) => unit, list('a)) => unit;

iteri() call ~f for every element of its list. The arguments are the index of the element and the element. It returns unit, which means that anything useful that iteri does, it does via side effects.

The following function uses iteri() to fill an array. It does so as a side effect, by writing to an array arr:

let arrayFromList = (~init: 'a, l: list('a)) => {
  let arr = ArrayLabels.make(ListLabels.length(l), init);
  ListLabels.iteri(~f=(i, x) => arr[i]=x, l);
  arr;
};

~init is a required parameter, because make() needs it (why is explained later).

arrayFromList() in action:

# arrayFromList(~init=0, [1,2,3]);
- : array(int) = [|1, 2, 3|]

Arrays  

Arrays are much like lists: all of their elements have the same type and they are accessed by position. But they are also different:

  • Arrays are mutable. Lists are immutable.
  • Arrays can’t be resized. With a list, you can efficiently prepend elements and retrieve tails. (BuckleScript lets you resize arrays, but you lose cross-platform compatibility if you do so.)
  • Arrays provide fast indexed access. Lists are processed via recursion and pattern matching.

Creating arrays  

The following subsections explain three common ways of creating arrays.

Array literals  

# [| "a", "b", "c" |];
- : array(string) = [|"a", "b", "c"|]

ArrayLabels.make()  

Signature:

let make: (int, 'a) => array('a);

The first parameter specifies the length of the result. The second parameter specifies the value it is to be filled with. Why is the second parameter mandatory? The result of make() must only contain values of type 'a. ReasonML has no null, so you must pick a member of type 'a, manually.

This is how make() works:

# ArrayLabels.make(3, "x");
- : array(string) = [|"x", "x", "x"|]
# ArrayLabels.make(3, true);
- : array(bool) = [|true, true, true|]

ArrayLabels.init()  

Signature:

let init: (int, ~f: int => 'a) => array('a);

The first parameter specifies the length of the result. The function ~f maps an index to an initial value at that index. For example:

# ArrayLabels.init(~f=i=>i, 3);
- : array(int) = [|0, 1, 2|]
# ArrayLabels.init(~f=i=>"abc".[i], 3);
- : array(char) = [|'a', 'b', 'c'|]

Getting the length of an array  

ListLabels.length() returns the length of an array:

# ArrayLabels.length([| "a", "b", "c" |]);
- : int = 3

Reading and writing array elements  

This is how you read and write array elements:

# let arr = [| "a", "b", "c" |];
let arr: array(string) = [|"a", "b", "c"|];
# arr[1]; /* read */
- : string = "b"
# arr[1] = "x"; /* write */
- : unit = ()
# arr;
- : array(string) = [|"a", "x", "c"|]

Pattern matching and arrays  

Pattern-matching arrays is similar to matching tuples, not to matching lists. Let’s start with tuples and lists (we can ignore the exhaustiveness warnings, because we are working with fixed data):

# let (a, b) = (1, 2);
let a: int = 1;
let b: int = 2;
# let [a, ...b] = [1, 2, 3];
Warning: this pattern-matching is not exhaustive.
let a: int = 1;
let b: list(int) = [2, 3];

We’ll destructure an array next:

# let [| a, b |] = [| 1, 2 |];
Warning: this pattern-matching is not exhaustive.
let a: int = 1;
let b: int = 2;

Similar to tuples, the pattern must have the same length as the data (that’s what the exception is about):

# let [| a, b |] = [| 1, 2, 3 |];
Warning: this pattern-matching is not exhaustive.
Exception: Match_failure

Converting between lists and arrays  

This is how you convert between lists and arrays:

  • From array to list (module ArrayLabels):
    let to_list: array('a) => list('a);
    
  • From list to array (module ArrayLabels):
    let of_list: list('a) => array('a);
    

Sometimes you have data in an array that would be easier to process in a list. Then you can convert it to a list (and convert it back to an array afterwards, should that be needed).

Processing arrays  

The standard library is still in flux. Therefore, I’ll only demonstrate a few highlights for now.

ArrayLabels.map()  

map() for arrays works similar to the same function for lists:

# ArrayLabels.map(s => s ++ "x", [| "a", "b" |]);
- : array(string) = [|"ax", "bx"|]

ArrayLabels.fold_left()  

fold_left() is also similar to its list version:

let maxOfArray = (arr) =>
  ArrayLabels.fold_left(~f=max, ~init=min_int, arr);

This is how maxOfArray() is used:

# maxOfArray([||]);
- : int = -4611686018427387904
# maxOfArray([|3, -1, 5|]);
- : int = 5

Once again, we have used fold to go from a binary operation (max()) to an n-ary operation (maxOfArray). In addition to max(), we also use the integer constant min_int. Both are part of module Pervasives and therefore available without qualification.

max is a binary function that works for most types:

# max(1.0, 1.1);
- : float = 1.1
# max(None, Some(1));
- : option(int) = Some(1)
# max("a", "b");
- : string = "b"
# max(4, -3);
- : int = 4

min_int is the lowest possible int value (its exact value depends on the platform that you are using):

# min_int;
- : int = -4611686018427387904

Converting arrays to lists via fold_right()  

fold_right() works like fold_left(), but it starts with the last element. Its type signature is:

let fold_right: (~f: ('b, 'a) => 'a, array('b), ~init: 'a) => 'a;

One use case for this function is converting an array to a list. That list has to be constructed as follows (i.e., you have to start with the last array element):

[··· [x_2nd_last, ...[x_last, ...[]]]]

The function looks like this:

let listFromArray = (arr: array('a)) =>
  ArrayLabels.fold_right(~f=(ele, l) => [ele, ...l], arr, ~init=[]);

This is listFromArray() in action:

# listFromArray([||]);
- : list('a) = []
# listFromArray([| 1, 2, 3 |]);
- : list(int) = [1, 2, 3]
# listFromArray([| "a", "b", "c" |]);
- : list(string) = ["a", "b", "c"]

Filtering arrays  

All array functions return arrays that have the same length as the input arrays. Therefore, if you want to remove elements, you have to take a detour via lists:

let filterArray = (~f, arr) =>
  arr
  |> ArrayLabels.to_list
  |> ListLabels.filter(~f)
  |> ArrayLabels.of_list;

filterArray() in use:

# filterArray(~f=x=>x>0, [|-2, 3, -4, 1|]);
- : array(int) = [|3, 1|]