Elixir: coleções, sigils e padrões

Adriano Croco
6 min readSep 26, 2020

--

Olá!

No artigo anterior exploramos como subir o projeto dos Koans e experimentamos com algumas funcionalidades básicas do Elixir.

Vamos continuar?

Os itens que iremos abordar nesse artigo são do 7 até o 12:

lista de features

Keywords Lists: É apenas um açúcar sintático de uma lista de tuplas. Vale mencionar as seguintes características: as chaves precisam ser atoms, ordenadas e podem se repetir. Segue exemplo:

iex> list = [{:a, 1}, {:b, 2}] #declaração de uma lista de 2 tuplas
[a: 1, b: 2]
iex> list == [a: 1, b: 2] #aqui é o açúcar sintático mencionado
true

A recomendação oficial da documentação da linguagem é que esse tipo deve ser usada para passagem de parâmetros opcionais entre funções. Pode ser manipulado pelo módulo Keyword. Todas as operações efetuadas pelos módulos Enum e List também se aplicam a esse tipo. Um detalhe interessante que percebi é que é possível utilizar-se de técnicas de Lazy Evaluation apenas utilizando a biblioteca padrão do módulo Keyword. Achei a forma de se trabalhar com Lazy Evaluation mais simples que em outras linguagens (como C#, por exemplo).

Maps: Nada mais é que o tipo recomendado para tratar de estrutura de dados que estejam no formato chave-valor. Apesar de serem bastante parecidos com tuplas comuns, a sintaxe para declarar um map é um pouco diferente. Alguns detalhes que vale a pena mencionar: maps podem ser usados para representar uma espécie de objeto anônimo, não tem ordenação e qualquer coisa pode ser uma chave:

Exemplo de declaração de um map como se fosse um objeto da classe person

Não vi nada de exótico na biblioteca padrão desse tipo e me pareceu bastante simples de usar.

MapSets: Mais um tipo para manipulação de listas, porém, o diferencial dele é que ele não permite duplicados e mantém a ordenação dos elementos até 32 elementos. Do número 33 em diante, internamente esse tipo se transforma em um Hash Array Mapped Trie (HAMT) e as propriedades de ordenação desaparecem, mais detalhes aqui.

Aqui vale explicar o que diabos é um HAMT: é um array que possui propriedades de um hash table (como chaves únicas para garantir lookups O(1) e suscetível a hash collisions da mesma forma). Além de propriedades de um trie, que é uma estrutura de dados hierárquica que pode ser representada graficamente da seguinte forma:

exemplo de uma trie

Qualquer semelhança com árvores binárias não é mera coincidência, dado que é uma estrutura de dados que parte da mesma ideia fundamental como ponto de partida. Porém, ao invés de nós com números são utilizados caracteres. A título de curiosidade, são tries que permitem algoritmos performáticos de autocomplete no teclado do seu smartphone, por exemplo.

E porque o HAMT é utilizado? Performance, oras. Ele é bem econômico em termos de uso de memória comparado a outras estruturas de dados do tipo tree). Aqui vale uma observação empírica que percebi: a maioria das estruturas de dados básicas (como stacks, tree, queues e afins), tem alguns problemas estruturais que acabaram incentivando a criação de outras estruturas mais eficientes em determinados cenários que são mais complexas que as originais, portanto, vale o estudo dessas estruturas especializadas.

Jovens, não existe nada novo na TI: algumas dessas estruturas já estão documentas desde a década de 70/80! Jovens de front-end: se você acha que estruturada de dados não serve para front-end, pesquise aí como DOM / Shadow DOM funcionam.

Para finalizar: tenha bastante atenção ao usar MapSets, pois podem gerar bugs difíceis de lidar caso sejam usados sem levar em consideração os detalhes mencionados acima.

Structs: Se você precisar de um pouco mais de “sensação de estrutura” no seu código que maps puros (e anônimos) não podem te dar, utilize esse tipo. A diferença básica é que você precisa declarar uma struct dentro de um module (foi o que mais me pareceu “fortemente tipado” no Elixir até agora). Se não ficou claro lendo os artigos anteriores, Elixir é uma linguagem de tipagem dinâmica (como JS ou Python).

Segue um exemplo para ilustrar esse “sensação de estrutura”:

Exemplo de declaração de struct

Sim, também achei que é basicamente um classe. É tão parecido que caso você não declare o valor padrão de alguma “propriedade” (o termo correto não é esse, pois estamos utilizando um map e maps não possuem propriedades no sentido estrito do termo, mas vale a menção para fins didáticos nesse caso), o valor assumido é null (devido a influência de Ruby na criação do Elixir, a palavra reservada é nil para representar nulos, assim como em Ruby).

Sigils: São mecanismos da linguagem Elixir para manipulação de representações textuais (ou seja, strings). São compostos da seguinte estrutura: til (~)+ letra que representa o sigil + delimitador.

O sigil~r define expressões regulares (ou regex, para os íntimos):

# Uma expressão regular que encontram strings que contenham "foo" ou "bar":iex> regex = ~r/foo|bar/
~r/foo|bar/
#o operador =~ compara uma regex com uma stringiex> "foo" =~ regex
true
iex> "bat" =~ regex
false

Os delimitadores são bem flexíveis, podendo ser usados até 8 delimitadores diferentes para expressar uma sigil:

~r/hello/
~r|hello|
~r"hello"
~r'hello'
~r(hello)
~r[hello]
~r{hello}
~r<hello>

Além de regular expressions, podem ser usados sigils para gerar strings comuns, lista de caracteres (charlists), lista de palavras separadas por espaços (wordlists), datas simples (AAAA-MM-DD), tempo (HH:MM:SS) e datas no formato UTC (com ou sem timezone). Segue exemplos:

#sigil string
iex> ~s(string "comum")
"string \"comum\""
#sigil charlist
iex> ~c(charlist com 'aspas' simples)
'charlist com \'aspas\' simples'
#sigil wordlists
iex> ~w(uni duni te)
["uni", "duni", "te"]
#sigil heredoc
iex> ~s"""
...> isso é
...> uma string heredoc
...> sou criada com três aspas duplas
...> e sou usada para documentação
...> """
#sigil data simples (AAAA-MM-DD)
iex> data = ~D[2020-09-26]
~D[2020-09-26]
iex> data.year
2020
#sigil de hora (HH:MM:SS)
iex> t = ~T[16:31:07]
~T[16:31:07]
iex> t.second
7
#sigil de data completa sem timezone
iex> data_completa = ~N[2020-09-26 16:32:21]
~N[2020-09-26 16:32:21]
iex> data_completa.hour
16

O lema do Elixir é “simplicidade e extensibilidade”, portanto, é possível expandir o uso de sigils e criar mecanismos customizados facilmente, mais detalhes aqui.

Pattern Matching: Conhecer essa feature foi um divisor de águas para mim. Em elixir, o match operator consiste somente em usar o =. Uma dica que eu consigo fornecer aqui é que ao invés de pensar como uma operação de atribuição (ou seja, isso recebe daquilo), pense em: isso tem um padrão igual ao daquilo? Se o padrão não bater, ocorre um MatchError.

Com isso, é possível verificar por padrões em variáveis simples, maps, tuples e lists. Vamos a dois exemplos básicos:

#uso em variáveis
iex> x = 1
1
iex> 1 = x
1
iex> 2 = x
** (MatchError) no match of right hand side value: 1
#uso em lists, perceba que a atribuição é feita através de padrões
iex> [a, b, c] = [1, 2, 3]
[1, 2, 3]
iex> a
1

Para ilustrar do poder dessa feature, vamos a um exemplo de uma solução do problema FizzBuzz:

exemplo de fizzbuzz usando pattern matching

Esclarecendo algumas coisas que não foram mencionadas anteriormente: IO.inspect é uma função que fica escutando o resultado de uma outra função e escreve no console o que foi ouvido (isso é usado para debug), defp é a palavra reservada para criar uma função privada, e o operador _ é para avisar ao compilador que aquela variável não será usada (caso isso não seja feito, é emitido um warning informando essa declaração sem uso).

Lembre-se: checar por padrões usando funções e resultado de funções é possível também, ao invés de somente checar por padrões em variáveis e demais tipos simples. Agora, pense no poder de expressividade (e legibilidade!) de construções semelhantes em regras complexas usando essa feature.

Elixir is all about simplicity and extensibility ❤️

Até o próximo artigo!

Você gostou do conteúdo e gostaria de fazer mentoria comigo? Clique aqui e descubra como.

--

--