Metaprogramming

Elixir code is represented as three-element tuples {name, metadata, args}:

38.1 Quoted expressions — the AST

Elixir code is represented as three-element tuples {name, metadata, args}:

Literals that quote to themselves: atoms, numbers, lists, strings, two-element tuples.

quote do: 1 + 2
# {:+, [context: Elixir, imports: [{1, Kernel}, {2, Kernel}]], [1, 2]}

quote do: sum(1, 2, 3)
# {:sum, [], [1, 2, 3]}

38.2 unquote

x = 5
quote do: 1 + unquote(x)            # {:+, _, [1, 5]}
args = [1, 2]
quote do: sum(unquote_splicing(args))   # sum(1, 2)
Code.eval_quoted(quote do: 1 + 2)   # {3, []}
Macro.to_string(quote do: 1 + 2)    # "1 + 2"

38.3 Writing macros

Macros receive quoted arguments and return quoted code, expanded at compile time.

defmodule MyMacros do
  defmacro unless_(condition, do: block) do
    quote do
      if !unquote(condition), do: unquote(block)
    end
  end

  defmacro log_time(name, do: block) do
    quote do
      start = System.monotonic_time(:microsecond)
      result = unquote(block)
      IO.puts("#{unquote(name)} took #{System.monotonic_time(:microsecond) - start}µs")
      result
    end
  end
end

require MyMacros
MyMacros.unless_ 1 > 2, do: IO.puts("ok")
MyMacros.log_time "sleep", do: Process.sleep(10)

38.4 Hygiene

Variables defined inside a macro don't leak into the caller (start above won't clash with a caller's start). Escape deliberately with var!/1:

defmacro set_x(v), do: quote(do: var!(x) = unquote(v))

38.5 Macro tools

Macro.expand_once(quote(do: unless(true, do: 1)), __ENV__)
Macro.expand(ast, __ENV__)
Macro.escape(%{a: 1})       # convert a runtime value into its quoted form
Macro.prewalk(ast, fn node -> node end), Macro.postwalk
Macro.var(:name, nil)
__CALLER__, __ENV__, __MODULE__, __DIR__, __STACKTRACE__
quote location: :keep, bind_quoted: [x: x] do ... end   # bind_quoted evaluates once

38.6 Compile-time code generation

String.Unicode, Mime, and many libraries generate thousands of function clauses this way — fast pattern-matched lookups.

defmodule HTTPStatus do
  @codes %{200 => "OK", 404 => "Not Found", 500 => "Internal Server Error"}

  for {code, msg} <- @codes do
    def message(unquote(code)), do: unquote(msg)
  end
  def message(_), do: "Unknown"
end

38.7 Module hooks

defmodule Plugin do
  defmacro __before_compile__(_env) do
    quote do
      def registered, do: @plugins
    end
  end
end
# @before_compile Plugin, @after_compile, @on_definition

38.8 Rules for macros

Don't write a macro if a function will do. Keep macro bodies small; delegate to functions.

Document generated functions.

Macros make code harder to debug — use them for DSLs and compile-time optimisations.

38.9 Runtime code

Code.eval_string/2 (avoid on untrusted input), Code.string_to_quoted/1, Code.compile_string/1, Code.ensure_loaded?/1, Module.create/3.