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.