Macros em Elixir: escrevendo código que escreve código
1. Introdução às Macros: O Poder da Metaprogramação em Elixir
Macros em Elixir são ferramentas de metaprogramação que permitem gerar código durante a compilação. Diferentemente de funções comuns, que operam em tempo de execução com valores, as macros operam em tempo de compilação com a própria estrutura do código — a Árvore Sintática Abstrata (AST). Isso significa que uma macro recebe código como entrada e produz código transformado como saída.
No ecossistema Elixir, macros são onipresentes: o Ecto usa macros para definir esquemas de banco de dados (schema), o Phoenix as utiliza para roteamento (get, post), e o ExUnit depende delas para criar testes (test, assert). No entanto, macros devem ser usadas com moderação — cada macro adiciona complexidade ao código, dificulta a depuração e quebra a composição funcional tradicional. A regra de ouro é: prefira funções sempre que possível; recorra a macros apenas quando precisar de transformações em tempo de compilação.
2. AST (Árvore Sintática Abstrata): A Matéria-Prima das Macros
No Elixir, todo código é representado internamente como uma estrutura de dados de três elementos: uma tupla contendo o nome da função/macro, o módulo e os argumentos. O Elixir expõe essa representação através de quote, que captura o código como AST.
Para inspecionar o AST de uma expressão simples:
iex> quote do: 2 + 3
{:+, [context: Elixir, import: Kernel], [2, 3]}
Expressões mais complexas geram ASTs aninhados:
iex> quote do: if true, do: :ok, else: :error
{:if, [context: Elixir, import: Kernel], [true, [do: :ok, else: :error]]}
A função Macro.to_string/1 faz o caminho inverso, convertendo AST de volta para código legível:
iex> ast = quote do: Enum.map([1,2,3], &(&1 * 2))
iex> ast |> Macro.to_string() |> IO.puts()
Enum.map([1, 2, 3], &(&1 * 2))
Essa capacidade de manipular código como dados é a base de toda metaprogramação em Elixir. Compreender o AST é essencial para escrever macros previsíveis e seguras.
3. quote e unquote: Construindo e Injetando Código
quote captura um bloco de código e retorna sua representação AST. unquote faz o oposto: permite injetar valores ou expressões dentro de um bloco quote.
Exemplo básico de quote:
iex> quote do: 1 + 1
{:+, [context: Elixir, import: Kernel], [1, 1]}
Com unquote, podemos interpolar valores dinâmicos:
iex> x = 42
iex> quote do: 1 + unquote(x)
{:+, [context: Elixir, import: Kernel], [1, 42]}
Unquoting de expressões completas:
iex> expr = quote do: 5 * 3
iex> quote do: unquote(expr) + 10
{:+, [context: Elixir, import: Kernel],
[{:*, [context: Elixir, import: Kernel], [5, 3]}, 10]}
Essa combinação permite construir ASTs dinâmicos de forma segura e controlada, injetando valores calculados em tempo de compilação dentro de estruturas de código.
4. defmacro: Criando Sua Primeira Macro
A estrutura básica de uma macro usa defmacro no lugar de def. A diferença fundamental: def recebe valores e retorna valores em tempo de execução; defmacro recebe AST (código não avaliado) e retorna AST transformado em tempo de compilação.
Exemplo: uma macro unless personalizada:
defmodule MyMacros do
defmacro my_unless(condition, do: block) do
quote do
if !unquote(condition), do: unquote(block)
end
end
end
defmodule Test do
require MyMacros
def check(val) do
MyMacros.my_unless val > 10 do
IO.puts("Valor é menor ou igual a 10")
end
end
end
Outro exemplo prático: macro if com logging automático:
defmodule LogMacros do
defmacro logged_if(condition, do: block) do
quote do
result = if unquote(condition), do: unquote(block)
IO.puts("Condição: #{unquote(Macro.to_string(condition))} => #{result}")
result
end
end
end
Note que require é necessário para usar macros de outro módulo, pois elas são expandidas em tempo de compilação.
5. Higiene de Macros: Evitando Conflitos de Nomes
Um dos maiores desafios com macros é a "captura de variáveis" — quando uma macro introduz variáveis que conflitam com variáveis do contexto de chamada. O Elixir resolve isso com higiene de macros: variáveis criadas dentro de quote são únicas e não interferem com o escopo externo.
Exemplo de problema de higiene:
defmodule BadMacro do
defmacro set_x(val) do
quote do
x = unquote(val) # Esta variável x é higiênica
end
end
end
x = 10
BadMacro.set_x(20)
IO.puts(x) # Ainda imprime 10, não 20
Para quebrar a higiene intencionalmente, use var!:
defmodule Unhygienic do
defmacro set_x!(val) do
quote do
var!(x) = unquote(val)
end
end
end
x = 10
Unhygienic.set_x!(20)
IO.puts(x) # Agora imprime 20
Para valores complexos (listas, mapas, tuplas), use Macro.escape/1:
defmodule SafeMacro do
defmacro store_data(data) do
escaped_data = Macro.escape(data)
quote do: unquote(escaped_data)
end
end
6. Macros Avançadas: Macro.prewalk e Manipulação de AST
Para transformações complexas do AST, o Elixir oferece Macro.prewalk/2 e Macro.postwalk/2, que percorrem recursivamente a árvore.
Exemplo: macro @decorator para logging automático de chamadas:
defmodule Decorator do
defmacro def_logged({:when, _, [func_head, guard]}) do
quote do
def unquote(func_head) when unquote(guard) do
IO.puts("Chamando: #{unquote(Macro.to_string(func_head))}")
result = unquote(func_head)
IO.puts("Resultado: #{result}")
result
end
end
end
defmacro def_logged(func_head) do
quote do
def unquote(func_head) do
IO.puts("Chamando: #{unquote(Macro.to_string(func_head))}")
result = unquote(func_head)
IO.puts("Resultado: #{result}")
result
end
end
end
end
defmodule Calculator do
require Decorator
Decorator.def_logged add(a, b), do: a + b
Decorator.def_logged multiply(a, b), do: a * b
end
Macro.prewalk permite modificar cada nó antes de processar seus filhos:
defmodule Transform do
defmacro add_logging(expr) do
Macro.prewalk(expr, fn
{:+, meta, args} ->
quote do
result = unquote({:+, meta, args})
IO.puts("Soma resultou em: #{result}")
result
end
node -> node
end)
end
end
7. Casos de Uso Reais: Macros em Frameworks e Bibliotecas
Ecto usa macros extensivamente para definição de esquemas:
defmodule User do
use Ecto.Schema
schema "users" do
field :name, :string
field :email, :string
field :age, :integer, default: 0
timestamps()
end
end
Cada field e timestamps são macros que geram funções de acesso, validações e metadados do schema.
Phoenix utiliza macros para roteamento declarativo:
defmodule MyAppWeb.Router do
use Phoenix.Router
get "/users", UserController, :index
post "/users", UserController, :create
resources "/posts", PostController
end
Cada get, post e resources expande em múltiplas funções de pipeline e match de rotas.
ExUnit define toda sua DSL de testes via macros:
defmodule MyTest do
use ExUnit.Case
test "truth" do
assert 1 + 1 == 2
end
end
test e assert são macros que geram funções de teste com captura de exceções e formatação de falhas.
8. Limitações e Boas Práticas: Quando Não Usar Macros
Macros têm limitações importantes:
- Não compõem com pipes:
1 |> my_macronão funciona, pois macros operam em AST, não em valores. - Depuração complexa: erros apontam para código gerado, não para a chamada original.
- Quebram previsibilidade: o comportamento pode variar dependendo do contexto de compilação.
Checklist para decidir se uma macro é necessária:
- A transformação precisa acontecer em tempo de compilação?
- Você está criando uma DSL que precisa de sintaxe especial?
- A alternativa com funções seria significativamente mais verbosa ou menos segura?
- O código gerado é determinístico e previsível?
- Você pode testar a macro isoladamente?
Alternativas preferíveis:
- Funções de ordem superior (map, reduce, filter)
- Protocolos (polimorfismo)
- Módulos comuns com funções bem definidas
- Comportamentos (behaviours)
Em resumo: macros são extremamente poderosas, mas seu uso deve ser reservado para situações onde a metaprogramação em tempo de compilação traz benefícios claros — como criação de DSLs, otimizações ou redução de boilerplate — e não como substituto para boa arquitetura de software.
Referências
- Documentação oficial de Macros em Elixir — Guia completo sobre definição e uso de macros, incluindo higiene e AST.
- Metaprogramming Elixir: Write Code that Writes Code — Livro de Chris McCord sobre metaprogramação avançada com macros.
- Elixir School: Metaprogramming — Tutorial prático sobre quote, unquote e criação de macros.
- Understanding Elixir Macros: A Beginner's Guide — Artigo introdutório com exemplos do mundo real de macros.
- Ecto.Schema source code — Código-fonte real mostrando como macros são usadas em um framework de banco de dados.
- Phoenix Router source code — Implementação de macros de roteamento no framework web Phoenix.
- ExUnit source code — Como o ExUnit implementa macros
testeassertpara testes. - Macro.escape/1 documentation — Referência oficial para escape de valores em macros.
- Elixir Forum: When to use macros — Discussão da comunidade sobre boas práticas e trade-offs de macros.