Do you want to pick up from where you left of?
Take me there

Metaprogramação

Metaprogramação é o processo de utilização de código para escrever código. Em Elixir isso nos dá a capacidade de estender a linguagem para se adequar às nossas necessidades e dinamicamente alterar o código. Vamos começar observando como Elixir é representado por debaixo dos panos, em seguida como modificá-lo, e finalmente, como podemos usar esse conhecimento para estendê-la.

Uma palavra de cautela: Metaprogramação é complicado e só deve ser usado quando for absolutamente necessário. O uso excessivo certamente levará a um código complexo que é difícil de entender e debugar

Quote

O primeiro passo para metaprogramação é a compreensão de como as expressões são representadas. Em Elixir, a árvore de sintaxe abstrata (AST), a representação interna do nosso código, é composta de tuplas. Estas tuplas contêm três partes: o nome da função, metadados e argumentos da função.

A fim de ver essas estruturas internas, Elixir nos fornece a função quote/2. Usando quote/2 podemos converter o código Elixir em sua representação subjacente:

iex> quote do: 42
42
iex> quote do: "Hello"
"Hello"
iex> quote do: :world
:world
iex> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}
iex> quote do: if value, do: "True", else: "False"
{:if, [context: Elixir, import: Kernel],
 [{:value, [], Elixir}, [do: "True", else: "False"]]}

Notou que os três primeiros exemplos não retornaram tuplas? Existem cinco literais que retornam eles mesmos quando citados (quoted):

iex> :atom
:atom
iex> "string"
"string"
iex> 1 # All numbers
1
iex> [1, 2] # Lists
[1, 2]
iex> {"hello", :world} # 2 element tuples
{"hello", :world}

Unquote

Agora que podemos recuperar a estrutura interna do nosso código, como podemos modificá-lo? Para injetar novo código ou valores usamos unquote/1. Quando o unquote é usado em uma expressão, essa expressão será validada e injetada na AST. Para demonstrar unquote/1 vejamos alguns exemplos:

iex> denominator = 2
2
iex> quote do: divide(42, denominator)
{:divide, [], [42, {:denominator, [], Elixir}]}
iex> quote do: divide(42, unquote(denominator))
{:divide, [], [42, 2]}

No primeiro exemplo, a variável denominator é citada de modo que a AST resultante inclua uma tupla para acessar a variável. No exemplo de unquote/1, o código resultante inclui o valor de denominator no lugar.

Macros

Uma vez que entendemos quote/2 e unquote/1 estamos prontos para mergulhar em macros. É importante lembrar que macros, como todas as metaprogramações, devem ser usadas com moderação.

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

Começamos por definir uma macro usando defmacro/2 que por si só, já é uma macro, como grande parte da linguagem Elixir (pode parecer confuso, mas você vai se acostumar com isso). Como exemplo, vamos implementar unless como uma macro. Lembre-se que a nossa macro precisa retornar uma expressão entre aspas:

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

Vamos usar require OurMacro para declarar que queremos usar a macro:

iex> require OurMacro
nil
iex> OurMacro.unless true, do: "Hi"
nil
iex> OurMacro.unless false, do: "Hi"
"Hi"

Já que macros substituem o código em nossa aplicação, podemos controlar quando e o que é compilado. Um exemplo disso pode ser encontrado no módulo Logger. Quando o log está desabilitado, nenhum código é injetado e a aplicação resultante não contém referências ou chamadas de função para logar uma mensagem. Isso é diferente de outras linguagens onde ainda existe a sobrecarga de uma chamada de função, mesmo quando a implementação é NOP (nenhuma operação a ser executada).

Para demonstrar isso, vamos fazer um logger simples que pode ser ativado ou desativado:

defmodule Logger do
  defmacro log(msg) do
    if Application.get_env(:logger, :enabled) do
      quote do
        IO.puts("Logged message: #{unquote(msg)}")
      end
    end
  end
end

defmodule Example do
  require Logger

  def test do
    Logger.log("This is a log message")
  end
end

Com o log ativado a nossa função test resultaria em um código parecido com isto:

def test do
  IO.puts("Logged message: #{"This is a log message"}")
end

Mas se desativarmos o log, o código resultante seria:

def test do
end

Debugando

Certo, agora sabemos como usar quote/2, unquote/1 e escrever macros. Mas e se você tiver uma grande quantidade de código quoted e você precisa entendê-lo? Nesse caso, você pode usar Macro.to_string/2. Veja este exemplo:

iex> Macro.to_string(quote(do: foo.bar(1, 2, 3)))
"foo.bar(1, 2, 3)"

E quando você quiser ver o código gerado por macros, você pode combinar eles com Macro.expand/2 e Macro.expand_once/2, essas funções expandem as macros para seus códigos quoted. O primeiro pode expandir ele várias vezes, enquanto o último - apenas uma vez. Por exemplo, vamos modificar o exemplo do unless da seção anterior:

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

require OurMacro

quoted =
  quote do
    OurMacro.unless(true, do: "Hi")
  end
iex> quoted |> Macro.expand_once(__ENV__) |> Macro.to_string |> IO.puts
if(!true) do
  "Hi"
end

Se nós rodarmos o mesmo código com Macro.expand/2, é intrigante:

iex> quoted |> Macro.expand(__ENV__) |> Macro.to_string |> IO.puts
case(!true) do
  x when x in [false, nil] ->
    nil
  _ ->
    "Hi"
end

Você deve lembrar que nós mencionamos que if é um macro em Elixir, aqui nós vemos expandido para sua declaração case subjacente.

Macros Privadas

Embora não seja tão comum, Elixir suporta macros privadas. Uma macro privada é definida com defmacrop e só pode ser chamada a partir do módulo no qual ela foi definida. Macros privadas devem ser definidas antes do código que as invoca.

Higienização de Macros

A característica de como macros interagem com o contexto de quem a chamou quando expandida é conhecida como a higienização de macro. Por padrão, macros em Elixir são higiênicas e não entrarão em conflito com nosso contexto:

defmodule Example do
  defmacro hygienic do
    quote do: val = -1
  end
end

iex> require Example
nil
iex> val = 42
42
iex> Example.hygienic
-1
iex> val
42

Mas e se quisermos manipular o valor de val ? Para marcar uma variável como sendo anti-higiênica podemos usar var!/2. Vamos atualizar o nosso exemplo para incluir outra macro utilizando var!/2!

defmodule Example do
  defmacro hygienic do
    quote do: val = -1
  end

  defmacro unhygienic do
    quote do: var!(val) = -1
  end
end

Vamos comparar a forma como eles interagem com nosso contexto:

iex> require Example
nil
iex> val = 42
42
iex> Example.hygienic
-1
iex> val
42
iex> Example.unhygienic
-1
iex> val
-1

Ao incluir var!/2 em nossa macro, manipulamos o valor de val, sem passá-la em nossa macro. O uso de macros não higiênicas deve ser mantido a um mínimo. Ao incluir var!/2, aumentamos o risco de um conflito de resolução de variável.

Binding

Nós já cobrimos a utilidade do unquote/1 mas há outra maneira de injetar valores em nosso código: binding. Com o binding de variável, somos capazes de incluir múltiplas variáveis em nossa macro e garantir que eles são unquoted apenas uma vez, evitando revalidações acidentais. Para usar variáveis de vinculação (binding) precisamos passar uma lista de palavras-chave para a opção bind_quoted de quote/2.

Para ver o benefício de bind_quote e para demonstrar o problema de revalidação, vamos usar um exemplo. Podemos começar criando uma macro que simplesmente exibe a expressão duas vezes:

defmodule Example do
  defmacro double_puts(expr) do
    quote do
      IO.puts(unquote(expr))
      IO.puts(unquote(expr))
    end
  end
end

Vamos testar a nossa nova macro, passando a hora atual do sistema. Devemos ver o output sendo exibido duas vezes:

iex> Example.double_puts(:os.system_time)
1450475941851668000
1450475941851733000

Os tempos são diferentes! O que aconteceu? Usar unquote/1 na mesma expressão várias vezes, faz com que a expressão seja revalidada cada vez que é chamada, o que pode ter consequências inesperadas. Vamos atualizar o exemplo para usar bind_quoted e ver o que acontece:

defmodule Example do
  defmacro double_puts(expr) do
    quote bind_quoted: [expr: expr] do
      IO.puts(expr)
      IO.puts(expr)
    end
  end
end

iex> require Example
nil
iex> Example.double_puts(:os.system_time)
1450476083466500000
1450476083466500000

Com bind_quoted temos o resultado esperado: o mesmo tempo impresso duas vezes.

Agora que cobrimos quote/2, unquote/1, e defmacro/2 temos todas as ferramentas necessárias para estender Elixir para atender às nossas necessidades.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!