Elixir - Guia Rápido

Elixir é uma linguagem dinâmica e funcional projetada para construir aplicativos escaláveis ​​e de fácil manutenção. Ele aproveita o Erlang VM, conhecido por executar sistemas de baixa latência, distribuídos e tolerantes a falhas, ao mesmo tempo que é usado com sucesso no desenvolvimento da web e no domínio do software integrado.

Elixir é uma linguagem funcional e dinâmica construída sobre Erlang e Erlang VM. Erlang é uma linguagem que foi originalmente escrita em 1986 pela Ericsson para ajudar a resolver problemas de telefonia como distribuição, tolerância a falhas e simultaneidade. Elixir, escrito por José Valim, estende Erlang e fornece uma sintaxe mais amigável para o Erlang VM. Ele faz isso enquanto mantém o desempenho do mesmo nível de Erlang.

Características do Elixir

Vamos agora discutir alguns recursos importantes do Elixir -

  • Scalability - Todo o código Elixir roda em processos leves que são isolados e trocam informações por meio de mensagens.

  • Fault Tolerance- Elixir fornece supervisores que descrevem como reiniciar partes do seu sistema quando algo dá errado, voltando a um estado inicial conhecido que é garantido que funcione. Isso garante que seu aplicativo / plataforma nunca fique inativo.

  • Functional Programming - A programação funcional promove um estilo de codificação que ajuda os desenvolvedores a escrever códigos curtos, rápidos e de fácil manutenção.

  • Build tools- Elixir vem com um conjunto de ferramentas de desenvolvimento. O Mix é uma ferramenta que facilita a criação de projetos, o gerenciamento de tarefas, a execução de testes etc. Ele também possui seu próprio gerenciador de pacotes - Hex.

  • Erlang Compatibility - Elixir é executado na VM Erlang, dando aos desenvolvedores acesso completo ao ecossistema de Erlang.

Para executar o Elixir, você precisa configurá-lo localmente em seu sistema.

Para instalar o Elixir, você precisará primeiro do Erlang. Em algumas plataformas, os pacotes Elixir vêm com Erlang.

Instalando Elixir

Vamos agora entender a instalação do Elixir em diferentes sistemas operacionais.

configuração do Windows

Para instalar o Elixir no Windows, baixe o instalador em https://repo.hex.pm/elixirwebsetup.exe e simplesmente clicar Nextpara prosseguir em todas as etapas. Você o terá em seu sistema local.

Se você tiver algum problema ao instalá-lo, pode verificar esta página para obter mais informações.

Configuração do Mac

Se você tiver o Homebrew instalado, certifique-se de que é a versão mais recente. Para atualizar, use o seguinte comando -

brew update

Agora, instale Elixir usando o comando fornecido abaixo -

brew install elixir

Configuração do Ubuntu / Debian

As etapas para instalar o Elixir em uma configuração Ubuntu / Debian são as seguintes -

Adicionar repositório Erlang Solutions -

wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo 
dpkg -i erlang-solutions_1.0_all.deb 
sudo apt-get update

Instale a plataforma Erlang / OTP e todos os seus aplicativos -

sudo apt-get install esl-erlang

Instale Elixir -

sudo apt-get install elixir

Outras distros Linux

Se você tiver qualquer outra distribuição Linux, visite esta página para configurar o elixir em seu sistema local.

Testando a configuração

Para testar a configuração do Elixir em seu sistema, abra seu terminal e digite iex nele. Ele irá abrir o shell elixir interativo como o seguinte -

Erlang/OTP 19 [erts-8.0] [source-6dc93c1] [64-bit] 
[smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]  

Interactive Elixir (1.3.1) - press Ctrl+C to exit (type h() ENTER for help) 
iex(1)>

Elixir agora está configurado com sucesso em seu sistema.

Começaremos com o programa habitual 'Hello World'.

Para iniciar o shell interativo Elixir, digite o seguinte comando.

iex

Depois que o shell for iniciado, use o IO.putsfunção para "colocar" a string na saída do console. Digite o seguinte em seu shell Elixir -

IO.puts "Hello world"

Neste tutorial, usaremos o modo de script Elixir, onde manteremos o código Elixir em um arquivo com a extensão .ex. Vamos agora manter o código acima notest.exArquivo. Na etapa seguinte, iremos executá-lo usandoelixirc-

IO.puts "Hello world"

Vamos agora tentar executar o programa acima da seguinte maneira -

$elixirc test.ex

O programa acima gera o seguinte resultado -

Hello World

Aqui estamos chamando uma função IO.putspara gerar uma string para nosso console como saída. Esta função também pode ser chamada da maneira que fazemos em C, C ++, Java, etc., fornecendo argumentos entre parênteses após o nome da função -

IO.puts("Hello world")

Comentários

Os comentários de uma única linha começam com um símbolo '#'. Não há comentários de várias linhas, mas você pode empilhar vários comentários. Por exemplo -

#This is a comment in Elixir

Fim de linha

Não há terminações de linha obrigatórias como ';' em Elixir. No entanto, podemos ter várias instruções na mesma linha, usando ';'. Por exemplo,

IO.puts("Hello"); IO.puts("World!")

O programa acima gera o seguinte resultado -

Hello 
World!

Identificadores

Identificadores como variáveis, nomes de funções são usados ​​para identificar uma variável, função, etc. No Elixir, você pode nomear seus identificadores começando com um alfabeto minúsculo com números, sublinhados e letras maiúsculas depois disso. Essa convenção de nomenclatura é comumente conhecida como snake_case. Por exemplo, a seguir estão alguns identificadores válidos no Elixir -

var1       variable_2      one_M0r3_variable

Observe que as variáveis ​​também podem ser nomeadas com um sublinhado à esquerda. Um valor que não deve ser usado deve ser atribuído a _ ou a uma variável começando com sublinhado -

_some_random_value = 42

Além disso, o elixir depende de sublinhados para tornar as funções privadas para os módulos. Se você nomear uma função com um sublinhado à esquerda em um módulo e importar esse módulo, essa função não será importada.

Existem muitos outros meandros relacionados à nomenclatura de funções no Elixir, que discutiremos nos próximos capítulos.

Palavras reservadas

As palavras a seguir são reservadas e não podem ser usadas como nomes de variáveis, módulos ou funções.

after     and     catch     do     inbits     inlist     nil     else     end 
not     or     false     fn     in     rescue     true     when     xor 
__MODULE__    __FILE__    __DIR__    __ENV__    __CALLER__

Para usar qualquer idioma, você precisa entender os tipos de dados básicos que o idioma suporta. Neste capítulo, discutiremos 7 tipos de dados básicos suportados pela linguagem elixir: inteiros, flutuantes, booleanos, átomos, strings, listas e tuplas.

Tipos Numéricos

Elixir, como qualquer outra linguagem de programação, suporta números inteiros e flutuantes. Se você abrir seu shell elixir e inserir qualquer número inteiro ou flutuante como entrada, ele retornará seu valor. Por exemplo,

42

Quando o programa acima é executado, ele produz o seguinte resultado -

42

Você também pode definir números em bases octais, hexadecimais e binárias.

Octal

Para definir um número na base octal, prefixe-o com '0o'. Por exemplo, 0o52 em octal é equivalente a 42 em decimal.

Hexadecimal

Para definir um número na base decimal, prefixe-o com '0x'. Por exemplo, 0xF1 em hexadecimal é equivalente a 241 em decimal.

Binário

Para definir um número em base binária, prefixe-o com '0b'. Por exemplo, 0b1101 em binário é equivalente a 13 em decimal.

Elixir suporta precisão dupla de 64 bits para números de ponto flutuante. E eles também podem ser definidos usando um estilo de exponenciação. Por exemplo, 10145230000 pode ser escrito como 1.014523e10

Átomos

Os átomos são constantes cujo nome é seu valor. Eles podem ser criados usando o símbolo de cor (:). Por exemplo,

:hello

Booleanos

Elixir suporta true e falsecomo booleanos. Ambos os valores estão de fato ligados aos átomos: true e: false respectivamente.

Cordas

As strings no Elixir são inseridas entre aspas duplas e são codificadas em UTF-8. Eles podem se estender por várias linhas e conter interpolações. Para definir uma string, basta inseri-la entre aspas duplas -

"Hello world"

Para definir strings de várias linhas, usamos uma sintaxe semelhante a python com aspas duplas triplas -

"""
Hello
World!
"""

Aprenderemos sobre strings, binários e listas de caracteres (semelhantes a strings) em profundidade no capítulo de strings.

Binários

Binários são sequências de bytes entre << >> separados por uma vírgula. Por exemplo,

<< 65, 68, 75>>

Os binários são usados ​​principalmente para lidar com dados relacionados a bits e bytes, se houver algum. Eles podem, por padrão, armazenar de 0 a 255 em cada valor. Esse limite de tamanho pode ser aumentado usando a função de tamanho que diz quantos bits devem ser necessários para armazenar esse valor. Por exemplo,

<<65, 255, 289::size(15)>>

Listas

Elixir usa colchetes para especificar uma lista de valores. Os valores podem ser de qualquer tipo. Por exemplo,

[1, "Hello", :an_atom, true]

As listas vêm com funções integradas para início e fim da lista denominadas hd e tl que retornam o início e o fim da lista, respectivamente. Às vezes, quando você cria uma lista, ele retorna uma lista de caracteres. Isso ocorre porque quando o elixir vê uma lista de caracteres ASCII imprimíveis, ele a imprime como uma lista de caracteres. Observe que strings e listas de caracteres não são iguais. Discutiremos as listas mais adiante em capítulos posteriores.

Tuplas

Elixir usa chaves para definir tuplas. Como as listas, as tuplas podem conter qualquer valor.

{ 1, "Hello", :an_atom, true

Uma questão surge aqui, - por que fornecer ambos lists e tuplesquando os dois funcionam da mesma maneira? Bem, eles têm implementações diferentes.

  • As listas são, na verdade, armazenadas como listas vinculadas, portanto, as inserções e exclusões são muito rápidas nas listas.

  • Já as tuplas são armazenadas em blocos contíguos de memória, o que torna o acesso a elas mais rápido, mas acrescenta um custo adicional nas inserções e exclusões.

Uma variável nos fornece armazenamento nomeado que nossos programas podem manipular. Cada variável no Elixir possui um tipo específico, que determina o tamanho e o layout da memória da variável; a faixa de valores que podem ser armazenados nessa memória; e o conjunto de operações que podem ser aplicadas à variável.

Tipos de Variáveis

Elixir suporta os seguintes tipos básicos de variáveis.

Inteiro

Eles são usados ​​para inteiros. Eles têm tamanho de 32 bits em uma arquitetura de 32 bits e de 64 bits em uma arquitetura de 64 bits. Os inteiros são sempre assinados em elixir. Se um inteiro começar a expandir em tamanho acima de seu limite, elixir converta-o em um Grande Inteiro que ocupa a memória no intervalo de 3 a n palavras, o que quer que caiba na memória.

Floats

Os flutuadores têm uma precisão de 64 bits no elixir. Eles também são como números inteiros em termos de memória. Ao definir um float, a notação exponencial pode ser usada.

boleano

Eles podem assumir 2 valores que são verdadeiros ou falsos.

Cordas

Strings são codificados em utf-8 em elixir. Eles têm um módulo de strings que fornece muitas funcionalidades para o programador manipular strings.

Funções anônimas / Lambdas

Essas são funções que podem ser definidas e atribuídas a uma variável, que pode então ser usada para chamar essa função.

Coleções

Existem muitos tipos de coleção disponíveis no Elixir. Alguns deles são listas, tuplas, mapas, binários, etc. Eles serão discutidos nos capítulos subsequentes.

Declaração de Variável

Uma declaração de variável informa ao interpretador onde e quanto criar o armazenamento para a variável. Elixir não nos permite apenas declarar uma variável. Uma variável deve ser declarada e atribuída um valor ao mesmo tempo. Por exemplo, para criar uma variável chamada vida e atribuir a ela um valor 42, fazemos o seguinte -

life = 42

Isso vinculará a variável life ao valor 42. Se quisermos reatribuir a essa variável um novo valor, podemos fazer isso usando a mesma sintaxe acima, ou seja,

life = "Hello world"

Nomenclatura de Variável

Variáveis ​​de nomenclatura seguem um snake_caseconvenção no Elixir, ou seja, todas as variáveis ​​devem começar com uma letra minúscula, seguida por 0 ou mais letras (maiúsculas e minúsculas), seguida no final por um opcional '?' OU '!'.

Os nomes das variáveis ​​também podem ser iniciados com um sublinhado inicial, mas isso deve ser usado somente ao ignorar a variável, ou seja, essa variável não será usada novamente, mas precisa ser atribuída a algo.

Variáveis ​​de impressão

No shell interativo, as variáveis ​​serão impressas se você apenas inserir o nome da variável. Por exemplo, se você criar uma variável -

life = 42

E insira 'vida' em seu shell, você obterá a saída como -

42

Mas se você deseja enviar uma variável para o console (ao executar um script externo de um arquivo), você precisa fornecer a variável como entrada para IO.puts função -

life = 42  
IO.puts life

ou

life = 42 
IO.puts(life)

Isso lhe dará a seguinte saída -

42

Um operador é um símbolo que informa ao compilador para executar manipulações matemáticas ou lógicas específicas. Existem MUITOS operadores fornecidos pelo elixir. Eles são divididos nas seguintes categorias -

  • Operadores aritméticos
  • Operadores de comparação
  • Operadores booleanos
  • Operadores diversos

Operadores aritméticos

A tabela a seguir mostra todos os operadores aritméticos suportados pela linguagem Elixir. Assumir variávelA contém 10 e variável B contém 20, então -

Mostrar exemplos

Operador Descrição Exemplo
+ Adiciona 2 números. A + B dará 30
- Subtrai o segundo número do primeiro. AB dará -10
* Multiplica dois números. A * B dará 200
/ Divide o primeiro número do segundo. Isso converte os números em flutuantes e dá um resultado flutuante A / B dará 0,5.
div Esta função é usada para obter o quociente na divisão. div (10,20) dará 0
rem Esta função é usada para obter o resto da divisão. rem (A, B) dará 10

Operadores de comparação

Os operadores de comparação no Elixir são mais comuns aos fornecidos na maioria dos outros idiomas. A tabela a seguir resume os operadores de comparação no Elixir. Assumir variávelA contém 10 e variável B contém 20, então -

Mostrar exemplos

Operador Descrição Exemplo
== Verifica se o valor à esquerda é igual ao valor à direita (tipo projeta valores se eles não forem do mesmo tipo). A == B dará falso
! = Verifica se o valor à esquerda não é igual ao valor à direita. A! = B dará verdadeiro
=== Verifica se o tipo de valor à esquerda é igual ao tipo de valor à direita; em caso afirmativo, verifique o mesmo para o valor. A === B dará falso
! == O mesmo que acima, mas verifica a desigualdade em vez da igualdade. A! == B dará verdadeiro
> Verifica se o valor do operando esquerdo é maior que o valor do operando direito; se sim, então a condição se torna verdadeira. A> B dará falso
< Verifica se o valor do operando esquerdo é menor que o valor do operando direito; se sim, então a condição se torna verdadeira. A <B dará verdadeiro
> = Verifica se o valor do operando esquerdo é maior ou igual ao valor do operando direito; se sim, então a condição se torna verdadeira. A> = B dará falso
<= Verifica se o valor do operando esquerdo é menor ou igual ao valor do operando direito; se sim, então a condição se torna verdadeira. A <= B dará verdadeiro

Operadores lógicos

Elixir fornece 6 operadores lógicos: and, or, not, &&, || e! Os três primeiros,and or notsão operadores booleanos estritos, o que significa que eles esperam que seu primeiro argumento seja um booleano. O argumento não booleano gerará um erro. Enquanto os próximos três,&&, || and !não são estritos, não exigem que tenhamos o primeiro valor estritamente como um booleano. Eles funcionam da mesma maneira que suas contrapartes estritas. Assumir variávelA é verdadeiro e variável B contém 20, então -

Mostrar exemplos

Operador Descrição Exemplo
e Verifica se ambos os valores fornecidos são verdadeiros; em caso afirmativo, retorna o valor da segunda variável. (Lógico e). A e B darão 20
ou Verifica se algum valor fornecido é verdadeiro. Retorna o valor verdadeiro. Else retorna falso. (Lógico ou). A ou B dará verdadeiro
não Operador unário que inverte o valor da entrada dada. não A dará falso
&& Não estrito and. Funciona igual aand mas não espera que o primeiro argumento seja um booleano. B && A dará 20
|| Não estrito or. Funciona igual aor mas não espera que o primeiro argumento seja um booleano. B || A vai dar verdadeiro
! Não estrito not. Funciona igual anot mas não espera que o argumento seja um booleano. ! A vai dar falso

NOTE −e , ou , && e || || são operadores de curto-circuito. Isso significa que se o primeiro argumento deandfor falso, então ele não verificará mais o segundo. E se o primeiro argumento deorfor verdadeiro, então ele não verificará o segundo. Por exemplo,

false and raise("An error")  
#This won't raise an error as raise function wont get executed because of short
#circuiting nature of and operator

Operadores bit a bit

Operadores bit a bit trabalham em bits e realizam operação bit a bit. Elixir fornece módulos bit a bit como parte do pacoteBitwise, então, para usá-los, você precisa usar o módulo bit a bit. Para usá-lo, digite o seguinte comando em seu shell -

use Bitwise

Suponha que A seja 5 e B seja 6 para os seguintes exemplos -

Mostrar exemplos

Operador Descrição Exemplo
&&& O bit a bit e o operador copiam um bit para o resultado se ele existir em ambos os operandos. A &&& B dará 4
||| O operador bit a bit ou copia um bit para o resultado se ele existir em qualquer operando. A ||| B dará 7
>>> O operador de deslocamento para a direita bit a bit desloca os primeiros bits do operando para a direita pelo número especificado no segundo operando. A >>> B dará 0
<<< O operador de deslocamento para a esquerda bit a bit desloca os primeiros bits do operando para a esquerda pelo número especificado no segundo operando. A <<< B dará 320
^^^ O operador XOR bit a bit copia um bit para o resultado apenas se for diferente em ambos os operandos. A ^^^ B dará 3
~~~ O unário bit a bit não inverte os bits no número fornecido. ~~~ A dará -6

Operadores diversos

Além dos operadores acima, Elixir também oferece uma variedade de outros operadores como Concatenation Operator, Match Operator, Pin Operator, Pipe Operator, String Match Operator, Code Point Operator, Capture Operator, Ternary Operator que o tornam uma linguagem bastante poderosa.

Mostrar exemplos

A correspondência de padrões é uma técnica que Elixir herda de Erlang. É uma técnica muito poderosa que nos permite extrair subestruturas mais simples de estruturas de dados complicadas como listas, tuplas, mapas, etc.

Uma partida tem 2 partes principais, uma left e um rightlado. O lado direito é uma estrutura de dados de qualquer tipo. O lado esquerdo tenta combinar a estrutura de dados do lado direito e vincular quaisquer variáveis ​​à esquerda à respectiva subestrutura à direita. Se uma correspondência não for encontrada, o operador gerará um erro.

A combinação mais simples é uma variável isolada à esquerda e qualquer estrutura de dados à direita. This variable will match anything. Por exemplo,

x = 12
x = "Hello"
IO.puts(x)

Você pode colocar variáveis ​​dentro de uma estrutura para que possa capturar uma subestrutura. Por exemplo,

[var_1, _unused_var, var_2] = [{"First variable"}, 25, "Second variable" ]
IO.puts(var_1)
IO.puts(var_2)

Isso armazenará os valores, {"First variable"}em var_1 e"Second variable"em var_2 . Há também um especial_ variável (ou variáveis ​​prefixadas com '_') que funciona exatamente como outras variáveis, mas informa ao elixir, "Make sure something is here, but I don't care exactly what it is.". No exemplo anterior, _unused_var era uma dessas variáveis.

Podemos combinar padrões mais complicados usando esta técnica. Paraexample se você deseja desembrulhar e obter um número em uma tupla que está dentro de uma lista que está em uma lista, você pode usar o seguinte comando -

[_, [_, {a}]] = ["Random string", [:an_atom, {24}]]
IO.puts(a)

O programa acima gera o seguinte resultado -

24

Isso vai ligar a a 24. Outros valores são ignorados, pois estamos usando '_'.

Na correspondência de padrões, se usarmos uma variável no right, seu valor é usado. Se quiser usar o valor de uma variável à esquerda, você precisará usar o operador pin.

Por exemplo, se você tem uma variável "a" com valor 25 e deseja combiná-la com outra variável "b" com valor 25, você precisa inserir -

a = 25
b = 25
^a = b

A última linha corresponde ao valor atual de a, em vez de atribuí-lo, ao valor de b. Se tivermos um conjunto não correspondente do lado esquerdo e direito, o operador de correspondência gerará um erro. Por exemplo, se tentarmos combinar uma tupla com uma lista ou uma lista de tamanho 2 com uma lista de tamanho 3, um erro será exibido.

As estruturas de tomada de decisão requerem que o programador especifique uma ou mais condições a serem avaliadas ou testadas pelo programa, junto com uma instrução ou instruções a serem executadas se a condição for determinada como truee, opcionalmente, outras instruções a serem executadas se a condição for determinada como false.

A seguir está a parte geral de uma estrutura típica de tomada de decisão encontrada na maioria das linguagens de programação -

Elixir fornece construções condicionais if / else como muitas outras linguagens de programação. Também tem umconddeclaração que chama o primeiro valor verdadeiro que encontra. Case é outra instrução de fluxo de controle que usa correspondência de padrões para controlar o fluxo do programa. Vamos examiná-los profundamente.

Elixir fornece os seguintes tipos de declarações de tomada de decisão. Clique nos links a seguir para verificar seus detalhes.

Sr. Não. Declaração e descrição
1 declaração if

Uma instrução if consiste em uma expressão booleana seguida por do, uma ou mais instruções executáveis ​​e, finalmente, um endpalavra-chave. O código na instrução if é executado apenas se a condição booleana for avaliada como verdadeira.

2 if..else statement

Uma instrução if pode ser seguida por uma instrução else opcional (dentro do bloco do..end), que é executada quando a expressão booleana é falsa.

3 a menos que declaração

Uma declaração a menos que tem o mesmo corpo de uma declaração if. O código dentro da instrução a menos que executa somente quando a condição especificada é falsa.

4 a menos que outra declaração

Uma instrução except..else tem o mesmo corpo que uma instrução if..else. O código dentro da instrução a menos que executa somente quando a condição especificada é falsa.

5 cond

Uma instrução cond é usada quando queremos executar o código com base em várias condições. Funciona como um if ... else if ... outra construção em várias outras linguagens de programação.

6 caso

A instrução Case pode ser considerada uma substituição da instrução switch em linguagens imperativas. Case pega uma variável / literal e aplica a correspondência de padrões a ela com casos diferentes. Se houver correspondência de qualquer caso, Elixir executa o código associado a esse caso e sai da instrução de caso.

As strings no Elixir são inseridas entre aspas duplas e são codificadas em UTF-8. Ao contrário de C e C ++, onde as strings padrão são codificadas em ASCII e apenas 256 caracteres diferentes são possíveis, o UTF-8 consiste em 1.112.064 pontos de código. Isso significa que a codificação UTF-8 consiste em muitos caracteres diferentes possíveis. Como as strings usam utf-8, também podemos usar símbolos como: ö, ł, etc.

Crie uma string

Para criar uma variável de string, basta atribuir uma string a uma variável -

str = "Hello world"

Para imprimir isso em seu console, basta ligar para o IO.puts função e passa a variável str -

str = str = "Hello world" 
IO.puts(str)

O programa acima gera o seguinte resultado -

Hello World

Strings vazias

Você pode criar uma string vazia usando o literal de string, "". Por exemplo,

a = ""
if String.length(a) === 0 do
   IO.puts("a is an empty string")
end

O programa acima gera o seguinte resultado.

a is an empty string

Interpolação de String

A interpolação de string é uma maneira de construir um novo valor de string a partir de uma mistura de constantes, variáveis, literais e expressões, incluindo seus valores dentro de um literal de string. Elixir suporta interpolação de string, para usar uma variável em uma string, ao escrevê-la, envolva-a com chaves e preceda as chaves com um'#' placa.

Por exemplo,

x = "Apocalypse" 
y = "X-men #{x}"
IO.puts(y)

Isso pegará o valor de x e o substituirá em y. O código acima irá gerar o seguinte resultado -

X-men Apocalypse

String Concatenation

Já vimos o uso da concatenação de String em capítulos anteriores. O operador '<>' é usado para concatenar strings no Elixir. Para concatenar 2 strings,

x = "Dark"
y = "Knight"
z = x <> " " <> y
IO.puts(z)

O código acima gera o seguinte resultado -

Dark Knight

Comprimento da corda

Para obter o comprimento da corda, usamos o String.lengthfunção. Passe a string como parâmetro e ela mostrará seu tamanho. Por exemplo,

IO.puts(String.length("Hello"))

Ao executar o programa acima, ele produz o seguinte resultado -

5

Invertendo uma corda

Para reverter uma string, passe-a para a função String.reverse. Por exemplo,

IO.puts(String.reverse("Elixir"))

O programa acima gera o seguinte resultado -

rixilE

Comparação de cordas

Para comparar 2 strings, podemos usar os operadores == ou ===. Por exemplo,

var_1 = "Hello world"
var_2 = "Hello Elixir"
if var_1 === var_2 do
   IO.puts("#{var_1} and #{var_2} are the same")
else
   IO.puts("#{var_1} and #{var_2} are not the same")
end

O programa acima gera o seguinte resultado -

Hello world and Hello elixir are not the same.

String Matching

Já vimos o uso do operador = ~ string match. Para verificar se uma string corresponde a uma regex, também podemos usar o operador string match ou String.match? função. Por exemplo,

IO.puts(String.match?("foo", ~r/foo/))
IO.puts(String.match?("bar", ~r/foo/))

O programa acima gera o seguinte resultado -

true 
false

O mesmo também pode ser obtido usando o operador = ~. Por exemplo,

IO.puts("foo" =~ ~r/foo/)

O programa acima gera o seguinte resultado -

true

Funções de String

Elixir oferece suporte a um grande número de funções relacionadas a strings, algumas das mais usadas estão listadas na tabela a seguir.

Sr. Não. Função e sua finalidade
1

at(string, position)

Retorna o grafema na posição da string utf8 fornecida. Se a posição for maior do que o comprimento da string, ela retornará nil

2

capitalize(string)

Converte o primeiro caractere de uma determinada string em maiúsculas e o restante em minúsculas

3

contains?(string, contents)

Verifica se a string contém algum dos conteúdos fornecidos

4

downcase(string)

Converte todos os caracteres na string dada em minúsculas

5

ends_with?(string, suffixes)

Retorna verdadeiro se a string termina com qualquer um dos sufixos fornecidos

6

first(string)

Retorna o primeiro grafema de uma string utf8, nulo se a string estiver vazia

7

last(string)

Retorna o último grafema de uma string utf8, nulo se a string estiver vazia

8

replace(subject, pattern, replacement, options \\ [])

Retorna uma nova string criada pela substituição de ocorrências de padrão no assunto com substituição

9

slice(string, start, len)

Retorna uma substring começando no início do deslocamento e de comprimento len

10

split(string)

Divide uma string em substrings em cada ocorrência de espaço em branco Unicode com espaços em branco à esquerda e à direita ignorados. Grupos de espaços em branco são tratados como uma única ocorrência. As divisões não ocorrem em espaços em branco ininterruptos

11

upcase(string)

Converte todos os caracteres na string dada em maiúsculas

Binários

Um binário é apenas uma sequência de bytes. Binários são definidos usando<< >>. Por exemplo:

<< 0, 1, 2, 3 >>

Claro, esses bytes podem ser organizados de qualquer maneira, mesmo em uma sequência que não os torne uma string válida. Por exemplo,

<< 239, 191, 191 >>

Strings também são binários. E o operador de concatenação de string<> é na verdade um operador de concatenação binária:

IO.puts(<< 0, 1 >> <> << 2, 3 >>)

O código acima gera o seguinte resultado -

<< 0, 1, 2, 3 >>

Observe o caractere ł. Como é codificado em utf-8, essa representação de caractere ocupa 2 bytes.

Uma vez que cada número representado em um binário é um byte, quando esse valor sobe de 255, ele é truncado. Para evitar isso, usamos o modificador de tamanho para especificar quantos bits queremos que esse número tome. Por exemplo -

IO.puts(<< 256 >>) # truncated, it'll print << 0 >>
IO.puts(<< 256 :: size(16) >>) #Takes 16 bits/2 bytes, will print << 1, 0 >>

O programa acima irá gerar o seguinte resultado -

<< 0 >>
<< 1, 0 >>

Também podemos usar o modificador utf8, se um caractere for um ponto de código, ele será produzido na saída; senão os bytes -

IO.puts(<< 256 :: utf8 >>)

O programa acima gera o seguinte resultado -

Ā

Também temos uma função chamada is_binaryque verifica se uma determinada variável é binária. Observe que apenas as variáveis ​​armazenadas como múltiplos de 8 bits são binárias.

Bitstrings

Se definirmos um binário usando o modificador de tamanho e passarmos a ele um valor que não seja um múltiplo de 8, terminaremos com um bitstring em vez de um binário. Por exemplo,

bs = << 1 :: size(1) >>
IO.puts(bs)
IO.puts(is_binary(bs))
IO.puts(is_bitstring(bs))

O programa acima gera o seguinte resultado -

<< 1::size(1) >>
false
true

Isso significa que a variável bsnão é um binário, mas sim um bitstring. Também podemos dizer que um binário é um bitstring em que o número de bits é divisível por 8. A correspondência de padrões funciona em binários e também em bitstrings da mesma maneira.

Uma lista de caracteres nada mais é do que uma lista de caracteres. Considere o seguinte programa para entender o mesmo.

IO.puts('Hello')
IO.puts(is_list('Hello'))

O programa acima gera o seguinte resultado -

Hello
true

Em vez de conter bytes, uma lista de caracteres contém os pontos de código dos caracteres entre aspas simples. So while the double-quotes represent a string (i.e. a binary), singlequotes represent a char list (i.e. a list). Observe que o IEx gerará apenas pontos de código como saída se algum dos caracteres estiver fora do intervalo ASCII.

Listas de caracteres são usadas principalmente na interface com Erlang, em particular bibliotecas antigas que não aceitam binários como argumentos. Você pode converter uma lista de caracteres em uma string e vice-versa usando as funções to_string (char_list) e to_char_list (string) -

IO.puts(is_list(to_char_list("hełło")))
IO.puts(is_binary(to_string ('hełło')))

O programa acima gera o seguinte resultado -

true
true

NOTE - As funções to_string e to_char_list são polimórficos, ou seja, eles podem pegar vários tipos de entrada, como átomos, inteiros e convertê-los em strings e listas de caracteres, respectivamente.

Listas (vinculadas)

Uma lista encadeada é uma lista heterogênea de elementos que são armazenados em locais diferentes na memória e são controlados por meio de referências. Listas vinculadas são estruturas de dados usadas especialmente na programação funcional.

Elixir usa colchetes para especificar uma lista de valores. Os valores podem ser de qualquer tipo -

[1, 2, true, 3]

Quando Elixir vê uma lista de números ASCII imprimíveis, Elixir irá imprimir isso como uma lista de caracteres (literalmente uma lista de caracteres). Sempre que você vir um valor em IEx e não tiver certeza de qual é, você pode usar oi função para recuperar informações sobre ele.

IO.puts([104, 101, 108, 108, 111])

Os caracteres acima na lista podem ser impressos. Quando o programa acima é executado, ele produz o seguinte resultado -

hello

Você também pode definir listas ao contrário, usando aspas simples -

IO.puts(is_list('Hello'))

Quando o programa acima é executado, ele produz o seguinte resultado -

true

Tenha em mente que as representações entre aspas simples e aspas duplas não são equivalentes no Elixir, pois são representadas por tipos diferentes.

Comprimento de uma lista

Para encontrar o comprimento de uma lista, usamos a função de comprimento como no seguinte programa -

IO.puts(length([1, 2, :true, "str"]))

O programa acima gera o seguinte resultado -

4

Concatenação e Subtração

Duas listas podem ser concatenadas e subtraídas usando o ++ e --operadores. Considere o seguinte exemplo para entender as funções.

IO.puts([1, 2, 3] ++ [4, 5, 6])
IO.puts([1, true, 2, false, 3, true] -- [true, false])

Isso lhe dará uma string concatenada no primeiro caso e uma string subtraída no segundo. O programa acima gera o seguinte resultado -

[1, 2, 3, 4, 5, 6]
[1, 2, 3, true]

Cabeça e cauda de uma lista

A cabeça é o primeiro elemento de uma lista e a cauda é o resto de uma lista. Eles podem ser recuperados com as funçõeshd e tl. Vamos atribuir uma lista a uma variável e recuperar seu início e fim.

list = [1, 2, 3]
IO.puts(hd(list))
IO.puts(tl(list))

Isso nos dará o início e o fim da lista como saída. O programa acima gera o seguinte resultado -

1
[2, 3]

Note - Obter o início ou o fim de uma lista vazia é um erro.

Outras funções de lista

A biblioteca padrão Elixir fornece uma série de funções para lidar com listas. Vamos dar uma olhada em alguns deles aqui. Você pode verificar o resto aqui Lista .

S.no. Nome e descrição da função
1

delete(list, item)

Exclui o item fornecido da lista. Retorna uma lista sem o item. Se o item ocorrer mais de uma vez na lista, apenas a primeira ocorrência será removida.

2

delete_at(list, index)

Produz uma nova lista removendo o valor no índice especificado. Índices negativos indicam um deslocamento do final da lista. Se o índice estiver fora dos limites, a lista original será retornada.

3

first(list)

Retorna o primeiro elemento da lista ou nulo se a lista estiver vazia.

4

flatten(list)

Nivela a lista fornecida de listas aninhadas.

5

insert_at(list, index, value)

Retorna uma lista com o valor inserido no índice especificado. Observe que o índice é limitado ao comprimento da lista. Índices negativos indicam um deslocamento do final da lista.

6

last(list)

Retorna o último elemento da lista ou nulo se a lista estiver vazia.

Tuplas

Tuplas também são estruturas de dados que armazenam várias outras estruturas dentro delas. Ao contrário das listas, eles armazenam elementos em um bloco contíguo de memória. Isso significa que acessar um elemento de tupla por índice ou obter o tamanho da tupla é uma operação rápida. Os índices começam do zero.

Elixir usa chaves para definir tuplas. Como listas, tuplas podem conter qualquer valor -

{:ok, "hello"}

Comprimento de uma tupla

Para obter o comprimento de uma tupla, use o tuple_size funcionar como no programa a seguir -

IO.puts(tuple_size({:ok, "hello"}))

O programa acima gera o seguinte resultado -

2

Anexando um valor

Para acrescentar um valor à tupla, use a função Tuple.append -

tuple = {:ok, "Hello"}
Tuple.append(tuple, :world)

Isso criará e retornará uma nova tupla: {: ok, "Hello",: world}

Inserindo um Valor

Para inserir um valor em uma determinada posição, podemos usar o Tuple.insert_at função ou o put_elemfunção. Considere o seguinte exemplo para entender o mesmo -

tuple = {:bar, :baz}
new_tuple_1 = Tuple.insert_at(tuple, 0, :foo)
new_tuple_2 = put_elem(tuple, 1, :foobar)

Notar que put_elem e insert_atretornou novas tuplas. A tupla original armazenada na variável tupla não foi modificada porque os tipos de dados Elixir são imutáveis. Por ser imutável, o código Elixir é mais fácil de raciocinar, pois você nunca precisa se preocupar se um código específico está alterando sua estrutura de dados no local.

Tuplas vs. Listas

Qual é a diferença entre listas e tuplas?

As listas são armazenadas na memória como listas vinculadas, o que significa que cada elemento em uma lista mantém seu valor e aponta para o seguinte elemento até que o final da lista seja alcançado. Chamamos cada par de valor e ponteiro de célula cons. Isso significa que acessar o comprimento de uma lista é uma operação linear: precisamos percorrer toda a lista para descobrir seu tamanho. Atualizar uma lista é rápido, desde que estejamos adicionando elementos.

Por outro lado, tuplas são armazenadas de forma contígua na memória. Isso significa que obter o tamanho da tupla ou acessar um elemento por índice é rápido. No entanto, atualizar ou adicionar elementos às tuplas é caro porque requer a cópia de toda a tupla na memória.

Até agora, não discutimos nenhuma estrutura de dados associativa, ou seja, estruturas de dados que podem associar um determinado valor (ou vários valores) a uma chave. Diferentes idiomas chamam esses recursos com nomes diferentes, como dicionários, hashes, matrizes associativas, etc.

No Elixir, temos duas estruturas de dados associativas principais: listas de palavras-chave e mapas. Neste capítulo, vamos nos concentrar nas listas de palavras-chave.

Em muitas linguagens de programação funcional, é comum usar uma lista de tuplas de 2 itens como a representação de uma estrutura de dados associativa. No Elixir, quando temos uma lista de tuplas e o primeiro item da tupla (ou seja, a chave) é um átomo, o chamamos de lista de palavras-chave. Considere o seguinte exemplo para entender o mesmo -

list = [{:a, 1}, {:b, 2}]

Elixir suporta uma sintaxe especial para definir tais listas. Podemos colocar o cólon no final de cada átomo e nos livrar totalmente das tuplas. Por exemplo,

list_1 = [{:a, 1}, {:b, 2}]
list_2 = [a: 1, b: 2]
IO.puts(list_1 == list_2)

O programa acima irá gerar o seguinte resultado -

true

Ambos representam uma lista de palavras-chave. Como as listas de palavras-chave também são listas, podemos usar todas as operações que usamos nas listas delas.

Para recuperar o valor associado a um átomo na lista de palavras-chave, passe o átomo para [] após o nome da lista -

list = [a: 1, b: 2]
IO.puts(list[:a])

O programa acima gera o seguinte resultado -

1

As listas de palavras-chave têm três características especiais -

  • As chaves devem ser átomos.
  • As chaves são ordenadas, conforme especificado pelo desenvolvedor.
  • As chaves podem ser fornecidas mais de uma vez.

Para manipular listas de palavras-chave, Elixir fornece o módulo de palavras-chave . No entanto, lembre-se de que as listas de palavras-chave são simplesmente listas e, como tal, fornecem as mesmas características de desempenho linear das listas. Quanto mais longa a lista, mais tempo levará para encontrar uma chave, para contar o número de itens e assim por diante. Por esse motivo, as listas de palavras-chave são usadas no Elixir principalmente como opções. Se você precisa armazenar muitos itens ou garantir associados de uma chave com um valor máximo de um, você deve usar mapas.

Acessando uma chave

Para acessar os valores associados a uma determinada chave, usamos o Keyword.getfunção. Ele retorna o primeiro valor associado à chave fornecida. Para obter todos os valores, usamos a função Keyword.get_values. Por exemplo -

kl = [a: 1, a: 2, b: 3] 
IO.puts(Keyword.get(kl, :a)) 
IO.puts(Keyword.get_values(kl))

O programa acima irá gerar o seguinte resultado -

1
[1, 2]

Inserindo uma chave

Para adicionar um novo valor, use Keyword.put_new. Se a chave já existe, seu valor permanece inalterado -

kl = [a: 1, a: 2, b: 3]
kl_new = Keyword.put_new(kl, :c, 5)
IO.puts(Keyword.get(kl_new, :c))

Quando o programa acima é executado, ele produz uma nova lista de palavras-chave com a chave adicional c e gera o seguinte resultado -

5

Excluindo uma chave

Se você deseja excluir todas as entradas de uma chave, use Keyword.delete; para excluir apenas a primeira entrada de uma chave, use Keyword.delete_first.

kl = [a: 1, a: 2, b: 3, c: 0]
kl = Keyword.delete_first(kl, :b)
kl = Keyword.delete(kl, :a)

IO.puts(Keyword.get(kl, :a))
IO.puts(Keyword.get(kl, :b))
IO.puts(Keyword.get(kl, :c))

Isso irá deletar o primeiro b na lista e todos os ana lista. Quando o programa acima for executado, ele gerará o seguinte resultado -

0

Listas de palavras-chave são uma maneira conveniente de abordar o conteúdo armazenado em listas por chave, mas, por baixo, Elixir ainda está percorrendo a lista. Isso pode ser adequado se você tiver outros planos para essa lista que exijam uma análise completa, mas pode ser uma sobrecarga desnecessária se você estiver planejando usar as chaves como sua única abordagem para os dados.

É aqui que os mapas vêm em seu socorro. Sempre que você precisa de um armazenamento de valor-chave, os mapas são a estrutura de dados “ir para” no Elixir.

Criação de um mapa

Um mapa é criado usando a sintaxe% {} -

map = %{:a => 1, 2 => :b}

Em comparação com as listas de palavras-chave, já podemos ver duas diferenças -

  • Os mapas permitem qualquer valor como chave.
  • As chaves do Maps não seguem nenhuma ordem.

Acessando uma chave

Para acessar o valor associado a uma chave, o Maps usa a mesma sintaxe das listas de palavras-chave -

map = %{:a => 1, 2 => :b}
IO.puts(map[:a])
IO.puts(map[2])

Quando o programa acima é executado, ele gera o seguinte resultado -

1
b

Inserindo uma chave

Para inserir uma chave em um mapa, usamos o Dict.put_new função que leva o mapa, nova chave e novo valor como argumentos -

map = %{:a => 1, 2 => :b}
new_map = Dict.put_new(map, :new_val, "value") 
IO.puts(new_map[:new_val])

Isso irá inserir o par de valores-chave :new_val - "value"em um novo mapa. Quando o programa acima é executado, ele gera o seguinte resultado -

"value"

Atualizando um valor

Para atualizar um valor já presente no mapa, você pode usar a seguinte sintaxe -

map = %{:a => 1, 2 => :b}
new_map = %{ map | a: 25}
IO.puts(new_map[:a])

Quando o programa acima é executado, ele gera o seguinte resultado -

25

Correspondência de padrões

Em contraste com as listas de palavras-chave, os mapas são muito úteis com correspondência de padrões. Quando um mapa é usado em um padrão, ele sempre corresponderá a um subconjunto do valor fornecido -

%{:a => a} = %{:a => 1, 2 => :b}
IO.puts(a)

O programa acima gera o seguinte resultado -

1

Isso vai combinar a com 1. E, portanto, ele irá gerar a saída como1.

Conforme mostrado acima, um mapa corresponde, desde que as chaves no padrão existam no mapa fornecido. Portanto, um mapa vazio corresponde a todos os mapas.

Variáveis ​​podem ser usadas ao acessar, combinar e adicionar chaves de mapa -

n = 1
map = %{n => :one}
%{^n => :one} = %{1 => :one, 2 => :two, 3 => :three}

O módulo Mapa fornece uma API muito semelhante ao módulo Palavra-chave com funções convenientes para manipular mapas. Você pode usar funções como oMap.get, Map.delete, para manipular mapas.

Mapas com chaves Atom

Os mapas vêm com algumas propriedades interessantes. Quando todas as chaves em um mapa são átomos, você pode usar a sintaxe de palavra-chave por conveniência -

map = %{:a => 1, 2 => :b} 
IO.puts(map.a)

Outra propriedade interessante dos mapas é que eles fornecem sua própria sintaxe para atualizar e acessar as chaves atômicas -

map = %{:a => 1, 2 => :b}
IO.puts(map.a)

O programa acima gera o seguinte resultado -

1

Observe que, para acessar as chaves atômicas dessa forma, elas devem existir ou o programa não funcionará.

No Elixir, agrupamos várias funções em módulos. Já usamos diferentes módulos nos capítulos anteriores, como o módulo String, o módulo Bitwise, o módulo Tuple, etc.

Para criar nossos próprios módulos no Elixir, usamos o defmodulemacro. Nós usamos odef macro para definir funções nesse módulo -

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

Nas seções a seguir, nossos exemplos ficarão maiores e pode ser complicado digitá-los todos no shell. Precisamos aprender como compilar o código Elixir e também como executar scripts Elixir.

Compilação

É sempre conveniente escrever módulos em arquivos para que possam ser compilados e reutilizados. Vamos supor que temos um arquivo chamado math.ex com o seguinte conteúdo -

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

Podemos compilar os arquivos usando o comando -elixirc :

$ elixirc math.ex

Isso irá gerar um arquivo chamado Elixir.Math.beamcontendo o bytecode para o módulo definido. Se começarmosiexnovamente, nossa definição de módulo estará disponível (desde que o iex seja iniciado no mesmo diretório em que o arquivo bytecode está). Por exemplo,

IO.puts(Math.sum(1, 2))

O programa acima irá gerar o seguinte resultado -

3

Modo de script

Além da extensão do arquivo Elixir .ex, Elixir também suporta .exsarquivos para script. O Elixir trata os dois arquivos exatamente da mesma forma, a única diferença está no objetivo..ex os arquivos devem ser compilados enquanto os arquivos .exs são usados ​​para scripting. Quando executadas, ambas as extensões compilam e carregam seus módulos na memória, embora apenas.ex os arquivos gravam seu bytecode no disco no formato de arquivos .beam.

Por exemplo, se quisermos executar o Math.sum no mesmo arquivo, podemos usar os .exs da seguinte maneira -

Math.exs

defmodule Math do
   def sum(a, b) do
      a + b
   end
end
IO.puts(Math.sum(1, 2))

Podemos executá-lo usando o comando Elixir -

$ elixir math.exs

O programa acima irá gerar o seguinte resultado -

3

O arquivo será compilado na memória e executado, imprimindo “3” como resultado. Nenhum arquivo bytecode será criado.

Módulo Aninhado

Os módulos podem ser aninhados no Elixir. Esse recurso da linguagem nos ajuda a organizar nosso código de uma maneira melhor. Para criar módulos aninhados, usamos a seguinte sintaxe -

defmodule Foo do
   #Foo module code here
   defmodule Bar do
      #Bar module code here
   end
end

O exemplo fornecido acima definirá dois módulos: Foo e Foo.Bar. O segundo pode ser acessado comoBar dentro Foocontanto que eles estejam no mesmo escopo lexical. Se, mais tarde, oBar módulo é movido para fora da definição do módulo Foo, ele deve ser referenciado por seu nome completo (Foo.Bar) ou um alias deve ser definido usando a diretiva alias discutida no capítulo de alias.

Note- No Elixir, não há necessidade de definir o módulo Foo para definir o módulo Foo.Bar, já que a linguagem traduz todos os nomes dos módulos em átomos. Você pode definir módulos aninhados arbitrariamente sem definir nenhum módulo na cadeia. Por exemplo, você pode definirFoo.Bar.Baz sem definir Foo ou Foo.Bar.

A fim de facilitar a reutilização do software, Elixir fornece três diretivas - alias, require e import. Ele também fornece uma macro chamada use, resumida a seguir -

# Alias the module so it can be called as Bar instead of Foo.Bar
alias Foo.Bar, as: Bar

# Ensure the module is compiled and available (usually for macros)
require Foo

# Import functions from Foo so they can be called without the `Foo.` prefix
import Foo

# Invokes the custom code defined in Foo as an extension point
use Foo

Vamos agora entender em detalhes sobre cada diretiva.

apelido

A diretiva alias permite que você configure apelidos para qualquer nome de módulo fornecido. Por exemplo, se você deseja fornecer um alias'Str' para o módulo String, você pode simplesmente escrever -

alias String, as: Str
IO.puts(Str.length("Hello"))

O programa acima gera o seguinte resultado -

5

Um alias é dado ao String módulo como Str. Agora, quando chamamos qualquer função usando o literal Str, na verdade faz referência aoStringmódulo. Isso é muito útil quando usamos nomes de módulo muito longos e queremos substituí-los por outros mais curtos no escopo atual.

NOTE - Aliases MUST comece com uma letra maiúscula.

Os aliases são válidos apenas dentro do lexical scope eles são chamados. Por exemplo, se você tiver 2 módulos em um arquivo e fizer um alias dentro de um dos módulos, esse alias não estará acessível no segundo módulo.

Se você der o nome de um módulo embutido, como String ou Tupla, como um alias para algum outro módulo, para acessar o módulo embutido, você precisará acrescentá-lo com "Elixir.". Por exemplo,

alias List, as: String
#Now when we use String we are actually using List.
#To use the string module: 
IO.puts(Elixir.String.length("Hello"))

Quando o programa acima é executado, ele gera o seguinte resultado -

5

exigir

Elixir fornece macros como um mecanismo para meta-programação (escrever código que gera código).

Macros são pedaços de código que são executados e expandidos no momento da compilação. Isso significa que, para utilizar uma macro, precisamos garantir que seu módulo e implementação estejam disponíveis durante a compilação. Isso é feito com orequire diretiva.

Integer.is_odd(3)

Quando o programa acima for executado, ele gerará o seguinte resultado -

** (CompileError) iex:1: you must require Integer before invoking the macro Integer.is_odd/1

Em Elixir, Integer.is_odd é definido como um macro. Esta macro pode ser usada como um guarda. Isso significa que, a fim de invocarInteger.is_odd, precisaremos do módulo Integer.

Use o require Integer função e execute o programa conforme mostrado abaixo.

require Integer
Integer.is_odd(3)

Desta vez, o programa será executado e produzirá a saída como: true.

Em geral, um módulo não é necessário antes do uso, exceto se quisermos usar as macros disponíveis nesse módulo. Uma tentativa de chamar uma macro que não foi carregada gerará um erro. Observe que, como a diretiva alias, require também tem escopo léxico . Falaremos mais sobre macros em um capítulo posterior.

importar

Nós usamos o importdiretiva para acessar facilmente funções ou macros de outros módulos sem usar o nome totalmente qualificado. Por exemplo, se quisermos usar oduplicate função do módulo List várias vezes, podemos simplesmente importá-la.

import List, only: [duplicate: 2]

Nesse caso, estamos importando apenas a função duplicada (com tamanho de lista de argumentos 2) de List. Apesar:only é opcional, seu uso é recomendado para evitar a importação de todas as funções de um determinado módulo dentro do namespace. :except também pode ser fornecido como uma opção para importar tudo em um módulo, exceto uma lista de funções.

o import diretiva também suporta :macros e :functions para ser dado a :only. Por exemplo, para importar todas as macros, um usuário pode escrever -

import Integer, only: :macros

Observe que a importação também é Lexically scopedassim como as diretivas require e alias. Observe também que'import'ing a module also 'require's it.

usar

Embora não seja uma diretiva, use é uma macro intimamente relacionada com requireque permite que você use um módulo no contexto atual. A macro de uso é frequentemente usada por desenvolvedores para trazer funcionalidade externa para o escopo léxico atual, geralmente módulos. Vamos entender a diretiva de uso por meio de um exemplo -

defmodule Example do 
   use Feature, option: :value 
end

O uso é uma macro que transforma o acima em -

defmodule Example do
   require Feature
   Feature.__using__(option: :value)
end

o use Module primeiro requer o módulo e depois chama o __using__macro no módulo. Elixir tem ótimas capacidades de metaprogramação e macros para gerar código em tempo de compilação. A macro _ _using__ é chamada na instância acima, e o código é injetado em nosso contexto local. O contexto local é onde a macro de uso foi chamada no momento da compilação.

Uma função é um conjunto de instruções organizadas em conjunto para realizar uma tarefa específica. Funções em programação funcionam principalmente como funções em matemática. Você dá às funções alguma entrada, elas geram saída com base na entrada fornecida.

Existem 2 tipos de funções no Elixir -

Função anônima

Funções definidas usando o fn..end constructsão funções anônimas. Essas funções às vezes também são chamadas de lambdas. Eles são usados ​​atribuindo-os a nomes de variáveis.

Função nomeada

Funções definidas usando o def keywordsão funções nomeadas. Estas são funções nativas fornecidas no Elixir.

Funções anônimas

Assim como o nome indica, uma função anônima não tem nome. Freqüentemente, são passados ​​para outras funções. Para definir uma função anônima no Elixir, precisamos dofn e endpalavras-chave. Dentro deles, podemos definir qualquer número de parâmetros e corpos de função separados por->. Por exemplo,

sum = fn (a, b) -> a + b end
IO.puts(sum.(1, 5))

Ao executar o programa acima, é executado, ele gera o seguinte resultado -

6

Observe que essas funções não são chamadas como as funções nomeadas. Nós temos uma '.'entre o nome da função e seus argumentos.

Usando o operador de captura

Também podemos definir essas funções usando o operador de captura. Este é um método mais fácil de criar funções. Agora vamos definir a função de soma acima usando o operador de captura,

sum = &(&1 + &2) 
IO.puts(sum.(1, 2))

Quando o programa acima é executado, ele gera o seguinte resultado -

3

Na versão abreviada, nossos parâmetros não são nomeados, mas estão disponíveis para nós como & 1, & 2, & 3 e assim por diante.

Funções de correspondência de padrões

A correspondência de padrões não se limita apenas a variáveis ​​e estruturas de dados. Podemos usar a correspondência de padrões para tornar nossas funções polimórficas. Por exemplo, vamos declarar uma função que pode pegar 1 ou 2 entradas (dentro de uma tupla) e imprimi-las no console,

handle_result = fn
   {var1} -> IO.puts("#{var1} found in a tuple!")
   {var_2, var_3} -> IO.puts("#{var_2} and #{var_3} found!")
end
handle_result.({"Hey people"})
handle_result.({"Hello", "World"})

Quando o programa acima é executado, ele produz o seguinte resultado -

Hey people found in a tuple!
Hello and World found!

Funções Nomeadas

Podemos definir funções com nomes para que possamos nos referir facilmente a eles mais tarde. As funções nomeadas são definidas dentro de um módulo usando a palavra-chave def. As funções nomeadas são sempre definidas em um módulo. Para chamar funções nomeadas, precisamos referenciá-las usando seu nome de módulo.

A seguir está a sintaxe para funções nomeadas -

def function_name(argument_1, argument_2) do
   #code to be executed when function is called
end

Vamos agora definir nossa soma de função nomeada dentro do módulo Math.

defmodule Math do
   def sum(a, b) do
      a + b
   end
end

IO.puts(Math.sum(5, 6))

Ao executar o programa acima, ele produz o seguinte resultado -

11

Para funções de 1 linha, há uma notação abreviada para definir essas funções, usando do:. Por exemplo -

defmodule Math do
   def sum(a, b), do: a + b
end
IO.puts(Math.sum(5, 6))

Ao executar o programa acima, ele produz o seguinte resultado -

11

Funções Privadas

Elixir nos fornece a capacidade de definir funções privadas que podem ser acessadas de dentro do módulo em que estão definidas. Para definir uma função privada, usedefp ao invés de def. Por exemplo,

defmodule Greeter do
   def hello(name), do: phrase <> name
   defp phrase, do: "Hello "
end

Greeter.hello("world")

Quando o programa acima é executado, ele produz o seguinte resultado -

Hello world

Mas se apenas tentarmos chamar explicitamente a função de frase, usando o Greeter.phrase() função, isso gerará um erro.

Argumentos padrão

Se quisermos um valor padrão para um argumento, usamos o argument \\ value sintaxe -

defmodule Greeter do
   def hello(name, country \\ "en") do
      phrase(country) <> name
   end

   defp phrase("en"), do: "Hello, "
   defp phrase("es"), do: "Hola, "
end

Greeter.hello("Ayush", "en")
Greeter.hello("Ayush")
Greeter.hello("Ayush", "es")

Quando o programa acima é executado, ele produz o seguinte resultado -

Hello, Ayush
Hello, Ayush
Hola, Ayush

Recursão é um método em que a solução de um problema depende das soluções para instâncias menores do mesmo problema. A maioria das linguagens de programação de computador oferece suporte à recursão, permitindo que uma função se chame dentro do texto do programa.

Idealmente, funções recursivas têm uma condição final. Essa condição final, também conhecida como caso base, pára de reinserir a função e adicionar chamadas de função à pilha. É aqui que a chamada de função recursiva pára. Vamos considerar o exemplo a seguir para entender melhor a função recursiva.

defmodule Math do
   def fact(res, num) do
   if num === 1 do
      res
   else
      new_res = res * num
      fact(new_res, num-1)
      end
   end
end

IO.puts(Math.fact(1,5))

Quando o programa acima é executado, ele gera o seguinte resultado -

120

Então, na função acima, Math.fact, estamos calculando o fatorial de um número. Observe que estamos chamando a função dentro dela mesma. Vamos agora entender como isso funciona.

Fornecemos 1 e o número cujo fatorial queremos calcular. A função verifica se o número é 1 ou não e retorna res se for 1(Ending condition). Caso contrário, ele cria uma variável new_res e atribui a ela o valor de res anterior * num atual. Ele retorna o valor retornado por nosso fato de chamada de função (new_res, num-1) . Isso se repete até obtermos num como 1. Quando isso acontece, obtemos o resultado.

Vamos considerar outro exemplo, imprimindo cada elemento da lista um por um. Para fazer isso, vamos utilizar ohd e tl funções de listas e correspondência de padrões em funções -

a = ["Hey", 100, 452, :true, "People"]
defmodule ListPrint do
   def print([]) do
   end
   def print([head | tail]) do 
      IO.puts(head)
      print(tail)
   end
end

ListPrint.print(a)

A primeira função de impressão é chamada quando temos uma lista vazia(ending condition). Caso contrário, a segunda função de impressão será chamada, dividindo a lista em 2 e atribuindo o primeiro elemento da lista ao cabeçalho e o restante da lista ao final. A cabeça é então impressa e chamamos a função de impressão novamente com o resto da lista, ou seja, cauda. Quando o programa acima é executado, ele produz o seguinte resultado -

Hey
100
452
true
People

Devido à imutabilidade, os loops no Elixir (como em qualquer linguagem de programação funcional) são escritos de forma diferente das linguagens imperativas. Por exemplo, em uma linguagem imperativa como C, você escreverá -

for(i = 0; i < 10; i++) {
   printf("%d", array[i]);
}

No exemplo dado acima, estamos alterando a matriz e a variável i. A mutação não é possível no Elixir. Em vez disso, as linguagens funcionais dependem da recursão: uma função é chamada recursivamente até que uma condição seja alcançada que interrompa a ação recursiva de continuar. Nenhum dado é alterado neste processo.

Vamos agora escrever um loop simples usando recursão que imprime olá n vezes.

defmodule Loop do
   def print_multiple_times(msg, n) when n <= 1 do
      IO.puts msg
   end

   def print_multiple_times(msg, n) do
      IO.puts msg
      print_multiple_times(msg, n - 1)
   end
end

Loop.print_multiple_times("Hello", 10)

Quando o programa acima é executado, ele produz o seguinte resultado -

Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello
Hello

Utilizamos técnicas de correspondência de padrões de função e recursão para implementar um loop com sucesso. As definições recursivas são difíceis de entender, mas converter loops em recursão é fácil.

Elixir nos fornece o Enum module. Este módulo é usado para as chamadas de loop mais iterativas, pois é muito mais fácil usá-las do que tentar descobrir as definições recursivas para as mesmas. Discutiremos isso no próximo capítulo. Suas próprias definições recursivas só devem ser usadas quando você não encontrar uma solução usando esse módulo. Essas funções são otimizadas para chamada final e bastante rápidas.

Um enumerável é um objeto que pode ser enumerado. "Enumerado" significa contar os membros de um conjunto / coleção / categoria um por um (geralmente em ordem, geralmente por nome).

Elixir fornece o conceito de enumeráveis ​​e o módulo Enum para trabalhar com eles. As funções no módulo Enum são limitadas, como o nome diz, a enumerar valores em estruturas de dados. Exemplo de uma estrutura de dados enumerável é uma lista, tupla, mapa, etc. O módulo Enum nos fornece um pouco mais de 100 funções para lidar com enums. Discutiremos algumas funções importantes neste capítulo.

Todas essas funções tomam um enumerável como o primeiro elemento e uma função como o segundo e trabalham sobre eles. As funções são descritas a seguir.

todos?

Quando usamos all? , a coleção inteira deve ser avaliada como verdadeira, caso contrário, será retornado falso. Por exemplo, para verificar se todos os elementos da lista são números ímpares, então.

res = Enum.all?([1, 2, 3, 4], fn(s) -> rem(s,2) == 1 end) 
IO.puts(res)

Quando o programa acima é executado, ele produz o seguinte resultado -

false

Isso ocorre porque nem todos os elementos desta lista são estranhos.

qualquer?

Como o nome sugere, esta função retorna verdadeiro se qualquer elemento da coleção for avaliado como verdadeiro. Por exemplo -

res = Enum.any?([1, 2, 3, 4], fn(s) -> rem(s,2) == 1 end)
IO.puts(res)

Quando o programa acima é executado, ele produz o seguinte resultado -

true

pedaço

Esta função divide nossa coleção em pequenos pedaços do tamanho fornecido como o segundo argumento. Por exemplo -

res = Enum.chunk([1, 2, 3, 4, 5, 6], 2)
IO.puts(res)

Quando o programa acima é executado, ele produz o seguinte resultado -

[[1, 2], [3, 4], [5, 6]]

cada

Pode ser necessário iterar sobre uma coleção sem produzir um novo valor, para este caso, usamos o each função -

Enum.each(["Hello", "Every", "one"], fn(s) -> IO.puts(s) end)

Quando o programa acima é executado, ele produz o seguinte resultado -

Hello
Every
one

mapa

Para aplicar nossa função a cada item e produzir uma nova coleção, usamos a função de mapa. É uma das construções mais úteis na programação funcional, pois é bastante expressiva e curta. Vamos considerar um exemplo para entender isso. Vamos dobrar os valores armazenados em uma lista e armazená-los em uma nova listares -

res = Enum.map([2, 5, 3, 6], fn(a) -> a*2 end)
IO.puts(res)

Quando o programa acima é executado, ele produz o seguinte resultado -

[4, 10, 6, 12]

reduzir

o reducefunção nos ajuda a reduzir nosso enumerável a um único valor. Para fazer isso, fornecemos um acumulador opcional (5 neste exemplo) para ser passado para nossa função; se nenhum acumulador for fornecido, o primeiro valor será usado -

res = Enum.reduce([1, 2, 3, 4], 5, fn(x, accum) -> x + accum end)
IO.puts(res)

Quando o programa acima é executado, ele produz o seguinte resultado -

15

O acumulador é o valor inicial passado para o fn. Da segunda chamada em diante, o valor retornado da chamada anterior é passado como acum. Também podemos usar reduzir sem o acumulador -

res = Enum.reduce([1, 2, 3, 4], fn(x, accum) -> x + accum end)
IO.puts(res)

Quando o programa acima é executado, ele produz o seguinte resultado -

10

uniq

A função uniq remove duplicatas de nossa coleção e retorna apenas o conjunto de elementos da coleção. Por exemplo -

res = Enum.uniq([1, 2, 2, 3, 3, 3, 4, 4, 4, 4])
IO.puts(res)

Ao executar o programa acima, ele produz o seguinte resultado -

[1, 2, 3, 4]

Avaliação Eager

Todas as funções no módulo Enum são ansiosas. Muitas funções esperam um enumerável e retornam uma lista. Isso significa que ao realizar várias operações com Enum, cada operação vai gerar uma lista intermediária até chegarmos ao resultado. Vamos considerar o seguinte exemplo para entender isso -

odd? = &(odd? = &(rem(&1, 2) != 0) 
res = 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum 
IO.puts(res)

Quando o programa acima é executado, ele produz o seguinte resultado -

7500000000

O exemplo acima tem um pipeline de operações. Começamos com um intervalo e, em seguida, multiplicamos cada elemento no intervalo por 3. Esta primeira operação criará e retornará uma lista com 100_000 itens. Em seguida, mantemos todos os elementos ímpares da lista, gerando uma nova lista, agora com 50_000 itens, e somamos todas as entradas.

o |> símbolo usado no trecho acima é o pipe operator: ele simplesmente pega a saída da expressão em seu lado esquerdo e a passa como o primeiro argumento para a chamada de função em seu lado direito. É semelhante ao Unix | operador. Seu objetivo é destacar o fluxo de dados sendo transformado por uma série de funções.

Sem o pipe operador, o código parece complicado -

Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))

Temos muitas outras funções, no entanto, apenas algumas importantes foram descritas aqui.

Muitas funções esperam um enumerável e retornam um listde volta. Isso significa que, ao realizar várias operações com Enum, cada operação vai gerar uma lista intermediária até chegarmos ao resultado.

Streams oferecem suporte a operações lazy em oposição a operações ansiosas por enums. Em resumo,streams are lazy, composable enumerables. Isso significa que o Streams não executa uma operação a menos que seja absolutamente necessário. Vamos considerar um exemplo para entender isso -

odd? = &(rem(&1, 2) != 0)
res = 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum
IO.puts(res)

Quando o programa acima é executado, ele produz o seguinte resultado -

7500000000

No exemplo dado acima, 1..100_000 |> Stream.map(&(&1 * 3))retorna um tipo de dados, um fluxo real, que representa a computação do mapa no intervalo 1..100_000. Ainda não avaliou esta representação. Em vez de gerar listas intermediárias, os fluxos criam uma série de cálculos que são invocados apenas quando passamos o fluxo subjacente para o módulo Enum. Os fluxos são úteis ao trabalhar com coleções grandes, possivelmente infinitas.

Streams e enums têm muitas funções em comum. Streams fornecem principalmente as mesmas funções fornecidas pelo módulo Enum que gerou Listas como seus valores de retorno após realizar cálculos em enumeráveis ​​de entrada. Alguns deles estão listados na tabela a seguir -

Sr. Não. Função e sua descrição
1

chunk(enum, n, step, leftover \\ nil)

Transmite o enumerável em blocos, contendo n itens cada, onde cada novo bloco inicia os elementos da etapa no enumerável.

2

concat(enumerables)

Cria um fluxo que enumera cada enumerável em um enumerável.

3

each(enum, fun)

Executa a função fornecida para cada item.

4

filter(enum, fun)

Cria um fluxo que filtra os elementos de acordo com a função fornecida na enumeração.

5

map(enum, fun)

Cria um fluxo que aplicará a função fornecida na enumeração.

6

drop(enum, n)

Descarta preguiçosamente os próximos n itens do enumerável.

Structs são extensões construídas sobre mapas que fornecem verificações de tempo de compilação e valores padrão.

Definindo Structs

Para definir uma estrutura, a construção defstruct é usada -

defmodule User do
   defstruct name: "John", age: 27
end

A lista de palavras-chave usada com defstruct define quais campos a estrutura terá junto com seus valores padrão. As estruturas levam o nome do módulo em que são definidas. No exemplo dado acima, definimos uma estrutura chamada Usuário. Agora podemos criar estruturas de usuário usando uma sintaxe semelhante à usada para criar mapas -

new_john = %User{})
ayush = %User{name: "Ayush", age: 20}
megan = %User{name: "Megan"})

O código acima irá gerar três estruturas diferentes com valores -

%User{age: 27, name: "John"}
%User{age: 20, name: "Ayush"}
%User{age: 27, name: "Megan"}

As estruturas fornecem garantias em tempo de compilação de que apenas os campos (e todos eles) definidos por meio de defstruct terão permissão para existir em uma estrutura. Portanto, você não pode definir seus próprios campos depois de criar a estrutura no módulo.

Acessando e atualizando estruturas

Quando discutimos mapas, mostramos como podemos acessar e atualizar os campos de um mapa. As mesmas técnicas (e a mesma sintaxe) também se aplicam a estruturas. Por exemplo, se quisermos atualizar o usuário que criamos no exemplo anterior, então -

defmodule User do
   defstruct name: "John", age: 27
end
john = %User{}
#john right now is: %User{age: 27, name: "John"}

#To access name and age of John, 
IO.puts(john.name)
IO.puts(john.age)

Quando o programa acima é executado, ele produz o seguinte resultado -

John
27

Para atualizar um valor em uma estrutura, usaremos novamente o mesmo procedimento que usamos no capítulo do mapa,

meg = %{john | name: "Meg"}

As estruturas também podem ser usadas na correspondência de padrões, tanto para correspondência no valor de chaves específicas quanto para garantir que o valor correspondente seja uma estrutura do mesmo tipo do valor correspondido.

Os protocolos são um mecanismo para alcançar polimorfismo no Elixir. O despacho em um protocolo está disponível para qualquer tipo de dados, desde que implemente o protocolo.

Vamos considerar um exemplo de uso de protocolos. Usamos uma função chamadato_stringnos capítulos anteriores para converter de outros tipos para o tipo string. Este é realmente um protocolo. Ele atua de acordo com a entrada fornecida sem produzir um erro. Pode parecer que estamos discutindo funções de correspondência de padrões, mas à medida que avançamos, fica diferente.

Considere o exemplo a seguir para entender melhor o mecanismo do protocolo.

Vamos criar um protocolo que mostrará se a entrada fornecida está vazia ou não. Vamos chamar este protocoloblank?.

Definindo um Protocolo

Podemos definir um protocolo no Elixir da seguinte maneira -

defprotocol Blank do
   def blank?(data)
end

Como você pode ver, não precisamos definir um corpo para a função. Se você está familiarizado com interfaces em outras linguagens de programação, pode pensar em um protocolo como essencialmente a mesma coisa.

Portanto, este Protocolo está dizendo que qualquer coisa que o implemente deve ter um empty?função, embora seja responsabilidade do implementador como a função responde. Com o protocolo definido, vamos entender como adicionar algumas implementações.

Implementando um Protocolo

Como definimos um protocolo, agora precisamos dizer a ele como lidar com as diferentes entradas que ele pode obter. Vamos continuar com o exemplo que tínhamos tomado anteriormente. Vamos implementar o protocolo em branco para listas, mapas e strings. Isso mostrará se o que passamos está em branco ou não.

#Defining the protocol
defprotocol Blank do
   def blank?(data)
end

#Implementing the protocol for lists
defimpl Blank, for: List do
   def blank?([]), do: true
   def blank?(_), do: false
end

#Implementing the protocol for strings
defimpl Blank, for: BitString do
   def blank?(""), do: true
   def blank?(_), do: false
end

#Implementing the protocol for maps
defimpl Blank, for: Map do
   def blank?(map), do: map_size(map) == 0
end

IO.puts(Blank.blank? [])
IO.puts(Blank.blank? [:true, "Hello"])
IO.puts(Blank.blank? "")
IO.puts(Blank.blank? "Hi")

Você pode implementar seu protocolo para quantos ou poucos tipos desejar, o que fizer sentido para o uso de seu protocolo. Este foi um caso de uso bastante básico de protocolos. Quando o programa acima é executado, ele produz o seguinte resultado -

true
false
true
false

Note - Se você usar isso para qualquer tipo diferente daqueles para os quais definiu o protocolo, ocorrerá um erro.

O File IO é parte integrante de qualquer linguagem de programação, pois permite que a linguagem interaja com os arquivos no sistema de arquivos. Neste capítulo, discutiremos dois módulos - Caminho e Arquivo.

O Módulo Path

o pathmodule é um módulo muito pequeno que pode ser considerado um módulo auxiliar para operações do sistema de arquivos. A maioria das funções no módulo Arquivo espera caminhos como argumentos. Mais comumente, esses caminhos serão binários regulares. O módulo Path fornece recursos para trabalhar com esses caminhos. É preferível usar funções do módulo Path em vez de apenas manipular binários, uma vez que o módulo Path cuida de diferentes sistemas operacionais de forma transparente. Deve-se observar que Elixir converterá automaticamente barras (/) em barras invertidas (\) no Windows ao executar operações de arquivo.

Vamos considerar o exemplo a seguir para entender melhor o módulo Path -

IO.puts(Path.join("foo", "bar"))

Quando o programa acima é executado, ele produz o seguinte resultado -

foo/bar

Existem muitos métodos que o módulo de caminho fornece. Você pode dar uma olhada nos diferentes métodos aqui . Esses métodos são freqüentemente usados ​​se você estiver executando muitas operações de manipulação de arquivo.

O Módulo de Arquivo

O módulo de arquivo contém funções que nos permitem abrir arquivos como dispositivos IO. Por padrão, os arquivos são abertos no modo binário, o que requer que os desenvolvedores usem oIO.binread e IO.binwritefunções do módulo IO. Vamos criar um arquivo chamadonewfile e escrever alguns dados nele.

{:ok, file} = File.read("newfile", [:write]) 
# Pattern matching to store returned stream
IO.binwrite(file, "This will be written to the file")

Se você abrir o arquivo que acabamos de escrever, o conteúdo será exibido da seguinte maneira -

This will be written to the file

Vamos agora entender como usar o módulo de arquivo.

Abrindo um arquivo

Para abrir um arquivo, podemos usar qualquer uma das 2 funções a seguir -

{:ok, file} = File.open("newfile")
file = File.open!("newfile")

Vamos agora entender a diferença entre o File.open função e o File.open!() função.

  • o File.openfunção sempre retorna uma tupla. Se o arquivo for aberto com sucesso, ele retorna o primeiro valor na tupla como:oke o segundo valor é literal do tipo io_device. Se um erro for causado, ele retornará uma tupla com o primeiro valor como:error e o segundo valor como o motivo.

  • o File.open!() por outro lado, retornará um io_devicese o arquivo for aberto com sucesso, outro erro será gerado. NOTA: Este é o padrão seguido em todas as funções do módulo de arquivo que iremos discutir.

Também podemos especificar os modos em que queremos abrir este arquivo. Para abrir um arquivo como somente leitura e no modo de codificação utf-8, usamos o seguinte código -

file = File.open!("newfile", [:read, :utf8])

Gravando em um Arquivo

Temos duas maneiras de gravar arquivos. Vejamos o primeiro usando a função de gravação do módulo Arquivo.

File.write("newfile", "Hello")

Mas isso não deve ser usado se você estiver fazendo várias gravações no mesmo arquivo. Cada vez que essa função é chamada, um descritor de arquivo é aberto e um novo processo é gerado para gravar no arquivo. Se você estiver fazendo várias gravações em um loop, abra o arquivo viaFile.opene escreva para ele usando os métodos do módulo IO. Vamos considerar um exemplo para entender o mesmo -

#Open the file in read, write and utf8 modes. 
file = File.open!("newfile_2", [:read, :utf8, :write])

#Write to this "io_device" using standard IO functions
IO.puts(file, "Random text")

Você pode usar outros métodos do módulo IO, como IO.write e IO.binwrite para gravar em arquivos abertos como io_device.

Lendo de um arquivo

Temos duas maneiras de ler arquivos. Vamos ver o primeiro usando a função de leitura do módulo Arquivo.

IO.puts(File.read("newfile"))

Ao executar este código, você deve obter uma tupla com o primeiro elemento como :ok e o segundo como o conteúdo de newfile

Também podemos usar o File.read! função apenas para obter o conteúdo dos arquivos retornados para nós.

Fechando um arquivo aberto

Sempre que você abre um arquivo usando a função File.open, após terminar de usá-lo, você deve fechá-lo usando o File.close função -

File.close(file)

No Elixir, todo código é executado dentro de processos. Os processos são isolados uns dos outros, são executados simultaneamente entre si e se comunicam por meio da passagem de mensagens. Os processos do Elixir não devem ser confundidos com os processos do sistema operacional. Os processos no Elixir são extremamente leves em termos de memória e CPU (ao contrário dos threads em muitas outras linguagens de programação). Por causa disso, não é incomum ter dezenas ou até centenas de milhares de processos em execução simultaneamente.

Neste capítulo, aprenderemos sobre as construções básicas para gerar novos processos, bem como enviar e receber mensagens entre diferentes processos.

A Função de Spawn

A maneira mais fácil de criar um novo processo é usar o spawnfunção. ospawnaceita uma função que será executada no novo processo. Por exemplo -

pid = spawn(fn -> 2 * 2 end)
Process.alive?(pid)

Quando o programa acima é executado, ele produz o seguinte resultado -

false

O valor de retorno da função spawn é um PID. Este é um identificador único para o processo e, portanto, se você executar o código acima do seu PID, ele será diferente. Como você pode ver neste exemplo, o processo está morto quando verificamos se ele está vivo. Isso ocorre porque o processo será encerrado assim que terminar de executar a função fornecida.

Como já mencionado, todos os códigos Elixir são executados dentro de processos. Se você executar a função própria, verá o PID para sua sessão atual -

pid = self
 
Process.alive?(pid)

Quando o programa acima é executado, ele produz o seguinte resultado -

true

Passagem de mensagens

Podemos enviar mensagens para um processo com send e recebê-los com receive. Vamos passar uma mensagem para o processo atual e recebê-la no mesmo.

send(self(), {:hello, "Hi people"})

receive do
   {:hello, msg} -> IO.puts(msg)
   {:another_case, msg} -> IO.puts("This one won't match!")
end

Quando o programa acima é executado, ele produz o seguinte resultado -

Hi people

Enviamos uma mensagem para o processo atual usando a função send e a passamos para o próprio PID. Em seguida, lidamos com a mensagem recebida usando oreceive função.

Quando uma mensagem é enviada para um processo, a mensagem é armazenada no process mailbox. O bloco de recebimento passa pela caixa de correio do processo atual em busca de uma mensagem que corresponda a qualquer um dos padrões fornecidos. O bloco de recebimento oferece suporte a guardas e muitas cláusulas, como o caso.

Se não houver nenhuma mensagem na caixa de correio que corresponda a qualquer um dos padrões, o processo atual aguardará até que uma mensagem correspondente chegue. Um tempo limite também pode ser especificado. Por exemplo,

receive do
   {:hello, msg}  -> msg
after
   1_000 -> "nothing after 1s"
end

Quando o programa acima é executado, ele produz o seguinte resultado -

nothing after 1s

NOTE - Um tempo limite de 0 pode ser fornecido quando você já espera que a mensagem esteja na caixa de correio.

Links

A forma mais comum de desova no Elixir é, na verdade, via spawn_linkfunção. Antes de dar uma olhada em um exemplo com spawn_link, vamos entender o que acontece quando um processo falha.

spawn fn -> raise "oops" end

Quando o programa acima é executado, ele produz o seguinte erro -

[error] Process #PID<0.58.00> raised an exception
** (RuntimeError) oops
   :erlang.apply/2

Ele registrou um erro, mas o processo de geração ainda está em execução. Isso ocorre porque os processos são isolados. Se quisermos que a falha de um processo se propague para outro, precisamos vinculá-los. Isso pode ser feito com ospawn_linkfunção. Vamos considerar um exemplo para entender o mesmo -

spawn_link fn -> raise "oops" end

Quando o programa acima é executado, ele produz o seguinte erro -

** (EXIT from #PID<0.41.0>) an exception was raised:
   ** (RuntimeError) oops
      :erlang.apply/2

Se você está executando isso em iexo shell trata desse erro e não fecha. Mas se você executar primeiro fazendo um arquivo de script e, em seguida, usandoelixir <file-name>.exs, o processo pai também será desativado devido a essa falha.

Processos e links desempenham um papel importante na construção de sistemas tolerantes a falhas. Em aplicativos Elixir, frequentemente vinculamos nossos processos a supervisores que detectarão quando um processo morre e iniciarão um novo processo em seu lugar. Isso só é possível porque os processos são isolados e não compartilham nada por padrão. E, como os processos são isolados, não há como uma falha em um processo travar ou corromper o estado de outro. Enquanto outras linguagens exigem que capturemos / tratemos exceções; no Elixir, não há problema em deixar os processos falharem porque esperamos que os supervisores reiniciem nossos sistemas adequadamente.

Estado

Se você estiver criando um aplicativo que requer estado, por exemplo, para manter a configuração do aplicativo, ou precisar analisar um arquivo e mantê-lo na memória, onde o armazenaria? A funcionalidade de processo do Elixir pode ser útil ao fazer essas coisas.

Podemos escrever processos com loop infinito, manter o estado e enviar e receber mensagens. Como exemplo, vamos escrever um módulo que inicia novos processos que funcionam como um armazenamento de valor-chave em um arquivo chamadokv.exs.

defmodule KV do
   def start_link do
      Task.start_link(fn -> loop(%{}) end)
   end

   defp loop(map) do
      receive do
         {:get, key, caller} ->
         send caller, Map.get(map, key)
         loop(map)
         {:put, key, value} ->
         loop(Map.put(map, key, value))
      end
   end
end

Observe que o start_link função inicia um novo processo que executa o loopfunção, começando com um mapa vazio. oloopa função então espera por mensagens e executa a ação apropriada para cada mensagem. No caso de um:getmensagem, ele envia uma mensagem de volta para o chamador e faz um loop de chamadas novamente, para esperar por uma nova mensagem. Enquanto o:put a mensagem realmente invoca loop com uma nova versão do mapa, com a chave e o valor fornecidos armazenados.

Vamos agora executar o seguinte -

iex kv.exs

Agora você deve estar em seu iexConcha. Para testar nosso módulo, tente o seguinte -

{:ok, pid} = KV.start_link

# pid now has the pid of our new process that is being 
# used to get and store key value pairs 

# Send a KV pair :hello, "Hello" to the process
send pid, {:put, :hello, "Hello"}

# Ask for the key :hello
send pid, {:get, :hello, self()}

# Print all the received messages on the current process.
flush()

Quando o programa acima é executado, ele produz o seguinte resultado -

"Hello"

Neste capítulo, vamos explorar sigilos, os mecanismos fornecidos pela linguagem para trabalhar com representações textuais. Os sigilos começam com o caractere til (~) que é seguido por uma letra (que identifica o sigilo) e então um delimitador; opcionalmente, modificadores podem ser adicionados após o delimitador final.

Regex

Regexes no Elixir são sigilos. Vimos seu uso no capítulo String. Vamos novamente dar um exemplo para ver como podemos usar regex no Elixir.

# A regular expression that matches strings which contain "foo" or
# "bar":
regex = ~r/foo|bar/
IO.puts("foo" =~ regex)
IO.puts("baz" =~ regex)

Quando o programa acima é executado, ele produz o seguinte resultado -

true
false

Sigilos suportam 8 delimitadores diferentes -

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

A razão por trás do suporte a delimitadores diferentes é que delimitadores diferentes podem ser mais adequados para sigilos diferentes. Por exemplo, usar parênteses para expressões regulares pode ser uma escolha confusa, pois eles podem se misturar com os parênteses dentro da regex. No entanto, parênteses podem ser úteis para outros sigilos, como veremos na próxima seção.

Elixir suporta regexes compatíveis com Perl e também suporta modificadores. Você pode ler mais sobre o uso de regexes aqui .

Strings, listas de caracteres e listas de palavras

Além de regexes, Elixir tem mais 3 sigilos embutidos. Vamos dar uma olhada nos sigilos.

Cordas

O sigilo do ~ s é usado para gerar strings, como as aspas duplas. O sigilo do ~ s é útil, por exemplo, quando uma string contém aspas duplas e simples -

new_string = ~s(this is a string with "double" quotes, not 'single' ones)
IO.puts(new_string)

Este sigilo gera cordas. Quando o programa acima é executado, ele produz o seguinte resultado -

"this is a string with \"double\" quotes, not 'single' ones"

Listas de caracteres

O sigilo ~ c é usado para gerar listas de caracteres -

new_char_list = ~c(this is a char list containing 'single quotes')
IO.puts(new_char_list)

Quando o programa acima é executado, ele produz o seguinte resultado -

this is a char list containing 'single quotes'

Listas de Palavras

O sigilo ~ w é usado para gerar listas de palavras (palavras são apenas strings regulares). Dentro do sigilo ~ w, as palavras são separadas por espaços em branco.

new_word_list = ~w(foo bar bat)
IO.puts(new_word_list)

Quando o programa acima é executado, ele produz o seguinte resultado -

foobarbat

O sigilo ~ w também aceita o c, s e a modificadores (para listas de caracteres, strings e átomos, respectivamente), que especificam o tipo de dados dos elementos da lista resultante -

new_atom_list = ~w(foo bar bat)a
IO.puts(new_atom_list)

Quando o programa acima é executado, ele produz o seguinte resultado -

[:foo, :bar, :bat]

Interpolação e fuga em sigilos

Além de sigilos em minúsculas, Elixir suporta sigilos em maiúsculas para lidar com caracteres de escape e interpolação. Enquanto ~ se ~ S retornarão strings, o primeiro permite códigos de escape e interpolação, enquanto o último não. Vamos considerar um exemplo para entender isso -

~s(String with escape codes \x26 #{"inter" <> "polation"})
# "String with escape codes & interpolation"
~S(String without escape codes \x26 without #{interpolation})
# "String without escape codes \\x26 without \#{interpolation}"

Sigilos Personalizados

Podemos criar facilmente nossos próprios sigilos personalizados. Neste exemplo, criaremos um sigilo para converter uma string em maiúsculas.

defmodule CustomSigil do
   def sigil_u(string, []), do: String.upcase(string)
end

import CustomSigil

IO.puts(~u/tutorials point/)

Quando executamos o código acima, ele produz o seguinte resultado -

TUTORIALS POINT

Primeiro definimos um módulo chamado CustomSigil e dentro desse módulo, criamos uma função chamada sigil_u. Como não há sigilo ~ u existente no espaço de sigilos existente, nós o usaremos. O _u indica que desejamos usar u como o caractere após o til. A definição da função deve levar dois argumentos, uma entrada e uma lista.

Compreensões de listas são açúcares sintáticos para percorrer enumerables no Elixir. Neste capítulo, usaremos compreensões para iteração e geração.

Fundamentos

Quando examinamos o módulo Enum no capítulo de enumeráveis, encontramos a função map.

Enum.map(1..3, &(&1 * 2))

Neste exemplo, passaremos uma função como o segundo argumento. Cada item no intervalo será passado para a função e, em seguida, uma nova lista será retornada contendo os novos valores.

Mapear, filtrar e transformar são ações muito comuns no Elixir e, portanto, há uma maneira ligeiramente diferente de obter o mesmo resultado do exemplo anterior -

for n <- 1..3, do: n * 2

Quando executamos o código acima, ele produz o seguinte resultado -

[2, 4, 6]

O segundo exemplo é uma compreensão e, como você provavelmente pode ver, é simplesmente um açúcar sintático para o que você também pode alcançar se usar o Enum.mapfunção. No entanto, não há benefícios reais em usar uma compreensão sobre uma função do módulo Enum em termos de desempenho.

As compreensões não se limitam a listas, mas podem ser usadas com todos os enumeráveis.

Filtro

Você pode pensar nos filtros como uma espécie de guarda para as compreensões. Quando um valor filtrado retornafalse ou nilele é excluído da lista final. Vamos percorrer um intervalo e nos preocupar apenas com os números pares. Vamos usar ois_even função do módulo Integer para verificar se um valor é par ou não.

import Integer
IO.puts(for x <- 1..10, is_even(x), do: x)

Quando o código acima é executado, ele produz o seguinte resultado -

[2, 4, 6, 8, 10]

Também podemos usar vários filtros na mesma compreensão. Adicione outro filtro que você deseja após ois_even filtro separado por uma vírgula.

: na opção

Nos exemplos acima, todas as compreensões retornaram listas como resultado. No entanto, o resultado de uma compreensão pode ser inserido em diferentes estruturas de dados, passando o:into opção para a compreensão.

Por exemplo, um bitstring gerador pode ser usado com a opção: into para remover facilmente todos os espaços em uma string -

IO.puts(for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>)

Quando o código acima é executado, ele produz o seguinte resultado -

helloworld

O código acima remove todos os espaços da string usando c != ?\s filtrar e usar a opção: into, ele coloca todos os caracteres retornados em uma string.

Elixir é uma linguagem tipada dinamicamente, então todos os tipos no Elixir são inferidos pelo tempo de execução. No entanto, Elixir vem com typespecs, que são uma notação usada paradeclaring custom data types and declaring typed function signatures (specifications).

Especificações de função (especificações)

Por padrão, Elixir fornece alguns tipos básicos, como inteiro ou pid, e também tipos complexos: por exemplo, o roundfunção, que arredonda um float para seu inteiro mais próximo, recebe um número como argumento (um inteiro ou um float) e retorna um inteiro. Na documentação relacionada , a assinatura digitada por rodada é escrita como -

round(number) :: integer

A descrição acima implica que a função à esquerda toma como argumento o que está especificado entre parênteses e retorna o que está à direita de ::, ou seja, Inteiro. As especificações da função são escritas com o@spec, colocada logo antes da definição da função. A função round pode ser escrita como -

@spec round(number) :: integer
def round(number), do: # Function implementation
...

Tipos de especificações também suportam tipos complexos, por exemplo, se você deseja retornar uma lista de inteiros, então você pode usar [Integer]

Tipos personalizados

Embora o Elixir forneça muitos tipos embutidos úteis, é conveniente definir tipos personalizados quando apropriado. Isso pode ser feito ao definir módulos por meio da diretiva @type. Vamos considerar um exemplo para entender o mesmo -

defmodule FunnyCalculator do
   @type number_with_joke :: {number, String.t}

   @spec add(number, number) :: number_with_joke
   def add(x, y), do: {x + y, "You need a calculator to do that?"}

   @spec multiply(number, number) :: number_with_joke
   def multiply(x, y), do: {x * y, "It is like addition on steroids."}
end

{result, comment} = FunnyCalculator.add(10, 20)
IO.puts(result)
IO.puts(comment)

Quando o programa acima é executado, ele produz o seguinte resultado -

30
You need a calculator to do that?

NOTE - Os tipos personalizados definidos por meio de @type são exportados e estão disponíveis fora do módulo em que foram definidos. Se quiser manter um tipo personalizado privado, você pode usar o @typep diretiva em vez de @type.

Comportamentos em Elixir (e Erlang) são uma maneira de separar e abstrair a parte genérica de um componente (que se torna o módulo de comportamento) da parte específica (que se torna o módulo de retorno de chamada). Os comportamentos fornecem uma maneira de -

  • Defina um conjunto de funções que devem ser implementadas por um módulo.
  • Certifique-se de que um módulo implemente todas as funções desse conjunto.

Se for necessário, você pode pensar em comportamentos como interfaces em linguagens orientadas a objetos, como Java: um conjunto de assinaturas de função que um módulo deve implementar.

Definindo um comportamento

Vamos considerar um exemplo para criar nosso próprio comportamento e, em seguida, usar esse comportamento genérico para criar um módulo. Vamos definir um comportamento que cumprimenta as pessoas com um alô e um adeus em diferentes idiomas.

defmodule GreetBehaviour do
   @callback say_hello(name :: string) :: nil
   @callback say_bye(name :: string) :: nil
end

o @callbackdiretiva é usada para listar as funções que os módulos de adoção precisarão definir. Ele também especifica o não. de argumentos, seu tipo e seus valores de retorno.

Adotando um comportamento

Definimos um comportamento com sucesso. Agora vamos adotá-lo e implementá-lo em vários módulos. Vamos criar dois módulos implementando esse comportamento em inglês e espanhol.

defmodule GreetBehaviour do
   @callback say_hello(name :: string) :: nil
   @callback say_bye(name :: string) :: nil
end

defmodule EnglishGreet do
   @behaviour GreetBehaviour
   def say_hello(name), do: IO.puts("Hello " <> name)
   def say_bye(name), do: IO.puts("Goodbye, " <> name)
end

defmodule SpanishGreet do
   @behaviour GreetBehaviour
   def say_hello(name), do: IO.puts("Hola " <> name)
   def say_bye(name), do: IO.puts("Adios " <> name)
end

EnglishGreet.say_hello("Ayush")
EnglishGreet.say_bye("Ayush")
SpanishGreet.say_hello("Ayush")
SpanishGreet.say_bye("Ayush")

Quando o programa acima é executado, ele produz o seguinte resultado -

Hello Ayush
Goodbye, Ayush
Hola Ayush
Adios Ayush

Como você já viu, adotamos um comportamento usando o @behaviourdiretiva no módulo. Temos que definir todas as funções implementadas no comportamento para todos os módulos filhos . Isso pode ser considerado aproximadamente equivalente a interfaces em linguagens OOP.

Elixir tem três mecanismos de erro: erros, lançamentos e saídas. Vamos explorar cada mecanismo em detalhes.

Erro

Erros (ou exceções) são usados ​​quando coisas excepcionais acontecem no código. Um exemplo de erro pode ser recuperado ao tentar adicionar um número em uma string -

IO.puts(1 + "Hello")

Quando o programa acima é executado, ele produz o seguinte erro -

** (ArithmeticError) bad argument in arithmetic expression
   :erlang.+(1, "Hello")

Este foi um exemplo de erro embutido.

Levantando Erros

Podemos raiseerros usando as funções de aumento. Vamos considerar um exemplo para entender o mesmo -

#Runtime Error with just a message
raise "oops"  # ** (RuntimeError) oops

Outros erros podem ser gerados com raise / 2 passando o nome do erro e uma lista de argumentos de palavras-chave

#Other error type with a message
raise ArgumentError, message: "invalid argument foo"

Você também pode definir seus próprios erros e aumentá-los. Considere o seguinte exemplo -

defmodule MyError do
   defexception message: "default message"
end

raise MyError  # Raises error with default message
raise MyError, message: "custom message"  # Raises error with custom message

Resgatando Erros

Não queremos que nossos programas sejam encerrados abruptamente, mas os erros precisam ser tratados com cuidado. Para isso, usamos o tratamento de erros. Nósrescue erros usando o try/rescueconstruir. Vamos considerar o seguinte exemplo para entender o mesmo -

err = try do
   raise "oops"
rescue
   e in RuntimeError -> e
end

IO.puts(err.message)

Quando o programa acima é executado, ele produz o seguinte resultado -

oops

Tratamos os erros na instrução de resgate usando correspondência de padrões. Se não tivermos nenhum uso do erro, e apenas quisermos usá-lo para fins de identificação, também podemos usar o formulário -

err = try do
   1 + "Hello"
rescue
   RuntimeError -> "You've got a runtime error!"
   ArithmeticError -> "You've got a Argument error!"
end

IO.puts(err)

Ao executar o programa acima, ele produz o seguinte resultado -

You've got a Argument error!

NOTE- A maioria das funções na biblioteca padrão Elixir são implementadas duas vezes, uma retornando tuplas e a outra gerando erros. Por exemplo, oFile.read e a File.read!funções. O primeiro retornou uma tupla se o arquivo foi lido com sucesso e se um erro foi encontrado, esta tupla foi usada para dar o motivo do erro. O segundo gerava um erro se um erro fosse encontrado.

Se usarmos a abordagem da primeira função, precisamos usar o caso para o padrão que corresponde ao erro e agir de acordo com isso. No segundo caso, usamos a abordagem de recuperação de tentativa para código sujeito a erros e tratamos os erros de acordo.

Lança

No Elixir, um valor pode ser lançado e depois capturado. Throw e Catch são reservados para situações em que não é possível recuperar um valor, a menos que seja usando throw e catch.

As instâncias são bastante incomuns na prática, exceto na interface com bibliotecas. Por exemplo, vamos supor agora que o módulo Enum não forneceu nenhuma API para encontrar um valor e que precisávamos encontrar o primeiro múltiplo de 13 em uma lista de números -

val = try do
   Enum.each 20..100, fn(x) ->
      if rem(x, 13) == 0, do: throw(x)
   end
   "Got nothing"
catch
   x -> "Got #{x}"
end

IO.puts(val)

Quando o programa acima é executado, ele produz o seguinte resultado -

Got 26

Saída

Quando um processo morre de “causas naturais” (por exemplo, exceções não tratadas), ele envia um sinal de saída. Um processo também pode morrer enviando explicitamente um sinal de saída. Vamos considerar o seguinte exemplo -

spawn_link fn -> exit(1) end

No exemplo acima, o processo vinculado morreu enviando um sinal de saída com valor 1. Observe que a saída também pode ser “capturada” usando try / catch. Por exemplo -

val = try do
   exit "I am exiting"
catch
   :exit, _ -> "not really"
end

IO.puts(val)

Quando o programa acima é executado, ele produz o seguinte resultado -

not really

Depois de

Às vezes, é necessário garantir que um recurso seja limpo após alguma ação que pode potencialmente gerar um erro. A construção try / after permite que você faça isso. Por exemplo, podemos abrir um arquivo e usar uma cláusula after para fechá-lo - mesmo se algo der errado.

{:ok, file} = File.open "sample", [:utf8, :write]
try do
   IO.write file, "olá"
   raise "oops, something went wrong"
after
   File.close(file)
end

Quando executarmos este programa, ele nos dará um erro. Mas oafter declaração irá garantir que o descritor de arquivo seja fechado em qualquer evento.

As macros são um dos recursos mais avançados e poderosos do Elixir. Como acontece com todos os recursos avançados de qualquer linguagem, as macros devem ser usadas com moderação. Eles tornam possível realizar transformações de código poderosas em tempo de compilação. Agora entenderemos o que são macros e como usá-las resumidamente.

Citar

Antes de começarmos a falar sobre macros, vamos primeiro dar uma olhada nos componentes internos do Elixir. Um programa Elixir pode ser representado por suas próprias estruturas de dados. O bloco de construção de um programa Elixir é uma tupla com três elementos. Por exemplo, a soma da chamada de função (1, 2, 3) é representada internamente como -

{:sum, [], [1, 2, 3]}

O primeiro elemento é o nome da função, o segundo é uma lista de palavras-chave contendo metadados e o terceiro é a lista de argumentos. Você pode obter isso como a saída no shell iex se escrever o seguinte -

quote do: sum(1, 2, 3)

Os operadores também são representados como tuplas. As variáveis ​​também são representadas usando esses tripletos, exceto que o último elemento é um átomo, em vez de uma lista. Ao citar expressões mais complexas, podemos ver que o código é representado em tais tuplas, que muitas vezes estão aninhadas umas dentro das outras em uma estrutura semelhante a uma árvore. Muitas línguas chamariam essas representações deAbstract Syntax Tree (AST). Elixir chama essas expressões citadas.

Entre aspas

Agora que podemos recuperar a estrutura interna do nosso código, como podemos modificá-lo? Para injetar novo código ou valores, usamosunquote. Quando retiramos aspas de uma expressão, ela será avaliada e injetada no AST. Vamos considerar um exemplo (em shell iex) para entender o conceito -

num = 25

quote do: sum(15, num)

quote do: sum(15, unquote(num))

Quando o programa acima é executado, ele produz o seguinte resultado -

{:sum, [], [15, {:num, [], Elixir}]}
{:sum, [], [15, 25]}

No exemplo da expressão aspas, ela não substituiu automaticamente num por 25. Precisamos remover aspas esta variável se quisermos modificar o AST.

Macros

Agora que estamos familiarizados com aspas, podemos explorar a metaprogramação no Elixir usando macros.

No mais simples dos termos, macros são funções especiais projetadas para retornar uma expressão entre aspas que será inserida no código do nosso aplicativo. Imagine a macro sendo substituída pela expressão entre aspas, em vez de chamada como uma função. Com macros temos tudo o que é necessário para estender o Elixir e adicionar código dinamicamente às nossas aplicações

Vamos implementar a menos como uma macro. Começaremos definindo a macro usando odefmacromacro. Lembre-se de que nossa macro precisa retornar uma expressão entre aspas.

defmodule OurMacro do
   defmacro unless(expr, do: block) do
      quote do
         if !unquote(expr), do: unquote(block)
      end
   end
end

require OurMacro

OurMacro.unless true, do: IO.puts "True Expression"

OurMacro.unless false, do: IO.puts "False expression"

Quando o programa acima é executado, ele produz o seguinte resultado -

False expression

O que está acontecendo aqui é que nosso código está sendo substituído pelo código entre aspas retornado pela macro a menos . Tiramos as aspas da expressão para avaliá-la no contexto atual e também retiramos as aspas do bloco do para executá-la em seu contexto. Este exemplo nos mostra a metaprogramação usando macros no elixir.

As macros podem ser usadas em tarefas muito mais complexas, mas devem ser usadas com moderação. Isso ocorre porque a metaprogramação em geral é considerada uma prática ruim e deve ser usada somente quando necessário.

Elixir fornece excelente interoperabilidade com bibliotecas Erlang. Vamos discutir algumas bibliotecas em breve.

O Módulo Binário

O módulo Elixir String integrado lida com binários codificados em UTF-8. O módulo binário é útil quando você está lidando com dados binários que não são necessariamente codificados em UTF-8. Vamos considerar um exemplo para entender melhor o módulo Binário -

# UTF-8
IO.puts(String.to_char_list("Ø"))

# binary
IO.puts(:binary.bin_to_list "Ø")

Quando o programa acima é executado, ele produz o seguinte resultado -

[216]
[195, 152]

O exemplo acima mostra a diferença; o módulo String retorna pontos de código UTF-8, enquanto: binary lida com bytes de dados brutos.

O Módulo de Criptografia

O módulo criptográfico contém funções de hashing, assinaturas digitais, criptografia e muito mais. Este módulo não faz parte da biblioteca padrão Erlang, mas está incluído na distribuição Erlang. Isso significa que você deve listar: crypto na lista de aplicativos do seu projeto sempre que usá-lo. Vamos ver um exemplo usando o módulo cripto -

IO.puts(Base.encode16(:crypto.hash(:sha256, "Elixir")))

Quando o programa acima é executado, ele produz o seguinte resultado -

3315715A7A3AD57428298676C5AE465DADA38D951BDFAC9348A8A31E9C7401CB

O Módulo Digraph

O módulo dígrafo contém funções para lidar com gráficos direcionados construídos de vértices e arestas. Depois de construir o gráfico, os algoritmos lá ajudarão a encontrar, por exemplo, o caminho mais curto entre dois vértices, ou loops no gráfico. Observe que as funçõesin :digraph alterar a estrutura do gráfico indiretamente como um efeito colateral, enquanto retorna os vértices ou arestas adicionados.

digraph = :digraph.new()
coords = [{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}]
[v0, v1, v2] = (for c <- coords, do: :digraph.add_vertex(digraph, c))
:digraph.add_edge(digraph, v0, v1)
:digraph.add_edge(digraph, v1, v2)
for point <- :digraph.get_short_path(digraph, v0, v2) do 
   {x, y} = point
   IO.puts("#{x}, #{y}")
end

Quando o programa acima é executado, ele produz o seguinte resultado -

0.0, 0.0
1.0, 0.0
1.0, 1.0

O Módulo de Matemática

O módulo matemático contém operações matemáticas comuns que abrangem trigonometria, funções exponenciais e logarítmicas. Vamos considerar o seguinte exemplo para entender como funciona o módulo de matemática -

# Value of pi
IO.puts(:math.pi())

# Logarithm
IO.puts(:math.log(7.694785265142018e23))

# Exponentiation
IO.puts(:math.exp(55.0))

#...

Quando o programa acima é executado, ele produz o seguinte resultado -

3.141592653589793
55.0
7.694785265142018e23

O Módulo de Fila

A fila é uma estrutura de dados que implementa filas FIFO (primeiro a entrar, primeiro a sair) (terminação dupla) com eficiência. O exemplo a seguir mostra como um módulo de fila funciona -

q = :queue.new
q = :queue.in("A", q)
q = :queue.in("B", q)
{{:value, val}, q} = :queue.out(q)
IO.puts(val)
{{:value, val}, q} = :queue.out(q)
IO.puts(val)

Quando o programa acima é executado, ele produz o seguinte resultado -

A
B