Você está na página 1de 25

Machine Translated by Google

Embora simples, essas duas funções ilustram alguns recursos poderosos do Elixir. Você
provavelmente pensou primeiro em usar um loop para implementar a função, mas graças à recursão
e à correspondência de padrões, o loop não é necessário e o código é muito fácil de ler e entender.

Na verdade, esta operação poderia ser implementada usando uma função do módulo Enum . O Enum
O módulo contém a função de redução que pode ser usada para agregar valores de uma lista:

iex(5)> Enum.reduce([1, 2, 3, 4], fn x, acc -> x + acc end)


10

A função Enum.reduce recebe a lista enumerável para trabalhar e, para cada elemento da lista, chama
a função passada como segundo parâmetro. Esta função recebe o valor atual e um acumulador (resultado
da iteração anterior). Então, para somar todos os valores, basta adicionar ao acumulador o valor atual.

Outra função útil e conhecida do módulo Enum é a função map . O mapa


A assinatura da função é semelhante a reduzir, mas em vez de agregar a lista em um único valor, ela
retorna um array transformado:

iex(6)> Enum.map(["a", "b", "c"], fn x -> String.to_atom(x) end)

[:a, :b, :c]

Aqui estamos transformando cordas em átomos.

Outra função do módulo Enum é o filtro:

iex(7)> Enum.filter([1, 2, 3, 4, 5], fn x -> x > 2 final)


[3, 4, 5]

Todas essas funções podem ser chamadas usando uma sintaxe mais compacta. Por exemplo, a função de mapa :

iex(8)> Enum.map(["a", "b", "c"], &String.to_atom/1)


[:a, :b, :c]

O &String.to_atom/1 é uma forma de especificar qual função deve ser aplicada ao elemento da lista: a
função String.to_atom com aridade 1. O uso desta sintaxe é bastante típico.

O módulo Lista contém funções mais específicas da lista vinculada, como achatar, dobrar, primeiro,
último e excluir.

25
Machine Translated by Google

Mapa

Os mapas são provavelmente a segunda estrutura mais utilizada para gerenciar dados de aplicações, pois
são facilmente assemelhados a um objeto com campos e valores.

Considere um mapa como este:

livro = %
{ título: "Programming Elixir", autor: %
{ first_name:
"Dave", last_name:
"Thomas" }, ano: 2018 }

É fácil visualizar este mapa como um POJO/POCO; na verdade, podemos acessar seu campo usando a
sintaxe bem conhecida:

iex(2)> livro[:título]
"Elixir de Programação"

Na verdade, não podemos alterar o atributo do mapa hash – lembre-se que na programação
funcional, os valores são imutáveis:

iex(5)> book[:title] = "Programação Java"


** (CompileError) iex:5: não é possível invocar a função remota Access.get/2 dentro da partida

Para alterar um valor-chave em um mapa, podemos usar a função put :

iex(6)> Map.put(book, :title, "Programming Elixir >= 1.6") %{ autor: %{first_name:

"Dave", last_name: "Thomas"}, title: "Programming Elixir >= 1.6" , ano: 2018

A função Map.put não atualiza o mapa, mas cria um novo mapa com a chave modificada.
Os mapas possuem uma sintaxe especial para esta operação, e o put anterior pode ser reescrito assim:

26
Machine Translated by Google

iex(7)> novo_livro = %{ livro | título: "Programming Elixir >= 1.6"} %{ autor: %{first_name:

"Dave", last_name: "Thomas"}, título: "Programming Elixir >= 1.6", ano:


2018 }

A sintaxe curta leva o mapa original e uma lista de atributos a serem alterados:

novo_mapa = %{velho_mapa | attr1: valor1, attr2: valor2, ...}

Para ler um valor de um mapa, já vemos o operador [] . O módulo Map possui uma função especial
para obter o valor, a função fetch :

iex(7)> Map.fetch(livro, :ano) {:ok, 2018}

Aqui, pela primeira vez, vemos uma convenção usual usada no Elixir: o uso de uma tupla para retornar
um valor de uma função. Em vez de retornar apenas 2018, fetch retorna uma tupla com o “estado” da
operação e o resultado. Uma busca pode falhar de alguma forma?

iex(8)> Map.fetch(livro, :foo)


:erro

Esta forma de retornar resultados como tuplas é bastante útil quando usada em conjunto com correspondência
de padrões.

iex(9)> {:ok, y} = Map.fetch(book, :year) {:ok, 2018}


iex(10)> y 2018

Chamamos fetch, padrão que corresponde ao resultado com a tupla {:ok, y}. Se corresponder, em y
teremos o valor 2018.

Em caso de erro, a correspondência falha e podemos ramificar para gerenciar melhor o erro usando uma
instrução case (que veremos mais adiante).

27
Machine Translated by Google

iex(11)> {:ok, y} = Map.fetch(livro, :foo)


** (MatchError) nenhuma correspondência do valor do lado direito: :error (stdlib)
erl_eval.erl:453: :erl_eval.expr/5 (iex) lib/iex/evaluator.ex:249:
IEx.Evaluator.handle_eval/5 (iex) lib/iex/evaluator.ex:229: IEx.Evaluator.do_eval/
3 (iex) lib/iex/evaluator.ex:207: IEx.Evaluator.eval/3 (iex) lib/iex/evaluator.ex
:94: IEx.Evaluator.loop/1 (iex) lib/iex/evaluator.ex:24: IEx.Evaluator.init/
4

Controle de fluxo

Já vimos que com a correspondência de padrões podemos evitar a maioria dos fluxos de controle
condicional, mas há casos em que um if é mais conveniente.

Elixir possui algumas instruções de fluxo de controle, como if, less e case.

if tem a estrutura clássica de if…else:

se test_conditional fizer
# caso

verdadeiro else #
fim de caso falso

Como tudo no Elixir é uma expressão, e if não é exceção, a construção if…else retorna um valor
que podemos atribuir a uma variável para uso posterior:

a = se test_conditional faça #
...

Quando há mais de dois casos, podemos usar a instrução case em conjunto com a correspondência
de padrões para escolher a opção correta:

mensagem_de boas-vindas = case get_idioma(usuário) do


"IT" -> "Benvenuto #{user.name}"
"ES" -> "Bem-vindo #{user.name}"
"DE" -> "Bem-vindo #{user.name}"
_ -> "Bem-vindo"
fim

28
Machine Translated by Google

O último caso é usado quando nenhum dos casos anteriores corresponde ao resultado. case é uma espécie de
switch onde a _ case é o padrão. Assim como if, case retorna um valor; aqui,
mensagem de boas-vindas pode ser usada.

Guardas
Além das instruções de fluxo de controle, existem guardas que podem ser aplicadas às funções, o
que significa que a função será chamada se a guarda retornar verdadeiro:

defmodule Foo do def


divide_by_10(value) quando valor > 0 do valor / 10 end
end

A cláusula when adicionada à assinatura da função diz que esta função está disponível apenas se o valor
passado for maior que 0. Se passarmos um valor igual a 0, obteremos um erro de correspondência:

iex(4)> Foo.divide_by_10(0)
** (FunctionClauseError) nenhuma cláusula de função correspondente em Foo.divide_by_10/1

Os seguintes argumentos foram dados a Foo.divide_by_10/1:

#10

iex:2: Foo.divide_by_10/1

Guards funciona com expressões booleanas e até mesmo com uma série de funções integradas,
como: is_string, is_atom, is_binary, is_list, is_map.

defmodule Foo do
def divide_by_10(valor) quando valor > 0 e (is_float(valor) ou
é_inteiro(valor)) faça
valor / 10 final
final

Neste caso, estamos dizendo que a função divide_by_10 pode ser usada com números maiores que 0.

29
Machine Translated by Google

Operador de tubulação

Elixir suporta uma sintaxe de fluxo especial para concatenar diferentes funções.

Suponha, por exemplo, que precisamos filtrar uma lista para obter apenas os valores maiores que 5, e a
esses valores temos que somar 10, somar todos os valores e, por fim, imprimir o resultado no terminal.

A maneira clássica de implementá-lo poderia ser:

iex(14)> IO.puts Enum.reduce(Enum.map(Enum.filter([1, 3, 5, 7, 8, 9], fn x -> x > 5 end), fn x -> x + 10


final), fn acc, x -> acc + x final) 54 :ok

Não é muito legível, mas em programação funcional é muito fácil escrever código que compõe diferentes
funções.

Elixir nos fornece o operador pipe |> que pode compor funções de maneira fácil, para que o código
anterior se torne:

iex(15)> [1, 3, 5, 7, 8, 9] |> Enum.filter(fn x -> x > 5 final) |> Enum.map(fn x -> x + 10 final) |
> Enum.reduce(fn acc, x -> acc + x end) |> IO.puts

O operador pipe obtém o resultado do cálculo anterior e o passa como primeiro argumento para o
próximo. Portanto, na primeira etapa, a lista é passada como primeiro argumento para Enum.filter,
o resultado é passado para o próximo e assim por diante.

Dessa forma, o código fica mais legível, principalmente se escrevermos assim:

[1, 3, 5, 7, 8, 9]
|> Enum.filter(fn x -> x > 5 final)
|> Enum.map(fn x -> x + 10 final)
|> Enum.reduce(fn acc, x -> acc + x end)
|> IO.puts

Especificações de tipo

Elixir é uma linguagem dinâmica e não pode verificar em tempo de compilação se uma função foi chamada com
os argumentos corretos em termos de número e até mesmo em termos de tipos.

30
Machine Translated by Google

Mas o Elixir possui recursos chamados especificações e tipos que são úteis para especificar assinaturas de
módulos e como um tipo é composto. O compilador ignora essas especificações, mas existem ferramentas
que podem analisar essas informações e nos dizer se tudo corresponde.

Esses recursos são as macros @spec e @type .

defmodule Matemática do
@spec soma(inteiro, inteiro) :: inteiro
def soma(a, b) faça
um + b
fim
fim

A macro @spec vem logo antes da função a ser documentada. Nesse caso, nos ajuda a entender
que a função soma recebe dois inteiros e retorna um inteiro.

O inteiro é um tipo integrado; você pode encontrar tipos adicionais aqui.

As especificações também são úteis para funções que retornam valores diferentes:

defmodule Matemática do
@spec div(inteiro, inteiro) :: {:ok, inteiro} | {:erro, String.t }
def div(a, b) faça
# ...
fim
fim

Neste exemplo, o div retorna uma tupla: {ok, resultado} ou {:string, "mensagem de erro"}.

Mas como o Elixir é uma linguagem dinâmica, como as especificações podem ajudar a encontrar erros? O
compilador em si não se importa com as especificações – devemos usar o Dialyzer, uma ferramenta Erlang que analisa
as especificações e identifica possíveis problemas (principalmente incompatibilidade de tipo e casos não correspondentes).

O Dialyzer, que veio de Erlang, é uma ferramenta de linha de comando que analisa o código-fonte. Para
simplificar o uso do Dialyzer, a comunidade Elixir criou uma ferramenta chamada Dialyxir que envolve a
ferramenta Erlang e a integra às ferramentas Elixir.

A macro spec geralmente é usada em conjunto com as macros type e struct usadas para definir novos tipos:

31
Machine Translated by Google

defmodule Cliente faz


@type entidade_id() :: inteiro()

@type t ::%Cliente{id: entidade_id(), primeiro_nome: String.t, sobrenome:


String.t}
id defstruct: 0, primeiro_nome: nulo, sobrenome: nulo
fim

defmodule CustomerDao do
@type motivo :: String.t
@spec get_customer(Customer.entity_id()) :: {:ok, Cliente} | {:erro,
razão}
def get_customer(id) do
...
# IO.puts "OBTENDO CLIENTE"
fim
fim

Vamos dar uma olhada mais de perto neste exemplo de código, começando com @type entidade_id() :: inteiro().
Este é um alias de tipo simples; definimos o tipo entidade_id, que é um número inteiro. Por que ter um
tipo especial para um número inteiro? Porque entidade_id fala do ponto de vista da documentação
e é contextualizado, pois representa uma identidade para um cliente (pode ser uma chave primária ou
um número de identificação). Não usaremos entidade_id em outro contexto, como sum ou div.

Temos um novo tipo t (nome é apenas uma convenção) para especificar a forma de um cliente que
possui um ID: um nome e um sobrenome. A sintaxe %Customer{ ... } é usada para especificar um
tipo que é uma estrutura (veja a próxima linha). Podemos pensar nisso como um HashMap especial ou
um registro em outras línguas.

A estrutura é definida logo após o typespec; ele contém um id, um nome e um sobrenome.
Para este atributo, a macro defstruct também atribui valores padrão.

Este par de linhas define a forma de um novo tipo complexo: um Cliente com seus atributos.
Novamente, poderíamos ter usado um hash simples, mas estruturas com tipo definem um contexto melhor
e o resultado principal é mais legível.

Após o módulo do cliente no qual existe algum código, abrimos o módulo CustomerDao que utiliza os
tipos definidos anteriormente.

A função get_customer recebe um entidade_id (um inteiro) e retorna uma tupla que contém
um átomo (:ok) e uma estrutura Customer, ou uma tupla com o átomo :error e um motivo (String).

Adicionar todos esses metadados aos nossos programas tem um custo, mas se conseguirmos começar
do início, e o tamanho da aplicação crescer até um certo nível, é um investimento com alto retorno em
termos de documentação e menos bugs .

32
Machine Translated by Google

Comportamento e protocolos

Elixir é uma linguagem de programação funcional que suporta um paradigma diferente de C# ou Java,
que são linguagens de programação orientada a objetos (OOP). Um dos pilares da OOP é o polimorfismo.
O polimorfismo é provavelmente o recurso mais importante e poderoso da POO em termos de composição e
reutilização de código. Linguagens de programação funcionais também podem ter polimorfismo, e o
Elixir usa comportamento e protocolos para construir programas polimórficos.

Protocolos

Os protocolos se aplicam a tipos de dados e nos fornecem uma maneira de aplicar uma função a um tipo.

Por exemplo, digamos que queremos definir um protocolo para especificar que os tipos que irão
implementar este protocolo poderão ser impressos no formato CSV:

defprotocol para impressão fazer


def para_csv(dados)
fim

A macro defprotocol abre a definição de um protocolo; dentro, definimos uma ou mais funções com seus
próprios argumentos.

É uma espécie de contrato de interface: podemos dizer que todo tipo de dado imprimível terá uma implementação
para a função to_csv .

A segunda parte de um protocolo é a implementação.

defimpl Imprimível, para: Mapa do


def to_csv(mapa) do
Mapa.keys(mapa)
|> Enum.map(fn k -> mapa[k] fim)
|> Enum.join(",")
fim
fim

Definimos a implementação usando a macro defimpl e devemos especificar o tipo para o qual estamos escrevendo
a implementação (Map neste caso). Na prática, é como se estivéssemos estendendo o tipo de mapa com uma
nova função to_csv .

Nesta implementação, extraímos as chaves do mapa (:first_name, :last_name) e, a partir delas, obtemos os
valores usando um mapa na lista de chaves . E por fim, estamos juntando a lista usando uma vírgula como
separador.

33
Machine Translated by Google

iex(1)> c("./samples/protocols.exs")
[Printable.Map, Printable] iex(2)>
autor = %{first_name: "Dave", last_name: "Thomas"} %{first_name: "Dave",
last_name: "Thomas"} iex(3)> Printable.to_csv (autor) #
-> "Dave, Thomas"
"Dave, Thomas"

Nota: Se salvarmos a definição e implementação do protocolo em um arquivo de script


(.exs), podemos carregá-lo no REPL usando a função c (compilar). Isso nos permitirá usar
a função do módulo definida no script diretamente no REPL.

Podemos implementar o mesmo protocolo para outros tipos? Claro, vamos fazer isso para obter uma lista.

defimpl Imprimível, para: List do def


to_csv(list) do
Enum.map(list, fn item -> Printable.to_csv(item) end) end end

Aqui estamos usando a função to_csv que definimos para o Mapa, já que to_csv para uma lista é
uma lista de to_csv para seus elementos.

iex(1)> c("./samples/protocols.exs")
[Printable.List, Printable.Map, Printable] iex(2)> autor1
= %{first_name: "Dave", last_name: "Thomas"} %{first_name: "Dave", last_name:
"Thomas"} iex(3) > autor2 = %{nome: "Kent", sobrenome:
"Beck"} %{nome: "Kent", sobrenome: "Beck"} iex(4)> autor3 = %{nome:
"Martin", sobrenome: "Fowler "} %{primeiro_nome:
"Martin", sobrenome: "Fowler"} iex(5)> Printable.to_csv([autor1, autor2, autor3])

["Dave, Thomas", "Kent, Beck", "Martin, Fowler"]

Na saída, temos uma lista de strings CSV! Mas o que acontece se tentarmos aplicar a função
to_csv a uma lista de números? Vamos descobrir.

iex(1)> c("./samples/protocols.exs")
[Printable.List, Printable.Map, Printable] iex(2)>
Printable.to_csv([1,2,3])
** Protocolo (Protocol.UndefinedError) para impressão não implementado para 1
samples/protocols.exs:1: Printable.impl_for!/1 samples/
protocols.exs:2: Printable.to_csv/1 (elixir) lib/enum.ex:1314:
Enum."-map/2-lists^map/ 1-0-"/2

34
Machine Translated by Google

A mensagem de erro nos informa que Printable não está implementado para números e o tempo de
execução não sabe o que fazer com to_csv(1).

Também podemos adicionar uma implementação para Integer se acharmos que precisaremos dela:

defimpl Imprimível, para: Inteiro do def to_csv(i)


do to_string(i) end end

iex(1)> c("./samples/protocols.exs")
[Printable.Integer, Printable.List, Printable.Map, Printable] iex(2)> Printable.to_csv([1,2,3])
["1", "2", "3"]

Elixir possui alguns protocolos já implementados. Um dos mais populares é o protocolo to_string ,
disponível para quase todos os tipos. to_string retorna uma interpretação de string do valor.

Comportamentos

A outra característica interessante que se assemelha ao polimorfismo funcional são os comportamentos. Os


comportamentos fornecem uma maneira de definir um conjunto de funções que devem ser implementadas por um
módulo (um contrato) e garantem que um módulo implemente todas as funções desse conjunto.

Interfaces? Tipo de. Podemos definir um comportamento usando a macro @callback e especificando a
assinatura da função em termos de especificações.

defmodule TalkingAnimal do
@callback say(what :: String.t) :: { :ok } end

Estamos definindo uma “interface” para um animal falante que é capaz de dizer alguma coisa. Para
implementar o comportamento, usamos outra macro.

35
Machine Translated by Google

defmodule Gato do
@comportamento TalkingAnimal
def dizer (str) fazer
"Miau"
fim
fim

defmodule Cachorro faz


@comportamento TalkingAnimal
def dizer (str) fazer
"uau"
fim
fim

Isso se assemelha ao padrão de estratégia clássico. Na verdade, podemos usar funções sem conhecer a
implementação real.

defmodule Fábrica do
def get_animal() faça
#pode obter o módulo do arquivo de configuração
Gato
fim
fim

animal = Fábrica.get_animal()
IO.inspect animal.say("olá") # "miaooo"

Se o módulo estiver marcado com a macro @behaviour , mas a função não estiver implementada, o compilador
gerará um erro, função de comportamento indefinida, informando que não consegue encontrar a
implementação declarada.

Comportamentos e protocolos são duas formas de definir uma espécie de contrato entre módulos ou tipos.
Lembre-se sempre de que Elixir é uma linguagem dinâmica e não pode ser tão rígida como Java ou C#. Mas
com o Dialyzer, especificações, comportamentos e protocolos podem ser bastante úteis na definição e
respeito de contratos.

Macros
Um dos recursos mais poderosos do Elixir são as macros. Macros no Elixir são construções de linguagem
usadas para escrever código que gera novo código. Você deve estar familiarizado com o conceito de
metaprogramação e árvores de sintaxe abstrata; macros são o que você precisa para fazer
metaprogramação no Elixir.

É um tópico difícil e neste capítulo vemos apenas uma introdução suave às macros. No entanto, você provavelmente
não precisará escrever macros em seu trabalho diário com o Elixir.

36
Machine Translated by Google

Primeiro de tudo, a maioria das construções do Elixir que já usamos em nossos exemplos são macros: if é
definido como uma macro, def é uma macro e defmodule é uma macro. Na verdade, Elixir é uma linguagem
com poucas palavras-chave, e todas as outras palavras-chave são definidas como macros.

Macros, metaprogramação e árvores de sintaxe abstrata (AST) estão todas relacionadas. Um AST é
uma representação de código e, no Elixir, um AST é representado como uma tupla. Para visualizar um AST,
podemos usar a citação de instrução:

iex(1)> citação do: 4 + 5


{:+, [contexto: Elixir, importação: Kernel], [4, 5]}

Obtemos de volta uma tupla que contém a função (:+), um contexto e os dois argumentos [4,5].
Esta tupla representa a função que soma 4 a 5. Como tupla, é um dado, mas também é um código
porque podemos executá-lo:

iex(2)> Code.eval_quoted({:+, [contexto: Elixir, importação: Kernel], [4, 5]})


{9, []}

Utilizando o módulo Code, podemos avaliar um AST e recuperar o resultado da execução. Esta é a noção
básica que precisamos para entender o AST. Agora vamos ver como podemos usar um AST para criar
uma macro.

Considere o seguinte módulo. Representa um módulo Logger com apenas uma função para registrar
algo no terminal:

defmodule Logger faz


log defmacro(msg) faça
se is_log_enabled() faça
citar fazer
IO.puts("> Do registro: #{unquote(msg)}")
fim
fim
fim
fim

O defmacro é utilizado para iniciar a definição de uma macro; ele recebe uma mensagem para ser registrada.
A implementação verifica o valor da função is_log_enabled (suponha que esta função irá verificar uma
configuração ou variável de ambiente), e se esse valor for verdadeiro, retorna o AST da instrução IO.puts.

A função unquote é o oposto de quote: como estamos em um contexto entre aspas, para acessar o
valor de msg, precisamos sair do contexto entre aspas para ler esse valor—
unquote(msg) faz exatamente isso.

O que este módulo faz? Este Logger registra as informações somente se o log estiver habilitado. Se não
estiver habilitado, ele nem gera o código necessário para logar, ou seja, não afeta o desempenho da
aplicação, pois nenhum código é gerado.

37
Machine Translated by Google

Macros e metaprogramação são tópicos difíceis e não são o foco deste livro. Uma das principais
regras para escrever macros é não escrevê-las, a menos que seja realmente necessário. Eles
são úteis para escrever em uma DSL ou fazer alguma coisa mágica, mas sua introdução sempre tem
um custo.

38
Machine Translated by Google

O melhor do mundo
Conjunto de componentes de 4,6 de 5
estrelas

UI para construção

Aplicativos poderosos

SHOPMART
Filtros John Watson
Procure algo...

Painel
Receita por categorias de produtos

Portátil: 56%
Pedidos
Pedidos on-line Pedidos off-line Total de usuários

Produtos 23456 345 945 65 9789 95

Vendas
Clientes
Janeiro de 2022 Análise
Visão geral de vendas
S M T EM T F S Por mês
Mensagem
26 27 28 29 30 31 1
Celular: 25%
Acessórios: 19%
2 3 4 5 6 7 8 US$ 51.456

OUTRO
9 10 11 12 13 14 15 Computador portátil
Móvel Acessórios

16 17 18 19 20 21 22 Usuários

23 24 25 26 27 28 29
Equipes Produtos mais vendidos
Dinheiro
30 31 1 2 3 4 5
US$ 1.500
Contexto Apple iPhone 13 Pro US$ 999,00
Estatísticas de entrega de pedidos
Móvel +12,8%
100 mil

Concluído
AppleMacbook Pro US$ 1.299,00 50 mil
120
Faturas Nova fatura Computador portátil
+32,8%
Em andamento
25 mil
24
ID do pedido Data Nome do cliente Quantia Status
Galaxy S22 Ultra US$ 499,99
0
Móvel +22,8%
#1208 21 de janeiro de 2022 Teixo verde-oliva US$ 1.534,00 10 de maio 11 de maio 12 de maio hoje
Sair Concluído

DellInspiron 55 US$ 899,00

Obtenha seus componentes de UI .NET e JavaScript gratuitos

syncfusion.com/communitylicense

Mais de 1.700 componentes Suporte dentro de 24 horas em Qualidade intransigente


para plataformas todos os dias úteis
móveis, web e desktop

Mais de 20 anos
Licenciamento sem complicações Mais de 28.000 clientes
no mercado

Aprovado pelas principais empresas do mundo


Machine Translated by Google

Capítulo 2 A Plataforma

No capítulo anterior aprendemos como o Elixir funciona, como usar sua sintaxe e como escrever
funções e pequenos programas para fazer algumas coisas básicas. Mas o verdadeiro poder do Elixir é a
própria plataforma, baseada no ecossistema Erlang.

Na introdução dissemos que uma das arquiteturas mais utilizadas em Erlang é o modelo de ator, e
que tudo é um processo. Vamos começar com o processo.

Gerar um processo no Elixir é muito fácil e muito barato. Eles não são processos reais do sistema
operacional, mas processos da máquina virtual na qual o aplicativo Elixir é executado. Isso é o que
lhes dá uma pegada leve, e é normal que um aplicativo do mundo real gere milhares de processos.

Vamos começar aprendendo como gerar um processo para se comunicar com ele. Considere este módulo:

defmodule HelloProcess do
def diga (nome) faça
IO.puts "Olá #{nome}"
fim
fim

Este é um exemplo básico de “Hello World” que podemos executar apenas


chamando HelloProcess.say("adam"), e ele imprimirá Hello adam. Neste caso, ele é executado no
mesmo processo do chamador:

iex(1)> c("hello_process.exs")
iex(2)> HelloProcess.say("adam")
"Olá Adão"
é(3)>

Aqui estamos usando o módulo normalmente, mas podemos gerá-lo em um processo diferente e chamar
suas funções:

iex(1)> c("hello_process.exs")
iex(2)> spawn(HelloProcess, :say, [“adam”])
Olá adão
#PID<0,124.0>

A função spawn/3 executa a função say do módulo HelloProcess em um processo diferente.


Ele imprime Hello adam e retorna um PID (ID do processo), neste caso 0.124.0. Os PIDs são uma
parte central da plataforma Erlang/Elixir porque são os identificadores dos processos.

Um PID é composto de três partes: ABC

39
Machine Translated by Google

A é o número do nó. Ainda não falamos sobre nós; considere-os a máquina na qual o processo é executado. 0
representa a máquina local, portanto, todos os PIDs que começam com 0 estão em execução na máquina local.

B é a primeira parte do número do processo e C é a segunda parte do número do processo (geralmente 0).

Tudo no Elixir tem um PID, até o REPL:

iex(1)> próprio
#PID<0.105.0>

O self retorna o PID do processo atual, neste caso o REPL (iex).

Podemos usar o PID para inspecionar o status de um processo usando o módulo Process .

iex(1)> Process.alive?(self) verdadeiro

Podemos tentar com nosso módulo HelloProcess :

iex(12)> pid = spawn(HelloProcess, :say, ["adam"])


Olá adam
#PID<0.133.0>
iex(13)> Process.alive?(pid)
falso

Como podemos ver, o processo morre após a execução. Isso acontece porque não há nada que mantenha o
processo ativo – ele simplesmente coloca a string no console e então finaliza.

O módulo HelloProcess não é muito útil; precisamos de algo que faça alguns cálculos que possamos gerar em outro
processo para manter o processo principal livre.

Vamos escrever:

defmodule AsyncMath do def


sum(a, b) do a + b end
end

Este módulo é muito simples, mas precisamos dele para começar a pensar na comunicação do processo. Então
podemos começar a usar este módulo:

40
Machine Translated by Google

iex(1)> c("async_math.exs")
[AsyncMath]
iex(2)> pid = spawn(AsyncMath, :sum, [1,3])
#PID<0.115.0>
iex(3)> Process.alive?(pid)
Falso

Como podemos ver, ele simplesmente retorna o PID do processo gerado. Além disso, o processo morre após
a execução, não podendo obter o resultado da operação.

Para fazer com que os dois processos se comuniquem, precisamos introduzir duas novas instruções: receber
e enviar. Receber é uma operação de bloqueio que suspende o processo aguardando novas
mensagens. As mensagens são a forma como o processo se comunica: podemos enviar uma
mensagem a um processo e ele pode responder enviando uma mensagem.

Podemos refatorar nosso módulo assim:

defmodule AsyncMath do def


start() do recebe do
{:soma, [x, y],
pid} -> enviar pid, {:resultado, x
+ y}
fim
fim
fim

Definimos uma função inicial que é o ponto de entrada para este módulo; usaremos esta função para
gerar o processo.

Dentro da função start , esperamos por uma mensagem usando a estrutura receiver do . Dentro de
receiver, esperamos receber uma mensagem (uma tupla) com este formato:

{:soma, [x, y], pid}

O formato consiste em um átomo (:sum), uma matriz com um argumento para soma e o pid do remetente. Fazemos
a correspondência de padrões com base nisso e respondemos ao chamador usando uma instrução de envio .

send/2 precisa do pid do processo para enviar uma mensagem: uma tupla com :result e o resultado (soma
de x + y).

Se tudo estiver configurado corretamente, podemos carregar o novo módulo e testá-lo no REPL:

41
Machine Translated by Google

iex(1)> c("async_math.exs")
[AsyncMath]
iex(2)> pid = spawn(AsyncMath, :start, [])
#PID<0,151,0>
iex(3)> Process.alive?(pid)
verdadeiro

iex(4)> enviar(pid, {:soma, [1, 3], self})


{:soma, [1, 3], #PID<0.105.0>}
iex(5)> Process.alive?(pid)
falso

O que fizemos aqui? Carregamos o módulo async_math e geramos o processo usando a função spawn com a
função start do módulo.

Agora o módulo está ativo porque está aguardando uma mensagem (receber…fazer). Enviamos uma mensagem
solicitando a soma de 1 e 3. A função send retorna a mensagem enviada, mas não o resultado.
Além disso, o processo após o envio está morto.

Como podemos obter nosso resultado?

Uma coisa que ainda não mencionei é que todo processo no Elixir possui uma caixa de entrada, uma espécie de
fila onde chegam todas as suas mensagens. A partir dessa fila, o processo retira uma mensagem da fila por vez,
processa-a e depois passa para a próxima. É por isso que eu disse que dentro de um processo o código é de
thread único/processo único, porque funciona em uma única mensagem por vez.

Este mecanismo também está na base do modelo de ator, em que cada ator possui uma fila dedicada que
armazena as mensagens a serem processadas, e um ator trabalha em um único horário.

Voltando ao nosso exemplo, a fila que armazena as mensagens é a fila do REPLS,


já que é esse processo que pede a soma de 1 e 3. Podemos ver o que está dentro da fila do processo chamando
a função flush, que libera a caixa de entrada e imprime as mensagens no console:

iex(6)> liberar
{:resultado, 4}

Aqui está o resultado esperado: flush imprime as mensagens da fila (neste caso, apenas uma).
A mensagem tem o formato exato que usamos para enviar o resultado.

Agora que pedimos um resultado, podemos tentar pedir outra operação:

iex(6)> enviar(pid, {:soma, [5, 8], self})


iex(7)> liberar
:OK

42
Machine Translated by Google

Desta vez a caixa de entrada está vazia: parece que o nosso pedido ou a resposta ao nosso pedido se perdeu.
O problema é que a função AsyncMath.start aguarda a primeira mensagem, mas assim que a primeira
mensagem é processada ela sai do escopo. A macro de recebimento não faz loop entre si depois que uma
mensagem é recebida.

Para obter o resultado desejado, devemos fazer uma chamada recursiva no final da função start :

defmodule AsyncMath do
def início() faça
receber fazer
{:soma, [x, y], pid} ->
enviar pid, {:resultado, x + y}
fim
começar
fim
fim

Ao final do bloco de recebimento , fazemos uma chamada recursiva para iniciar para que o processo entre
em modo “aguardando mensagem”.

Com essa mudança, podemos chamar a operação de soma sempre que quisermos:

iex(1)> c("async_math.exs")
[AsyncMath]
iex(2)> pid = spawn(AsyncMath, :start, [])
#PID<0,126.0>
iex(3)> enviar(pid, {:soma, [5, 4], self})
{:soma, [5, 4], #PID<0.105.0>}
iex(4)> enviar(pid, {:soma, [3, 9], self})
{:soma, [3, 9], #PID<0.105.0>}
iex(5)> liberar
{:resultado, 9}
{:resultado, 12}
:OK
é(6)>

Quando chamamos a função flush , ela imprime as duas mensagens na caixa de entrada com os dois
resultados. Isso acontece porque a chamada recursiva para iniciar mantém o processo pronto para receber
novas mensagens.

Vimos muitos novos conceitos sobre Elixir e processos.

Para recapitular:

• Criamos módulo básico que ao ser iniciado espera uma mensagem e responde com outra mensagem

• Geramos essa função para outro processo


• Enviamos uma mensagem usando a função enviar
• Liberamos a caixa de entrada do REPL para visualizar o resultado

43
Machine Translated by Google

Existe uma maneira melhor de capturar o resultado? Sim, usando o mesmo padrão do AsyncMath
módulo.

defmodule AsyncMath do
def início() faça
receber fazer
{:soma, [x, y], pid} ->
enviar pid, {:resultado, x + y}
fim
começar()
fim
fim

pid = spawn(AsyncMath,:start, [])


enviar pid, {:soma, [5, 6], self()}

receber fazer
{:resultado, x} -> IO.coloca x
fim

Podemos colocar nosso programa em espera mesmo após a execução da operação de soma — lembre-se de que
as mensagens permanecem na fila da caixa de entrada, para que possamos processá-las após chegarem
(diferentemente dos eventos).

Agora vimos os fundamentos dos processos. Também vimos que mesmo que seja barato gerar um processo,
em uma aplicação do mundo real não é muito viável criar processos e comunicar-se com eles usando a
função de baixo nível que vimos. Precisamos de algo mais estruturado e pronto para uso.

Com Elixir e Erlang vem o OTP (Open Telecom Platform), um conjunto de instalações e blocos de construção
para aplicações do mundo real. Embora Telecom esteja no nome, não é específico das telecomunicações
– é mais um ambiente de desenvolvimento para aplicações simultâneas. OTP foi construído com Erlang (e em
Erlang), mas graças à completa interoperabilidade entre Erlang e Elixir, podemos usar todas as facilidades do
OTP em nosso programa Elixir sem nenhum custo.

Aplicações de elixir
Até agora, trabalhamos com arquivos de script Elixir simples (.exs). Eles são úteis em contextos simples,
mas não aplicável em aplicações do mundo real.

Quando instalamos o Elixir, também ganhamos o mix, uma ferramenta de linha de comando usada para criar e
manipular projetos do Elixir. Podemos considerar mix como uma espécie de npm (do Node.js). A partir de agora
usaremos mix para criar projetos e gerenciar projetos.

Vamos criar um novo projeto agora:

44
Machine Translated by Google

~/dev> mix new sample_app *


criando README.md *
criando .formatter.exs *
criando .gitignore * criando
mix.exs * criando config
* criando config/
config.exs * criando lib * criando lib/
sample_app.ex *
criando teste *criando test/
test_helper.exs
*criando test/sample_app_test.exs

Seu projeto Mix foi criado com sucesso.


Você pode usar "mix" para compilá-lo, testá-lo e muito mais:

teste de mixagem de
cd sample_app

Execute "mix help" para mais comandos.

Este comando CLI cria uma nova pasta chamada sample_app e coloca alguns arquivos e pastas
dentro dela.

Agora daremos uma olhada rápida em alguns desses arquivos.

45
Machine Translated by Google

Misturar.exs

defmodule SampleApp.MixProject use


Mix.Project

def projeto fazer [

app: :sample_app,
versão: "0.1.0", elixir:
"~> 1.8",
start_permanent: Mix.env() == :prod, deps:
deps()

] fim

# Execute "mix help compile.app" para aprender sobre os aplicativos.


aplicação def faz
[
extra_applications: [:logger]

] fim

# Execute "mix help deps" para aprender sobre dependências.


defp deps faz [

# {:dep_from_hexpm, "~> 0.3.0"}, #


{:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, ] fim fim

O arquivo mix atua como um arquivo de projeto (uma espécie de package.json). Ele contém o manifesto do
aplicativo, o aplicativo a ser iniciado e a lista de dependências. Não se preocupe se tudo não estiver claro agora –
aprenderemos mais à medida que avançamos.

Há duas coisas importantes a serem observadas aqui. A primeira é a função deps que retorna
uma lista de dependências externas das quais o aplicativo depende. As dependências são
especificadas como uma tupla com o nome do pacote (na forma de um átomo) e uma string que
representa a versão.

A outra coisa importante é que a função do aplicativo que usaremos para especificar o módulo
deve começar do início. Neste modelo, existe apenas um logger.

Lembre-se de que os comentários começam com um caractere de número (#) .

46
Machine Translated by Google

Exemplo_app.ex

defmodule SampleApp do
@moduledoc """
Documentação para SampleApp.
"""

@doc """
Olá Mundo.

## Exemplos

iex> SampleApp.hello() :mundo

"""
def olá do: fim
do
mundo
fim

Sample_app.ex é o arquivo principal deste projeto e por padrão consiste apenas em uma função
que retorna o átomo :world. Não é útil; é apenas um espaço reservado.

Sample_app_test.exs

defmodule SampleAppTest use


ExUnit.Case doctest
SampleApp

teste "cumprimenta o mundo" faça


assert SampleApp.hello() == :world end end

Este é um modelo para um teste simples da função SampleApp.hello. Ele usa ExUnit como estrutura
de teste. Para executar os testes a partir do terminal, devemos escrever mix test:

47
Machine Translated by Google

~/dev> mix test


Compilando 1 arquivo (.ex)
Aplicativo sample_app gerado
..

Concluído em 0,04 segundos 1


doctest, 1 teste, 0 falhas

Randomizado com semente 876926

Os outros arquivos não são importantes no momento; examinaremos mais de perto alguns deles nos
próximos capítulos.

Para iniciar a aplicação, devemos usar mix do terminal:

~/dev> mix run


Compilando 1 arquivo (.ex)
Aplicativo sample_app gerado

Na verdade, o aplicativo não faz nada.

GenServer
Um dos módulos de OTP mais utilizados é o GenServer que representa um servidor genérico básico,
um processo que vive por conta própria e é capaz de processar mensagens e responder a ações.

GenServer é um comportamento que podemos decidir implementar para aderir ao protocolo. Se fizermos
isso, obteremos um processo servidor que pode receber, processar e responder mensagens.

GenServer tem os seguintes recursos:

• Criando um processo de servidor


• Gerenciando o estado do
servidor • Criando um processo
de servidor • Gerenciando o
estado do servidor • Tratamento de solicitações e
envio de respostas •
Parada do servidor • Tratamento de falhas

É um comportamento, então os detalhes da implementação ficam por nossa conta. GenServer nos permite implementar
algumas funções (chamadas callbacks) para personalizar seus detalhes:

• init/1 atua como um construtor e é chamado quando o servidor é iniciado. O resultado esperado é
uma tupla {:ok, state} que contém o estado inicial do servidor. • handle_call/3 é o
retorno de chamada chamado quando uma mensagem chega ao servidor.
Esta é uma função síncrona e o resultado é uma tupla {:resposta, resposta,

48

Você também pode gostar