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:

  • As listas são uma estrutura de dados imutável para sequências de elementos que possuem o mesmo tipo. É especialmente utilizada para ser processada por pattern matching.
  • Array são uma estrutura de dados mutável com acesso aleatório da qual todos os elementos possuem o mesmo tipo. É utilizada para grandes quantidades de dados e sempre que você precisar de acesso aleatório.

A tabela a seguir compara as duas estruturas de dados.

1. Breve introdução: Assinaturas de Tipo

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

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:

  • let map: («parameters») => «result»;, map é uma função.
  • ’a => ‘b, primeiro parâmetro é uma função do tipo ‘a para tipo ‘b., o apóstrofo inicial indica que ‘a e ‘b são variáveis ​​de tipo — eles aceitam qualquer tipo. Mas, uma vez que uma variável tenha aceitado um tipo particular, ela aceita apenas aquele tipo.
  • list(‘a), o segundo parâmetro é uma lista cujos elementos são do tipo ‘a.
  • list(‘b), o resultado é uma lista cujos elementos são do tipo ‘b.

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

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

  • List com funções como:
  • Array com funções como:

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

Você pode usar um truque e “substituir” os módulos normais por suas versões rotuladas abrindo o módulo 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:

Os nomes Nil e Cons(

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

  • Primeiro, os elementos de mylist podem ter qualquer tipo.
  • Em segundo lugar, todos eles devem ter o mesmo tipo.

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:

  • Head (ou first, primeiro): é o primeiro valor da lista atual.
  • Tail (ou rest, restante): aponta para a outra lista com os elementos restantes.

Criando e padronizando listas em ReasonML .

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

  • Uma lista vazia []
  • Um par de Const com [head, …tail]

Funcionando assim:

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

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

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

Recapitulando, os dois construtores:

  • Uma lista vazia com o comprimento de 0
  • Uma lista não vazia que tem seu comprimento baseado em 1 + o tamanho de tail

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:

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:

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:

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:

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

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

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

E você usa append dessa maneira:

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

  • ReasonML irá inferir os tipos de 1, 2 etc como int
  • Em seguida, irá inferir os tipos de, por exemplo, a primeira lista ser list(int):
  • Na etapa seguinte, verifica se l1 e l2 tem o mesmo valor do parâmetro tipado ‘a
  • Por último, irá utilizar o tipo de dado inferido para l1 e l2 (list(int)), como resultado do append

3.2 Exemplos: criando listas

Usaremos range() para criar uma lista de int:

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():

E o fill() cria uma lista preenchida com valor ~element:

ReasonML usa o tipo de ~element para inferir o tipo do resultado:

3.3 Exemplos: lendo listas

Calculando a soma de uma lista

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

Acessando o n-ésimo elemento de uma lista

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

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

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

  • A falha é tratada por meio do tipo de variante option:
  • None significa uma falha
  • Some(x) significa que temos um resultado x
  • O índice 0 significa o resultado atual (head), segundo caso
  • Ao chegar no final da lista, [], temos uma falha. O primeiro caso do switch é acionado se uma das duas l estiver vazio ou se chegarmos ao final antes de ~index ser 0.

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:

  • Algoritmos que lêem dados recursivamente. Por exemplo: len(), summarize(), etc.
  • Algoritmos que criam listas novas recursivamente. Por exemplo: range(), fill(), etc.

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:

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:

replaceAll()

Com replaceAll(), iremos substituir valores:

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():

E replaceAll() em ação:

drop()

O drop() remove elementos da lista:

Utilizando drop():

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:

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.

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:

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

E utilizando setElementAt():

4.2 ListLabels.filter()

Esta é a assinatura da função:

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:

4.3 ListLabels.for_all()

Assinatura:

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

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:

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:

É assim que flatten() é usado:

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:

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:

  • Primeiro, filtrar as listas vazias (que não possuem primeiros elementos) com ListLabels.filter().
  • Em seguida, mapear cada lista não vazia e retornar o primeiro elemento (_head) com ListLabels.map().

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

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:

Essas etapas são equivalentes a:

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

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

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:

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

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

Agora nós temos o resultado desejado:

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

E usamos func() da seguinte maneira:

4.5 ListLabels.fold_left()

Assinatura:

fold_left() funciona da seguinte maneira:

  • Entrada: uma lista de tipo list(‘b) (último parâmetro)
  • Resultado: um valor do tipo ‘a

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:

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():

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

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

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:

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:

4.6 Convertendo listas para array via iteri()

Assinatura:

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:

~init é um parâmetro obrigatório, porque make() precisa dele (o por que é explicado mais tarde).

arrayFromList() em ação:

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:

  • Arrays são mutáveis. Listas são imutáveis.
  • Arrays não podem ser redimensionados. Com uma lista, você pode adicionar elementos e recuperá-los de forma eficiente. (O BuckleScript permite redimensionar arrays, mas você perderá a compatibilidade entre plataformas se o fizer.)
  • Arrays fornecem acesso rápido indexado. As listas são processadas por meio de recursão e correspondência de padrões (pattern matching).

5.1 Criando arrays

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

Arrays literais

ArrayLabels.make()

Assinatura:

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.init()

Assinatura:

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

5.2 Obtendo o comprimento de um array

ListLabels.length() retorna o tamanho do array:

5.3 Lendo e escrevendo elementos no array

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

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

Vamos desestruturar um array a seguir:

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

5.5 Conversão entre listas e arrays

É assim que você converte entre listas e arrays:

  • De array para lista (módulo ArrayLabels):
  • De lista para array (módulo ArrayLabels):

À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.fold_left()

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

É assim que maxOfArray() é usado:

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:

min_int é o valor int mais baixo possível (seu valor exato depende da plataforma que você está usando):

Convertendo arrays para listas via fold_right()

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

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

A função é assim:

E listFromArray() em ação:

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:

filterArray() em uso:

⭐️ Créditos

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store