Lesson 15 / 25
Macros and Metaprogramming
Understand quote, unquote, use and when to write macros.
Code that writes code
Elixir code is represented internally as an AST of three-element tuples, and macros transform that AST at compile time. quote turns code into its AST, and unquote injects values into a quoted expression. defmacro defines a macro whose arguments arrive as AST and whose return value is AST inserted at the call site. Much of Elixir itself is built with macros: if, unless, defstruct, |> and the test "..." do syntax in ExUnit are macros. use Module calls that module's __using__/1 macro, which typically injects functions, imports and module attributes; this is how use GenServer, use Phoenix.Controller and use Ecto.Schema work. Macros are hygienic: variables defined inside a macro do not leak into the caller unless you use var!. Use Macro.to_string and Macro.expand to inspect them. The community guideline is clear: do not write a macro when a function will do. Macros make code harder to read and debug, so reserve them for DSLs, compile-time validation and removing real boilerplate.
A small macro and a __using__ hook
quote and unquote generate functions at compile time.
defmodule Shop.Statuses do
@doc "Generates predicate functions such as paid?/1 for each status."
defmacro defstatuses(statuses) do
defs =
for status <- statuses do
name = :"#{status}?"
quote do
def unquote(name)(%{status: s}), do: s == unquote(status)
end
end
quote do
unquote_splicing(defs)
end
end
end
defmodule Shop.OrderStatus do
require Shop.Statuses
Shop.Statuses.defstatuses([:placed, :paid, :shipped])
end
IO.inspect(Shop.OrderStatus.paid?(%{status: :paid})) # true
defmodule Shop.Auditable do
defmacro __using__(opts) do
entity = Keyword.fetch!(opts, :entity)
quote do
require Logger
def audit(action, id) do
Logger.info("#{unquote(entity)} #{id}: #{action}")
end
end
end
end
defmodule Shop.Invoices do
use Shop.Auditable, entity: "invoice" # injects audit/2
end
IO.inspect(quote(do: 1 + 2)) # {:+, [context: Elixir, imports: ...], [1, 2]}Functions first
Before writing a macro, try a plain function, a higher-order function or a module attribute. Macros pay off when they remove significant boilerplate or provide compile-time checks that functions cannot.
Quick check: What does `use Shop.Auditable, entity: "invoice"` do?
- Imports a module at run time
- Creates a process
- Calls Shop.Auditable.__using__/1 at compile time, which injects code into the calling module
- Installs a dependency
Answer
Calls Shop.Auditable.__using__/1 at compile time, which injects code into the calling module — use invokes the __using__ macro, which returns code to inject.