DevTools

Cheatsheet Elixir

Linguagem funcional na BEAM VM para aplicações concorrentes e escaláveis

Voltar às linguagens
Elixir
76 cards encontrados
Categorias:
Versões:

Recursos Avançados


8 cards
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"
end

Um 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)
end

O 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
end

O 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
# => 4

Um 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"]
end

Os 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 nomes

O 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


10 cards
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 seguinte

O 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>>      # bitstring

O 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-8

As 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


9 cards
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)    # 10

As 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=20

O 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}"
end

O 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


9 cards
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á")     # 4

Um 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ções

Uma 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)   # true

Os 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 tipo

Uma 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 devolve

Para 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


9 cards
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


9 cards
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
end

Os 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
end

A 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)
end

O 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


8 cards
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}"
end

O 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, KeyError

O 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)   # 120

A 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 else

O 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


7 cards
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
end

Um 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
end

Uma 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)}")
end

Um 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


7 cards
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
end

O 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.Servidor

Um 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
end

O ExUnit é o framework de testes. Os testes usam test com assert/refute e correm com mix test. O assert_raise verifica exceções.