ReasonML: Lists e Arrays

Manipulando, alterando e entendo suas estruturas em ReasonML

Esse artigo faz parte da série “O que é ReasonML?”.

Neste artigo, vamos analisar duas estruturas de dados do ReasonML — listas e arrays:

A tabela a seguir compara as duas estruturas de dados.

Qual estrutura de dados é mais adequada para seu problema?

1. Breve introdução: Assinaturas de Tipo

Nesse artigo, você verá muitas assinaturas de tipos como esta:

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

Assinaturas de tipo podem parecer enigmáticas para você. Mas você vai ganhar muito aprendendo a entendê-las. Vamos explorar o que o tipo de assinatura nos diz sobre o método map — sem sabermos o que map faz (que será explicado mais adiante). Dicas:

2. Funcionalidades da biblioteca padrão para Listas e Arrays

O ReasonML atualmente possui dois módulos padrões para listas e array:

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

Cada função nesses módulos também está disponível em uma versão rotulada, através dos dois módulos a seguir:

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

Você pode usar um truque e “substituir” os módulos normais por suas versões rotuladas abrindo o módulo StdLabels:

open StdLabels;

Esse módulo tem os submódulos Array, Bytes, List e String que são atalhos para as declarações globais ArrayLabels, etc. Portanto, se você abri-lo, você vai substituir as implementações atuais de Array, etc.

3. Listas

As listas no ReasonML são uma estrutura de dados tipicamente funcional, em que seu tipo é definido recursivamente e que são imutáveis. Vamos explorar o que isso significa.

3.1 A estrutura de uma lista

Listas através de uma variante parametrizada auto-recursiva.

list é uma variante parametrizada auto-recursiva. Se você fosse definir por si mesmo, seria algo assim:

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

Os nomes Nil e Cons(

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

mylist tem o parâmetro de tipo ‘a. Ele é passado para Cons e seu uso recursivo de mylist. Isso significa duas coisas:

No exemplo anterior, você pode ver que ele automaticamente infere que para abc, o ‘a é string.

abc é uma lista ligada individualmente, singly-linked list. Em memória, pode se parecer com isso:

As duas partes de um par de Cons são chamadas:

Criando e padronizando listas em ReasonML .

ReasonML tem sintaxe especial para listas. Os dois construtores são:

Funcionando assim:

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

Abaixo, uma maneira de recriar o abc do exemplo anterior:

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

Você pode ver que o rtop sugere uma sintaxe mais compacta que são equivalentes:

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

Vamos usar pattern maching para calcular o tamanho de qualquer lista do myList:

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

Recapitulando, os dois construtores:

O parâmetro tipado ‘a transforma a função len em genérica. Mas nunca estivemos interessados nos tipos dos elementos, apenas na estrutura da lista. A interação a seguir usa len em várias listas:

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

Imprimindo uma Lista

O ReasonML não possui suporte nativo para imprimir estruturas de dados complexas. Mas o BuckleScript permite que você use console.log() do JavaScript:

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

Antes de imprimir a lista myList, nós estamos convertendo para um array. Por que? Isso irá imprimir o resultado de uma maneira mais agradável, listas em ReasonML são representadas como matrizes aninhadas de dois elementos em JavaScript (pares de cons).

Mais maneiras de criar listas

O “construtor de três pontos”, que também é chamado de operador de espalhamento, spread operator, permite que você preencha zero ou mais elementos antes de uma lista existente:

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

Infelizmente, esse operador só funciona no final de uma lista. em JavaScript, você pode usá-lo em qualquer lugar, mas no ReasonML você não pode:

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

O ReasonML tem seu próprio operador para concatenar listas:

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

Observe que a concatenação de listas é relativamente lenta, porque você deve preceder cada elemento da primeira lista para a segunda:

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

(Essa implementação pode ser melhorada. Veremos como em um artigo futuro.)

E você usa append dessa maneira:

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

Isto é, mais uma vez, inferência de tipos:

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

3.2 Exemplos: criando listas

Usaremos range() para criar uma lista de int:

/**
* 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_ é uma palavra-chave no ReasonML, portanto, não pode ser usado como nome de variável. Por isso, o parâmetro end_ tem um sublinhado em seu nome. Vamos experimentar com range():

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

E o fill() cria uma lista preenchida com valor ~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 usa o tipo de ~element para inferir o tipo do resultado:

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

3.3 Exemplos: lendo listas

Calculando a soma de uma lista

O exemplo summarize() calcula o total de todos os int de uma lista:

/**
* 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 */

Acessando o n-ésimo elemento de uma lista

O getElementAt() retorna um elemento por índice da lista:

/**
* 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);
}
};

Podemos eliminar if-then-else de expressões se usarmos uma cláusula when combinada com switch. A estrutura resultante é:

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

Algumas coisas são dignas de se notar neste código:

A biblioteca padrão tem ListLabels.nth() que funciona como getElementAt(), mas lança uma exceção para índices ilegais e não utiliza option.

3.4 Exemplos: alterando listas

Dado que as listas são imutáveis ​​- como você as altera? Para encontrar uma resposta, considere que, até agora, vimos dois tipos de algoritmos:

Para alterar uma lista, combinamos as duas abordagens: Criamos uma lista completamente nova e incluímos dados da lista existente ou derivamos os dados dela, conforme necessário.

removeAll()

Um exemplo inicial de uma função que altera uma lista existente:

/**
* 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)]
};

O primeiro caso significa que terminamos. O terceiro caso faz uma cópia exata da lista existente. O segundo caso remove elementos iguais a ~value.

Vamos ver removeAll() em ação:

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

replaceAll()

Com replaceAll(), iremos substituir valores:

/**
* 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)]
};

O primeiro caso significa que terminamos. O terceiro caso faz uma cópia exata. O segundo caso faz uma substituição.

Podemos alterar replaceAll(), deixando-a mais compacta e criar uma função interna 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)]
};
};

E replaceAll() em ação:

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

drop()

O drop() remove elementos da lista:

/**
* 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)
};

Utilizando 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) = []

Para o último exemplo de drop(), o ReasonML não consegue inferir o tipo de elemento e deixa o parâmetro de tipo ‘a desvinculado.

4. Funções da biblioteca padrão para listas

A biblioteca padrão do ReasonML ainda está em constante trabalho. Portanto, estamos apenas olhando para alguns destaques aqui. Você pode ler o resto (no momento dessa escrita) na documentação para ListLabels.

4.1 ListLabels.map()

Assinatura:

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

map() pega uma lista com elementos do tipo ‘a e aplica uma função do tipo ~fa para cada elemento e retorna os resultados em outra lista.

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

Esta função é uma ferramenta clássica para processar listas de dados.

mapi() é uma versão do map() que passa o elemento atual e o índice do elemento para a função passada ~f. Podemos usar mapi() para atualizar listas de uma maneira não destrutiva:

/**
* 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
);

O parâmetro ~f retorna todos os elementos inalterados, exceto o elemento no índice ~index.

E utilizando setElementAt():

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

4.2 ListLabels.filter()

Esta é a assinatura da função:

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

filter() aplica a função ~fa para cada elemento de seu parâmetro posicional. Se retornar true, o elemento será incluído no resultado. Se retornar false, não será. Ele é usado da seguinte maneira:

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

4.3 ListLabels.for_all()

Assinatura:

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

for_all() retorna true se ~f retornar verdadeiro para cada elemento da lista. Por exemplo:

# 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 irá parar seu processamento assim que ~f retornar false. Então, o resultado é garantido para ser false. for_all é nomeado após o operador matemático .

ListLabels.exists() está relacionado a for_all(): Ele retorna true se seus retornos de chamada retornarem truepara pelo menos um dos elementos de sua lista. existsé nomeado após o operador matemático ∃.

4.4 ListLabels.flatten()

Assinatura:

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

flatten() converte uma lista de listas l, para uma lista, concatenando os elementos de l. Ou seja, as três expressões a seguir são equivalentes:

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

É assim que flatten() é usado:

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

Se você estiver se perguntando sobre listas aninhadas, lembre-se que, no ReasonML, todos os elementos devem ter o mesmo tipo. Portanto, se um elemento da lista for ele mesmo uma lista, todos os elementos deverão ser listas:

# 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]]

Vamos continuar examinando casos de uso do flatten().

Caso de uso: filtragem e mapeamento ao mesmo tempo

flatten() permite filtrar e mapear ao mesmo tempo. Como exemplo, considere a tentativa de extrair os primeiros elementos de várias listas armazenadas em uma lista. Você poderia:

Ou você pode usar flatten e fazer as duas coisas ao mesmo tempo:

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));

Primeiro, mapeamos cada lista não vazia para uma lista com o primeiro elemento (head) e cada lista vazia para uma lista vazia. Então nós achatamos o resultado. Isso parece o seguinte:

# 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]

Essas etapas são equivalentes a:

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

É instrutivo comparar listFromHead com uma função getHead que usa option para expressar erros:

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

Ou seja, None significa: “l não tem o primeiro elemento (_head)”:

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

Com listFromHead, usamos a lista vazia ao invés de None, uma lista de um elemento ao invés de Some.

Caso de uso: mapeamento para vários valores

Vamos supor que criamos uma lista de pessoas e seus filhos:

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

Se queremos coletar as crianças em uma lista, ListLabels.map() quase nos dá o que queremos, mas não completamente:

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

Infelizmente, esta é uma lista de listas de strings, não uma lista de strings. Podemos corrigir isso aplicando ListLabels.flatten() a esta lista aninhada:

let collectChildren = (persons: list(person)) =>
ListLabels.flatten(
ListLabels.map(
~f=(Person(_, children)) => children,
persons));
collectChildren(persons);
/* ["Huey", "Dewey", "Louie", "Minnie"] */

Agora nós temos o resultado desejado:

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

Caso de uso: inserir valores condicionalmente

Às vezes, você cria listas em que alguns elementos são adicionados ou omitidos, dependendo de uma condição (cond no exemplo a seguir):

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

E usamos func() da seguinte maneira:

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

4.5 ListLabels.fold_left()

Assinatura:

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

fold_left() funciona da seguinte maneira:

Para calcular o resultado, fold_left() depende do seu parâmetro, a função ~f. Ele chama ~f em cada elemento elem da sua lista de entrada:

let nextIntermediateResult = f(intermediateResult, elem);

intermediateResult é o que já foi calculado. O primeiro resultado intermediário é ~init. O último nextIntermediateResult é o resultado de fold_left().

Vamos dar uma olhada em um exemplo concreto.

fold_left() pelo exemplo: summarize()

Já falamos da função summarize() que calcula o total de uma lista de ints. Vamos implementar essa função via _fold_left():

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

Para entender como summarize() funciona, considere a seguinte expressão:

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

Para calcular o resultado 6, são executados os seguintes passos:

/* 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 é o resultado de fold_left().

Outra maneira de olhar fold_left()

Outra maneira de olhar fold_left() é pegar um operador binário ~fe e transformá-lo em um operador n-ário para listas.

Um exemplo em matemática para ir de binário a n-ário é o operador binário +, também existente como uma versão n-ário (o operador Σ). summarize() foi de + para Σ. Também poderia ser escrito assim:

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

Eu acho fold_left mais fácil de entender neste modo — com um ~f que é comutativo (ordem de parâmetros não importa).

Um exemplo mais complexo: encontrando valores

A função contains() é usada para encontrar um valor em uma lista:

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

4.6 Convertendo listas para array via iteri()

Assinatura:

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

iteri() passa ~f para todos os elementos da sua lista. Os argumentos são o índice do elemento e o elemento. Ele retorna unit, o que significa que qualquer coisa útil que iteri faça, ele faz através de side effects.

A seguinte função usa iteri() para preencher um array. Ele faz isso como um side effect, escrevendo para um array chamado 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 é um parâmetro obrigatório, porque make() precisa dele (o por que é explicado mais tarde).

arrayFromList() em ação:

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

5. Arrays

Arrays são muito parecidos com listas: todos os seus elementos têm o mesmo tipo e são acessados ​​por posição. Mas eles também são diferentes:

5.1 Criando arrays

As subseções a seguir explicam três maneiras comuns de criar arrays.

Arrays literais

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

ArrayLabels.make()

Assinatura:

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

O primeiro parâmetro especifica o tamanho do resultado. O segundo parâmetro especifica o valor a ser preenchido. Por que o segundo parâmetro é obrigatório? O resultado de make() deve conter apenas valores do tipo ‘a. ReasonML não tem null, então você deve escolher um membro do tipo ‘a, manualmente.

Funcionando assim:

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

ArrayLabels.init()

Assinatura:

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

O primeiro parâmetro especifica o tamanho do resultado. A função ~f mapeia um índice para um valor inicial nesse índice. Por exemplo:

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

5.2 Obtendo o comprimento de um array

ListLabels.length() retorna o tamanho do array:

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

5.3 Lendo e escrevendo elementos no array

É assim que você lê e grava elementos de array:

# 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"|]

5.4 Arrays e pattern matching

Arrays e pattern matching são semelhantes às tuplas correspondentes (matching tuples), e não às listas correspondentes (_matching lists).

Vamos começar com tuplas e listas (podemos ignorar os avisos de exaustividade, porque estamos trabalhando com dados fixos):

# 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];

Vamos desestruturar um array a seguir:

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

Semelhante às tuplas, o padrão deve ter o mesmo comprimento que os dados (é a exceção):

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

5.5 Conversão entre listas e arrays

É assim que você converte entre listas e arrays:

let to_list: array('a) => list('a);
let of_list: list('a) => array('a);

Às vezes, você tem dados em uma matriz que seriam mais fáceis de processar em uma lista. Então você pode convertê-los para uma lista (e convertê-lo de volta para um array depois, caso seja necessário).

5.6 Processando Arrays

A biblioteca padrão do ReasonML ainda está em constante trabalho. Portanto, só demonstrarei alguns destaques por enquanto.

ArrayLabels.map()

map() para arrays funciona de forma semelhante à mesma função para listas:

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

ArrayLabels.fold_left()

fold_left() também é semelhante à sua versão da lista:

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

É assim que maxOfArray() é usado:

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

Mais uma vez, usamos fold em uma operação binária (max()) para uma operação n-ário (maxOfArray). Além disso em max(), também usamos a constante inteira min_int. Ambos fazem parte do módulo Pervasives e, portanto, estão disponíveis sem qualificação.

max é uma função binária que funciona para a maioria dos tipos:

# 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 é o valor int mais baixo possível (seu valor exato depende da plataforma que você está usando):

# min_int;
- : int = -4611686018427387904

Convertendo arrays para listas via fold_right()

fold_right() funciona como fold_left(), mas começa com o último elemento. Sua assinatura de tipo é:

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

Um caso de uso para essa função é converter um array em uma lista. Essa lista deve ser construída da seguinte forma (ou seja, você precisa começar com o último elemento do array):

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

A função é assim:

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

E listFromArray() em ação:

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

Filtrando arrays

Todas as funções de array retornam um array que têm o mesmo tamanho que os arrays originais. Portanto, se você deseja remover elementos, é necessário fazer um desvio por meio de listas:

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

filterArray() em uso:

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

⭐️ Créditos