Cheatsheet Elixir
Linguagem funcional na BEAM VM para aplicações concorrentes e escaláveis
Elixir
Recursos Avançados
Protocols
# Definir protocolo:
defprotocol Descritivel do
def descrever(dado)
end
# Implementar para tipos:
defimpl Descritivel, for: BitString do
def descrever(s), do: "String: #{s}"
end
defimpl Descritivel, for: Integer do
def descrever(n), do: "Número: #{n}"
end
Descritivel.descrever("olá") # "String: olá"
Descritivel.descrever(42) # "Número: 42"Um protocol define polimorfismo ad-hoc: declara uma função com defprotocol e implementa-se por tipo com defimpl. A função certa é escolhida em runtime pelo tipo do dado.
Behaviours
# Definir behaviour (contrato):
defmodule Worker do
@callback processar(dado :: any) :: {:ok, any}
@callback nome() :: String.t()
end
# Implementar:
defmodule MeuWorker do
@behaviour Worker
@impl true
def processar(d), do: {:ok, d}
@impl true
def nome, do: "MeuWorker"
endUm behaviour define um contrato de callbacks com @callback. O módulo que o adota usa @behaviour e marca cada implementação com @impl true, garantido em compilação.
defimpl e Any
# Para structs:
defimpl Descritivel, for: User do
def descrever(u), do: "User: #{u.nome}"
end
# Vários tipos de uma vez:
defimpl Descritivel, for: [List, Tuple] do
def descrever(d), do: "Coleção: #{inspect(d)}"
end
# Fallback para qualquer tipo:
defimpl Descritivel, for: Any do
def descrever(d), do: inspect(d)
endO defimpl implementa o protocolo para um tipo (ou vários, com uma lista). O for: Any define um fallback para tipos sem implementação específica, evitando erros.
use e __using__
# use injeta código de um módulo:
defmodule MeuHelper do
defmacro __using__(_opts) do
quote do
import MeuHelper
@before_compile MeuHelper
end
end
end
# Uso:
defmodule App do
use MeuHelper
# agora tem o código injetado
endO use chama o macro __using__ de um módulo, que injeta código (imports, callbacks, etc.) no módulo atual. É como GenServer e Supervisor funcionam por baixo.
Macros
defmodule Logger do
defmacro log(expr) do
quote do
IO.puts("[LOG] #{inspect(unquote(expr))}")
unquote(expr)
end
end
end
require Logger
Logger.log(2 + 2)
# [LOG] 4
# => 4Um macro (com defmacro) recebe código e devolve código, executado em tempo de compilação. O quote cria a AST e o unquote insere valores. Requer require para usar.
Module attributes
defmodule Config do
# Atributos de módulo (@nome):
@versao "1.0.0"
@timeout 5000
def versao, do: @versao
# Acumulador (registar vários):
Module.register_attribute(__MODULE__,
:rotas, accumulate: true)
@rota "/home"
@rota "/sobre"
def rotas, do: @rotas # ["/sobre", "/home"]
endOs atributos de módulo (@nome) são constantes avaliadas em compilação. Servem para configuração, documentação e, com accumulate: true, para registar listas de valores.
quote e unquote
# quote: transforma código em AST
quote do
1 + 2
end
# {:+, [], [1, 2]}
# unquote: insere valor na AST
x = 5
quote do
unquote(x) + 10
end
# {:+, [], [5, 10]}
# Macro hygiene evita conflitos de nomesO quote converte código na sua representação AST (árvore de sintaxe). O unquote injeta um valor dentro de um quote. São a base da metaprogramação em Elixir.
Sigils custom e docs
# Documentação com @doc e @moduledoc:
defmodule Math do
@moduledoc "Funções matemáticas"
@doc """
Dobra um número.
## Exemplos
iex> Math.dobro(2)
4
"""
def dobro(x), do: x * 2
end
# Doctests:
# ExUnit valida os exemplos iex>O @moduledoc documenta o módulo e o @doc documenta funções. Exemplos com iex> viram doctests validados automaticamente pelo ExUnit.
Básico
Variáveis e imutabilidade
nome = "Elixir" idade = 30 ativo = true pi = 3.14 # Reatribuição cria novo binding: x = 1 x = 2 # x agora é 2 (não muta o 1) # Tudo é imutável: # operações devolvem novos valores
As variáveis são atribuídas com = e todos os dados são imutáveis. Reatribuir uma variável cria um novo binding; os valores originais nunca são alterados.
Pipe operator
# Sem pipe (aninhado):
String.upcase(String.trim(" olá "))
# Com pipe (|>):
" olá "
|> String.trim()
|> String.upcase()
|> String.reverse()
# O resultado de cada passo é o
# primeiro argumento do seguinteO operador |> (pipe) passa o resultado da expressão anterior como primeiro argumento da seguinte. Torna pipelines de transformações legíveis, evitando chamadas aninhadas.
Sigils
# ~w: lista de palavras
~w(palavra1 palavra2) # ["palavra1", "palavra2"]
~w(a b c)a # [:a, :b, :c] (atoms)
# ~r: expressão regular
~r/\d+/ # regex de dígitos
Regex.run(~r/(\d+)/, "abc123") # ["123", "123"]
# ~s: string com interpolação
~s(olá #{nome})
# ~D, ~T, ~N: datas e tempo
~D[2024-01-15]Os sigils começam com ~ e criam valores com sintaxe especial: ~w (listas de palavras), ~r (regex), ~s (strings) e ~D (datas). Modificadores como a alteram o tipo.
Tipos básicos
# Tipos primitivos:
42 # integer
3.14 # float
"texto" # string (binário)
?a # char (97)
true # boolean
:atomo # atom
# Estruturas:
{1, 2, 3} # tuple
[1, 2, 3] # list
%{a: 1} # map
<<1, 2>> # bitstringO Elixir tem integers, floats, strings (binários), atoms (:nome), booleans e estruturas como tuples, lists e maps.
IO (entrada/saída)
# Imprimir com newline:
IO.puts("Olá Mundo")
# Inspeção (debug, mostra estrutura):
IO.inspect(%{a: 1, b: 2})
IO.inspect(lista, label: "Dados")
# Ler input:
nome = IO.gets("Nome? ")
# Inspect devolve string (não imprime):
inspect([1, 2, 3]) # "[1, 2, 3]"O IO.puts imprime texto com quebra de linha e o IO.inspect mostra a representação de qualquer valor (ótimo para debug). O IO.gets lê uma linha do stdin.
Operadores
# Aritméticos: 2 + 3 # 5 10 - 4 # 6 3 * 7 # 21 10 / 2 # 5.0 (sempre float) div(10, 3) # 3 (inteiro) rem(10, 3) # 1 (resto) # Comparação: 1 == 1 # true 1 != 2 # true 1 === 1.0 # false (estrito: int vs float) # Lógicos (só booleans): true and false # false true or false # true not true # false
Os operadores incluem + - * / (a divisão / devolve sempre float; usa div/rem para inteiros). A comparação estrita === distingue 1 de 1.0.
Atoms
# Atom: constante cujo nome é o valor
:ok
:error
:nome
# Booleans são atoms:
true == :true # true
false == :false # true
nil == :nil # true
# Atoms com espaços:
:"olá mundo"
# Uso comum em tuplas de resultado:
{:ok, valor}
{:error, razao}Um atom é uma constante cujo nome é o próprio valor (começa com :). Os booleanos true/false e nil são atoms. São muito usados em tuplas {:ok, _} e {:error, _}.
Strings
s = "Olá Mundo"
String.upcase(s) # "OLÁ MUNDO"
String.downcase(s) # "olá mundo"
String.length(s) # 10
String.trim(" oi ") # "oi"
String.split(s, " ") # ["Olá", "Mundo"]
String.replace(s, "Mundo", "Elixir")
String.contains?(s, "Mundo") # true
# Strings são binários UTF-8As strings são binários UTF-8. O módulo String oferece operações como upcase, split, replace e contains?, todas imutáveis (devolvem novas strings).
Módulos
defmodule Saudacao do
def ola(nome) do
"Olá, #{nome}!"
end
# Função privada:
defp secreto do
"interno"
end
end
# Chamar:
Saudacao.ola("Mundo") # "Olá, Mundo!"O código organiza-se em módulos com defmodule. As funções públicas definem-se com def e as privadas com defp (só visíveis dentro do módulo).
Interpolação e concatenação
nome = "Ana"
idade = 30
# Interpolação com #{}:
"Olá, #{nome}!" # "Olá, Ana!"
"2 + 2 = #{2 + 2}" # "2 + 2 = 4"
# Concatenação:
"Olá" <> " " <> "Mundo" # "Olá Mundo"
# Multilinha (heredoc):
texto = """
Linha 1
Linha 2
"""A interpolação #{expr} insere valores numa string. O operador <> concatena binários. Strings de várias linhas usam três aspas (heredoc).
Funções
def e defp
defmodule Math do
# Corpo longo:
def dobro(x) do
x * 2
end
# Corpo curto (one-liner):
def triplo(x), do: x * 3
# Privada (só no módulo):
defp auxiliar(x), do: x + 1
end
Math.dobro(5) # 10As funções definem-se com def (públicas) ou defp (privadas). A forma do: é usada para corpos de uma linha; o bloco do...end para várias.
Guards
def classificar(idade) when idade < 12 do "Criança" end def classificar(idade) when idade < 18 do "Adolescente" end def classificar(_), do: "Adulto" # Funções de guarda: def processa(x) when is_integer(x), do: "int" def processa(x) when is_list(x), do: "lista" # is_atom, is_binary, is_number, >, <, ==
Os guards (when) acrescentam condições às cláusulas. Usam funções permitidas como is_integer, is_list, is_atom e operadores de comparação.
Default args e cláusulas
# Argumento com default (\\):
def saudar(nome, pontuacao \\ "!") do
"Olá, #{nome}#{pontuacao}"
end
saudar("Ana") # "Olá, Ana!"
saudar("Ana", "?") # "Olá, Ana?"
# Várias cláusulas da mesma função:
def fat(0), do: 1
def fat(n), do: n * fat(n - 1)Argumentos opcionais usam \ para definir um valor por omissão. Uma função pode ter várias cláusulas (mesmo nome/aridade), tentadas por ordem até casar.
Pattern matching em parâmetros
# Casa por estrutura:
def area({:circulo, r}), do: :math.pi * r * r
def area({:rect, l, a}), do: l * a
area({:circulo, 2}) # 12.56...
area({:rect, 3, 4}) # 12
# Múltiplas cláusulas por pattern:
def descrever([]), do: "vazia"
def descrever([_ | _]), do: "com elementos"Os parâmetros fazem pattern matching: cada cláusula casa com uma forma específica. O Elixir tenta as cláusulas por ordem até uma casar, permitindo funções polimórficas elegantes.
Funções anónimas (fn)
# Definir: dobro = fn x -> x * 2 end soma = fn a, b -> a + b end # Chamar com ponto: dobro.(5) # 10 soma.(3, 4) # 7 # Várias cláusulas: tipo = fn x when is_integer(x) -> "int" x when is_binary(x) -> "string" end tipo.(42) # "int"
As funções anónimas criam-se com fn ... end e chamam-se com um ponto (dobro.(5)). Podem ter várias cláusulas com pattern matching e guards.
Destructuring
# Tuplas:
{x, y, z} = {1, 2, 3} # x=1, y=2, z=3
# Listas (head/tail):
[head | tail] = [1, 2, 3, 4] # head=1, tail=[2,3,4]
# Maps:
%{nome: n} = %{nome: "Ana", idade: 30} # n="Ana"
# Ignorar com _:
{_, segundo, _} = {10, 20, 30} # segundo=20O destructuring extrai partes de estruturas no lado esquerdo do =. O _ ignora posições e o [head | tail] separa o primeiro elemento do resto da lista.
Shorthand &()
# &() é atalho para fn:
dobro = &(&1 * 2) # fn x -> x * 2 end
soma = &(&1 + &2) # fn a, b -> a + b end
# &1, &2... são os parâmetros:
Enum.map([1, 2, 3], &(&1 * 2)) # [2, 4, 6]
# Misturar com texto:
saudar = &"Olá, #{&1}!"
saudar.("Ana") # "Olá, Ana!"A sintaxe &() é uma forma curta de função anónima, onde &1, &2 representam os parâmetros por posição. É ideal para funções pequenas em Enum.
Pin operator (^)
x = 1
# Sem pin: re-bind (x passa a 2)
x = 2
# Com pin: casa com o valor atual
^x = 2 # OK, x é 2
^x = 3 # MatchError! (x é 2, não 3)
# Útil em parâmetros e case:
case {1, 2} do
{^x, y} -> "x casou, y=#{y}"
endO operador ^ (pin) impede o re-binding e força a comparação com o valor atual da variável. Sem ele, o = reatribui; com ele, faz match contra o valor existente.
Captura de função
# Capturar função nomeada:
upcase = &String.upcase/1
upcase.("olá") # "OLÁ"
# Notação Módulo.função/aridade:
Enum.map(["a", "b"], &String.upcase/1)
# ["A", "B"]
# Captura parcial:
dobro = &Kernel.*/2
# mais comum: &(&1 * 2)A captura &Módulo.função/aridade transforma uma função nomeada em anónima (ex.: &String.upcase/1). A aridade é o número de argumentos.
Estruturas de Dados
Lists
lista = [1, 2, 3, 4] # Construtor cons (|): [0 | lista] # [0, 1, 2, 3, 4] # Acesso: hd(lista) # 1 (head) tl(lista) # [2, 3, 4] (tail) length(lista) # 4 # Listas são ligadas: # adicionar ao início é O(1)
As lists são listas ligadas: adicionar ao início com [x | lista] é eficiente (O(1)). O hd dá a cabeça, o tl o resto e length o tamanho.
Tuples
# Tuple: tamanho fixo, acesso por índice
point = {3, 4}
elem(point, 0) # 3
elem(point, 1) # 4
tuple_size(point) # 2
# Atualizar posição:
put_elem(point, 1, 5) # {3, 5}
# Uso comum: retornos
{:ok, dados}
{:error, "falhou"}As tuples têm tamanho fixo e acesso por índice com elem. São ideais para retornos como {:ok, valor}. Ao contrário das lists, não são iteráveis.
Binaries e bitstrings
# Binary (sequência de bytes):
<<1, 2, 3>> # 3 bytes
"olá" # é um binary
# Tamanho em bits:
<<x::8, y::8>> = <<10, 20>> # x=10, y=20
# Concatenar:
<<1, 2>> <> <<3>> # <<1, 2, 3>>
# Strings são binaries:
is_binary("olá") # true
byte_size("olá") # 4Um binary (<< >>) é uma sequência de bytes; uma bitstring pode ter tamanho em bits. As strings são binaries UTF-8. O <<x::8>> especifica o tamanho.
Operações com lists
a = [1, 2, 3] # Concatenação: a ++ [4, 5] # [1, 2, 3, 4, 5] # Diferença: [1, 2, 3] -- [2] # [1, 3] # Pertença: 3 in a # true 9 in a # false # Reversão: Enum.reverse(a) # [3, 2, 1]
O ++ concatena duas listas e o -- remove elementos. O operador in testa pertença. Estas operações percorrem a lista, sendo O(n).
Keyword lists
# Lista de pares {atom, valor}:
opts = [nome: "App", debug: true]
# Equivale a:
[{:nome, "App"}, {:debug, true}]
# Acesso:
opts[:nome] # "App"
Keyword.get(opts, :debug) # true
Keyword.get(opts, :porta, 4000) # default
# Muito usada em opções de funçõesUma keyword list é uma lista de tuplas {atom, valor}, escrita como [chave: valor]. Mantém ordem e permite chaves repetidas; é comum em opções de funções.
Maps
pessoa = %{nome: "Ana", idade: 30}
# Acesso:
pessoa.nome # "Ana" (só atom keys)
pessoa[:idade] # 30
Map.get(pessoa, :email, "N/A") # "N/A"
# Chaves:
Map.keys(pessoa) # [:nome, :idade]
Map.values(pessoa) # ["Ana", 30]
Map.has_key?(pessoa, :nome) # trueOs maps (%{}) guardam pares chave-valor sem ordem. Com chaves atom, acede-se por ponto (pessoa.nome). O Map.get aceita um valor default.
Structs
defmodule User do
defstruct nome: "", idade: 0, ativo: true
end
# Criar:
u = %User{nome: "Ana", idade: 30}
# Acesso:
u.nome # "Ana"
# Atualizar:
%{u | idade: 31}
# Struct é um map com __struct__:
%User{} = u # pattern match por tipoUma struct é um map com chaves fixas e valores por omissão, definida com defstruct. Garante a forma dos dados e permite pattern matching por tipo (%User{}).
Atualizar maps
pessoa = %{nome: "Ana", idade: 30}
# Sintaxe de update (só chaves existentes):
%{pessoa | idade: 31}
# Map.put (adiciona ou atualiza):
Map.put(pessoa, :email, "ana@mail.com")
# Map.update (com função):
Map.update(pessoa, :idade, 0, &(&1 + 1))
# Map.delete:
Map.delete(pessoa, :idade)Os maps são imutáveis. A sintaxe %{m | chave: valor} atualiza chaves existentes; Map.put adiciona ou substitui, Map.update aplica uma função e Map.delete remove.
Acesso aninhado
dados = %{user: %{nome: "Ana", morada: %{cidade: "Porto"}}}
# get_in: lê caminho
get_in(dados, [:user, :morada, :cidade])
# "Porto"
# update_in: atualiza caminho
update_in(dados.user.idade, &(&1 + 1))
# put_in: define caminho
put_in(dados[:user][:email], "ana@mail.com")
# pop_in: remove e devolvePara estruturas aninhadas, get_in lê um caminho de chaves, update_in aplica uma função nesse caminho e put_in define um valor — tudo de forma imutável.
Enum e Stream
map, filter, reduce
# map: transforma cada elemento Enum.map([1, 2, 3], &(&1 * 2)) # [2, 4, 6] # filter: mantém os que passam Enum.filter([1, 2, 3, 4], &(&1 > 2)) # [3, 4] # reduce: agrega com acumulador Enum.reduce([1, 2, 3], 0, &(&1 + &2)) # 6
O Enum.map transforma cada elemento, o Enum.filter seleciona os que passam no predicado e o Enum.reduce combina tudo num valor com um acumulador inicial.
Agregação
Enum.sum([1, 2, 3, 4]) # 10 Enum.max([3, 1, 4]) # 4 Enum.min([3, 1, 4]) # 1 Enum.count([1, 2, 3]) # 3 # Com predicado: Enum.count([1, 2, 3, 4], &(&1 > 2)) # 2 # Por função: Enum.max_by(users, & &1.idade) Enum.min_by(users, & &1.idade)
Estas funções resumem coleções: sum, max, min e count. O count aceita um predicado e max_by/min_by usam uma função de critério.
Ranges e comprehensions
# Range:
1..10
Enum.to_list(1..5) # [1, 2, 3, 4, 5]
Enum.sum(1..100) # 5050
# Comprehension (for):
for x <- 1..5, do: x * x
# [1, 4, 9, 16, 25]
# Com filtro:
for x <- 1..10, even?(x), do: x
# Múltiplos geradores:
for x <- 1..3, y <- 1..3, do: {x, y}Um range (1..10) é uma sequência lazy de inteiros. A comprehension for constrói listas com geradores (<-) e filtros, semelhante a list comprehensions.
each e find
# each: efeito colateral (não transforma) Enum.each([1, 2, 3], &IO.puts/1) # find: primeiro que passa Enum.find([1, 2, 3, 4], &(&1 > 2)) # 3 # find com default: Enum.find([1, 2], &(&1 > 10), :nenhum) # :nenhum # find_index: posição Enum.find_index([10, 20, 30], &(&1 == 20)) # 1
O Enum.each executa um efeito colateral sem devolver lista. O Enum.find devolve o primeiro elemento que satisfaz o predicado (ou um default) e o find_index a sua posição.
group_by e frequencies
# group_by: agrupa por função
Enum.group_by(1..10, &rem(&1, 2))
# %{0 => [2,4,6,8,10], 1 => [1,3,5,7,9]}
Enum.group_by(users, & &1.cidade)
# frequencies: conta ocorrências
Enum.frequencies(~w(a b a c a b))
# %{a: 3, b: 2, c: 1}
# frequencies_by:
Enum.frequencies_by(users, & &1.tipo)O Enum.group_by agrupa elementos num map por uma função-chave. O Enum.frequencies conta quantas vezes cada valor aparece; frequencies_by conta por um critério.
sort e uniq
Enum.sort([3, 1, 2]) # [1, 2, 3] Enum.sort([3, 1, 2], :desc) # [3, 2, 1] # sort_by: ordena por função Enum.sort_by(users, & &1.idade) Enum.sort_by(users, & &1.idade, :desc) # uniq: remove duplicados Enum.uniq([1, 1, 2, 3, 3]) # [1, 2, 3] Enum.uniq_by(users, & &1.email)
O Enum.sort ordena (com :desc para decrescente) e o sort_by ordena por uma função. O Enum.uniq remove duplicados; uniq_by por um critério.
any?, all?, member?
# any?: algum passa? Enum.any?([1, 2, 3], &(&1 > 2)) # true # all?: todos passam? Enum.all?([1, 2, 3], &(&1 > 0)) # true # member?: contém valor? Enum.member?([1, 2, 3], 2) # true # empty?: Enum.empty?([]) # true # take / drop: Enum.take([1, 2, 3, 4], 2) # [1, 2] Enum.drop([1, 2, 3, 4], 2) # [3, 4]
O Enum.any? verifica se algum elemento passa, o all? se todos passam e o member? se contém um valor. O take/drop retiram N elementos.
chunk, zip, flat_map
# chunk_every: agrupa em blocos
Enum.chunk_every([1, 2, 3, 4, 5], 2)
# [[1, 2], [3, 4], [5]]
# zip: combina em pares
Enum.zip([1, 2], [:a, :b]) # [{1, :a}, {2, :b}]
# flat_map: map + flatten
Enum.flat_map([[1, 2], [3, 4]], & &1)
# [1, 2, 3, 4]
# with_index:
Enum.with_index([:a, :b]) # [{:a, 0}, {:b, 1}]O chunk_every agrupa em blocos de N, o zip combina duas coleções em pares e o flat_map mapeia e achata. O with_index junta cada elemento ao seu índice.
Streams (lazy)
# Stream: avaliação preguiçosa
1..1_000_000
|> Stream.filter(&(rem(&1, 2) == 0))
|> Stream.map(&(&1 * &1))
|> Enum.take(5)
# [4, 16, 36, 64, 100]
# Stream de ficheiro (linha a linha):
File.stream!("grande.txt")
|> Stream.map(&String.trim/1)
|> Stream.filter(&(&1 != ""))
|> Enum.to_list()O Stream é lazy: só processa os elementos quando necessário (com Enum.take ou to_list). É ideal para dados grandes ou infinitos, evitando criar listas intermédias.
Processos
spawn
# Criar processo:
pid = spawn(fn ->
IO.puts("Olá de outro processo")
end)
# pid é o identificador (#PID<0.123.0>)
Process.alive?(pid) # true/false
# spawn com módulo/função:
spawn(MeuModulo, :funcao, [arg1, arg2])
# Processos são leves (milhares OK)O spawn cria um processo concorrente leve e devolve o seu pid. Os processos da BEAM são isolados e baratos — é comum ter milhares em simultâneo.
Task async/await
# Task: computação assíncrona task = Task.async(fn -> Process.sleep(1000) 42 end) # Fazer outro trabalho... # await bloqueia até ao resultado: resultado = Task.await(task, 5000) # 42 # Várias tasks em paralelo: tasks = Enum.map(1..5, &Task.async(fn -> &1 * 2 end)) Enum.map(tasks, &Task.await/1)
Uma Task executa trabalho assíncrono. O Task.async inicia e devolve a task; o Task.await bloqueia até obter o resultado. Várias tasks correm em paralelo.
GenServer callbacks
defmodule Contador do
use GenServer
# Inicialização:
def init(v), do: {:ok, v}
# Trata call (síncrono):
def handle_call(:get, _from, n) do
{:reply, n, n}
end
# Trata cast (assíncrono):
def handle_cast(:inc, n) do
{:noreply, n + 1}
end
endOs callbacks do GenServer tratam as mensagens: init define o estado inicial, handle_call responde a call ({:reply, resp, estado}) e handle_cast trata cast ({:noreply, estado}).
send e receive
# Enviar mensagem:
send(pid, {:ola, "Mundo"})
# Receber (bloqueia até chegar):
receive do
{:ola, msg} -> IO.puts(msg)
{:adeus, _} -> IO.puts("Adeus")
_ -> IO.puts("Desconhecido")
end
# self() é o pid do processo atual
send(self(), :ping)Os processos comunicam por mensagens: send envia para um pid e receive bloqueia até chegar uma mensagem que case. O self() é o pid do processo atual.
Task.async_stream
# Processar coleção em paralelo:
1..10
|> Task.async_stream(&processar/1,
max_concurrency: 4)
|> Enum.map(fn {:ok, r} -> r end)
# timeout e on_timeout:
|> Task.async_stream(&lento/1,
timeout: 3000,
on_timeout: :kill_task)O Task.async_stream processa uma coleção em paralelo, preservando a ordem. O max_concurrency limita processos simultâneos; o timeout controla o tempo por tarefa.
receive com timeout
receive do
{:resultado, valor} -> valor
after
5000 -> IO.puts("Timeout (5s)")
end
# after define tempo máximo de espera
# (em milissegundos)
# 0 = não bloquear (verifica já)
receive do
msg -> msg
after
0 -> :nada
endA cláusula after no receive define um timeout em milissegundos; se nenhuma mensagem chegar, executa o ramo de timeout. Com 0, verifica sem bloquear.
Agent
# Agent: estado simples partilhado
{:ok, pid} = Agent.start_link(fn -> 0 end)
# Ler:
Agent.get(pid, & &1) # 0
# Atualizar:
Agent.update(pid, &(&1 + 1)) # ok
Agent.get(pid, & &1) # 1
# Ler e atualizar:
Agent.get_and_update(pid, fn s ->
{s, s + 10}
end)
# Com nome:
Agent.start_link(fn -> %{} end, name: MeuEstado)Um Agent guarda estado partilhado de forma simples. O Agent.get lê, o Agent.update modifica e o get_and_update faz ambos. Ideal para estado simples.
Process (info e dicionário)
# Informação do processo: Process.info(self()) Process.alive?(pid) # Dicionário do processo (estado local): Process.put(:chave, "valor") Process.get(:chave) # "valor" # Links: Process.link(pid) Process.monitor(pid)
O módulo Process dá informações (info, alive?) e um dicionário local (put/get). O link liga processos; o monitor observa sem propagar falhas.
GenServer (call e cast)
defmodule Contador do
use GenServer
def start_link(init \\ 0) do
GenServer.start_link(__MODULE__, init,
name: __MODULE__)
end
# API síncrona (responde):
def value, do: GenServer.call(__MODULE__, :get)
# API assíncrona (não responde):
def increment, do: GenServer.cast(__MODULE__, :inc)
endO GenServer é um processo com estado. O GenServer.call é síncrono (espera resposta) e o GenServer.cast é assíncrono (não espera). A API pública encapsula as mensagens.
Controlo e Erros
case
case {1, 2, 3} do
{x, y, z} when x > 0 -> "positivo"
{0, _, _} -> "zero"
_ -> "outro"
end
# Com resultado de função:
case File.read("f.txt") do
{:ok, conteudo} -> conteudo
{:error, razao} -> "Erro: #{razao}"
endO case faz pattern matching de um valor contra vários padrões, com guards opcionais. O _ é o caso padrão. É ideal para tratar tuplas {:ok, _}/{:error, _}.
try e rescue
try do
raise "Algo falhou!"
rescue
e in RuntimeError -> IO.puts(e.message)
e -> IO.puts("Outro: #{inspect(e)}")
after
IO.puts("Sempre executa")
end
# rescue por tipo de exceção:
# RuntimeError, ArgumentError,
# ArithmeticError, KeyErrorO try/rescue captura exceções por tipo (ex.: RuntimeError). O bloco after executa sempre. Em Elixir, prefere-se tuplas {:error, _} a exceções.
cond
# cond: múltiplas condições (if/elseif) cond do x > 10 -> "grande" x > 5 -> "médio" true -> "pequeno" # default end # Equivalente a if aninhados: # Cada ramo é uma expressão booleana # O true final é o else
O cond avalia várias condições em sequência, como um if/elseif. Cada ramo é uma expressão booleana; o true final funciona como caso padrão (else).
raise e exceções
# Lançar exceção: raise "Erro simples" raise ArgumentError, "valor inválido" # Definir exceção custom: defmodule MeuErro do defexception message: "erro padrão" end raise MeuErro, message: "falha custom" # try/rescue captura-as
O raise lança uma exceção com uma mensagem ou tipo. Exceções custom definem-se com defexception. Em Elixir, exceções são para situações verdadeiramente excecionais.
if e unless
# if (forma curta): if x > 0, do: "pos", else: "neg" # if (bloco): if x > 0 do "positivo" else "negativo" end # unless (negativo): unless x > 0 do "não positivo" end # Em Elixir, if é uma macro/expressão
O if devolve um valor e tem forma curta (do:/else:) ou em bloco. O unless é o if negado. Ambos são expressões que devolvem o resultado do ramo executado.
Recursão
defmodule Math do
# Caso base + caso recursivo:
def factorial(0), do: 1
def factorial(n) when n > 0 do
n * factorial(n - 1)
end
# Somar lista:
def sum([]), do: 0
def sum([h | t]), do: h + sum(t)
end
Math.factorial(5) # 120A recursão substitui os ciclos no Elixir (não há for/while imperativos). Define-se um caso base e um caso recursivo, frequentemente com pattern matching em listas.
with (happy path)
# Encadear operações que podem falhar:
with {:ok, user} <- find_user(id),
{:ok, posts} <- get_posts(user) do
{:ok, posts}
else
{:error, reason} -> {:error, reason}
:not_found -> {:error, "não existe"}
end
# Se algum <- falhar, salta para elseO with encadeia operações no "caminho feliz": cada <- faz match; se todos casarem, corre o bloco do. Se algum falhar, salta para o else.
Recursão com acumulador
defmodule Math do
# Tail-recursive com acumulador:
def factorial(n), do: do_fact(n, 1)
defp do_fact(0, acc), do: acc
defp do_fact(n, acc) do
do_fact(n - 1, n * acc)
end
end
# O acumulador evita crescer a stack
# (tail-call optimization na BEAM)A recursão com acumulador (tail-recursive) é otimizada na BEAM: o resultado é passado como argumento, evitando crescer a stack. É a forma preferida para performance.
OTP e Supervisão
Supervisor
defmodule MyApp.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts)
end
def init(_opts) do
children = [
Contador,
MeuGenServer
]
Supervisor.init(children,
strategy: :one_for_one)
end
endUm Supervisor gere processos filhos e reinicia-os se falharem. Os filhos listam-se em children e a estratégia define como reiniciar. É a base da tolerância a falhas.
DynamicSupervisor
# Supervisor para filhos dinâmicos:
children = [
{DynamicSupervisor,
name: MeuApp.DynSup,
strategy: :one_for_one}
]
# Iniciar filho em runtime:
DynamicSupervisor.start_child(
MeuApp.DynSup,
{Worker, arg}
)
# Contar filhos:
DynamicSupervisor.count_children(MeuApp.DynSup)O DynamicSupervisor supervisiona filhos criados em tempo de execução (não pré-definidos). O start_child adiciona processos dinamicamente — ideal para um número variável de workers.
Estratégias de supervisão
# :one_for_one - só o que falhou Supervisor.init(children, strategy: :one_for_one) # :one_for_all - todos reiniciam strategy: :one_for_all # :rest_for_one - o falhado e os seguintes strategy: :rest_for_one # max_restarts / max_seconds: Supervisor.init(children, strategy: :one_for_one, max_restarts: 3, max_seconds: 5)
As estratégias definem o reinício: :one_for_one (só o falhado), :one_for_all (todos) e :rest_for_one (o falhado e os seguintes). max_restarts limita reinícios.
ETS
# ETS: tabela em memória (mutável!)
tabela = :ets.new(:cache, [:set, :public])
# Inserir:
:ets.insert(tabela, {:chave, "valor"})
:ets.insert(tabela, [{:a, 1}, {:b, 2}])
# Procurar:
:ets.lookup(tabela, :chave) # [{:chave, "valor"}]
# Apagar:
:ets.delete(tabela, :chave)
# Named table:
:ets.new(:cache, [:set, :named_table, :public])O ETS (Erlang Term Storage) é uma tabela em memória partilhada e mutável, usada como cache de alta performance. Tipos: :set, :ordered_set, :bag. Acedida via módulo :ets.
Application
# mix.exs define a aplicação:
def application do
[
mod: {MeuApp.Application, []},
extra_applications: [:logger]
]
end
# Application inicia a árvore:
defmodule MeuApp.Application do
use Application
def start(_type, _args) do
children = [Contador]
Supervisor.start_link(children,
strategy: :one_for_one)
end
endUma Application é o ponto de entrada que arranca a árvore de supervisão. Define-se em mix.exs com mod: e implementa o callback start/2.
Links e monitors
# Link: falha propaga (bidirecional)
Process.link(pid)
# spawn_link: cria já ligado
spawn_link(fn -> tarefa() end)
# Monitor: notifica sem propagar
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, ^pid, razao} ->
IO.puts("Morreu: #{inspect(razao)}")
endUm link liga dois processos: se um falha, o outro também morre (propagação). Um monitor apenas notifica a falha (mensagem :DOWN) sem propagar, permitindo reagir.
Registry
# Registry: nomear processos dinâmicos
children = [
{Registry, keys: :unique, name: MeuApp.Registry}
]
# Registar processo:
Registry.register(MeuApp.Registry, "user_1", nil)
# Procurar pid:
Registry.lookup(MeuApp.Registry, "user_1")
# [{pid, valor}]
# Enviar a todos com uma chave:
Registry.dispatch(MeuApp.Registry, "sala", fn entries ->
for {pid, _} <- entries, do: send(pid, :msg)
end)O Registry permite nomear e procurar processos dinâmicos por chave. Com keys: :unique cada chave tem um processo; keys: :duplicate permite vários (útil para pub/sub).
Mix e Ferramentas
mix new
# Criar projeto: mix new meu_app # Com árvore de supervisão: mix new meu_app --sup # Estrutura gerada: # meu_app/ # lib/meu_app.ex # test/meu_app_test.exs # mix.exs # README.md
O mix new cria um novo projeto com a estrutura padrão. A flag --sup adiciona uma árvore de supervisão (Application). O mix.exs é o ficheiro de configuração.
mix format
# Formatar todo o projeto:
mix format
# Verificar sem alterar (CI):
mix format --check-formatted
# Configuração em .formatter.exs:
[
inputs: ["{mix,.formatter}.exs",
"{config,lib,test}/**/*.{ex,exs}"],
line_length: 98
]O mix format formata o código segundo o estilo oficial do Elixir. O --check-formatted valida sem alterar (útil em CI). A configuração fica em .formatter.exs.
mix.exs e dependências
defmodule MeuApp.MixProject do
use Mix.Project
def project do
[app: :meu_app, version: "0.1.0"]
end
def application do
[extra_applications: [:logger]]
end
defp deps do
[
{:phoenix, "~> 1.7"},
{:jason, "~> 1.4"}
]
end
endO mix.exs configura o projeto: project (nome/versão), application (apps OTP) e deps (dependências do Hex). A versão usa o formato ~> 1.7 (compatível).
IEx (REPL)
# Iniciar REPL: iex iex -S mix # com o projeto # Ajuda: h Enum.map # docs i "olá" # info do termo # Continuar expressão incompleta: # Enter em linha incompleta continua # Recompilar em runtime: recompile() # h() lista ajuda, c() compila ficheiro
O IEx é o REPL interativo. O iex -S mix carrega o projeto. O h mostra documentação, o i inspeciona um termo e o recompile() recompila em runtime.
Comandos mix
# Dependências: mix deps.get # descarregar mix deps.update jason # atualizar # Compilar: mix compile # Correr: mix run # script mix run -e "IO.puts(1)" # inline # Tarefas comuns: mix format # formatar código mix test # testes mix help # listar tarefas
O mix é a ferramenta de build: deps.get descarrega dependências, compile compila, run executa, format formata e test corre os testes.
Estrutura de projeto
meu_app/
lib/
meu_app.ex # módulo principal
meu_app/
servidor.ex # MeuApp.Servidor
test/
meu_app_test.exs
test_helper.exs
config/
config.exs
mix.exs
.formatter.exs
# Nome do ficheiro -> módulo:
# lib/meu_app/servidor.ex -> MeuApp.ServidorUm projeto organiza-se em lib/ (código), test/ (testes) e config/. O caminho do ficheiro define o módulo: lib/meu_app/servidor.ex é MeuApp.Servidor.
mix test e ExUnit
defmodule MathTest do
use ExUnit.Case
test "dobro de 2 é 4" do
assert Math.dobro(2) == 4
end
test "lista não vazia" do
refute Enum.empty?([1, 2])
end
test "lança erro" do
assert_raise ArgumentError, fn ->
raise ArgumentError
end
end
endO ExUnit é o framework de testes. Os testes usam test com assert/refute e correm com mix test. O assert_raise verifica exceções.