Files
elixir-patterns/patterns/macros.md
T
Aaron Weiker 4ea9a884aa docs: idiomatic Elixir and Phoenix patterns with source citations
Extracted patterns, conventions, and code smells directly from the
Elixir and Phoenix source code with file path and line number citations.

Covers: GenServer, error handling, data transforms, process design,
testing, documentation, typespecs, macros, behaviours, module organization,
Phoenix-specific patterns, framework deviations, and anti-patterns.
2026-04-29 22:50:12 -07:00

14 KiB
Raw Blame History

Macros Patterns

Patterns extracted from the Elixir standard library source code.


1. Context-Aware Macros (CALLER.context)

Source: lib/elixir/lib/kernel.ex lines 20322067 (or/and operators)

What it does: Macros check __CALLER__.context to determine if they're being invoked in a guard, match, or normal context, and generate different code accordingly.

Why: Elixir guards have restricted syntax (no function calls, only BIFs). A macro like or must emit :erlang.orelse/2 in guards but can use a case expression in normal code. Context-awareness lets one macro serve multiple contexts correctly.

Anti-pattern: Writing macros that only work in one context and crash confusingly in others, or ignoring guard context entirely.

Code example:

defmacro left or right do
  case __CALLER__.context do
    nil -> build_boolean_check(:or, left, true, right)
    :match -> invalid_match!(:or)
    :guard -> quote(do: :erlang.orelse(unquote(left), unquote(right)))
  end
end

defmacro left and right do
  case __CALLER__.context do
    nil -> build_boolean_check(:and, left, right, false)
    :match -> invalid_match!(:and)
    :guard -> quote(do: :erlang.andalso(unquote(left), unquote(right)))
  end
end

2. defguard — Macro for Guard-Safe Expressions

Source: lib/elixir/lib/kernel.ex lines 58895966

What it does: defguard creates a public macro that the compiler verifies is valid in guard clauses. It raises at compile time if the guard body uses non-guard-safe expressions.

Why: Regular macros have no guard validation. defguard provides compile-time safety: you can't accidentally create a "guard" that uses IO.puts or Enum.map. The result can be used both in guards and normal code.

Anti-pattern: Defining guard-like functions with defmacro and no validation. Users will discover at runtime (or never) that the macro isn't guard-safe.

Code example:

defmodule Integer.Guards do
  defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
end

defmodule Collatz do
  import Integer.Guards

  def converge(n) when n > 0, do: step(n, 0)

  defp step(1, step_count), do: step_count

  defp step(n, step_count) when is_even(n) do
    step(div(n, 2), step_count + 1)
  end

  defp step(n, step_count) do
    step(3 * n + 1, step_count + 1)
  end
end

3. quote + unquote for Code Generation

Source: lib/elixir/lib/kernel.ex lines 56245640 (defstruct)

What it does: quote bind_quoted: [fields: fields] captures the macro argument into a variable available inside the quoted block. unquote injects computed values back into the AST.

Why: bind_quoted is preferred over raw unquote for macro arguments because it evaluates the expression exactly once and binds it to a variable. This prevents double-evaluation bugs and makes the generated code clearer.

Anti-pattern: Using unquote(expr) multiple times in a quote block when expr has side effects or is expensive — it will be evaluated multiple times in the expansion.

Code example:

defmacro defstruct(fields) do
  quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do
    {struct, derive, escaped_struct, kv, body} =
      Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__)

    case derive do
      [] -> :ok
      _ -> Protocol.__derive__(derive, __MODULE__, __ENV__)
    end

    def __struct__(), do: unquote(escaped_struct)
    def __struct__(unquote(kv)), do: unquote(body)

    Kernel.Utils.announce_struct(__MODULE__)
    struct
  end
end

4. var! for Breaking Hygiene

Source: lib/elixir/lib/kernel.ex lines 48844901

What it does: var! marks a variable inside quote as unhygienic — it will refer to the variable in the caller's scope rather than creating a new hygienic binding.

Why: Macro hygiene prevents accidental variable capture. But sometimes you want to reference the caller's variables (e.g., Kernel.var!(example) = 1 in tests, or injecting into module scope). var! is the explicit escape hatch.

Anti-pattern: Using var! casually. Every use breaks hygiene and creates implicit coupling between macro and caller. Prefer passing values through macro arguments instead.

Code example:

defmacro var!(var, context \\ nil)

defmacro var!({name, meta, atom}, context) when is_atom(name) and is_atom(atom) do
  # Remove counter and force them to be vars
  meta = :lists.keydelete(:counter, 1, meta)
  meta = :lists.keystore(:if_undefined, 1, meta, {:if_undefined, :raise})

  case Macro.expand(context, __CALLER__) do
    context when is_atom(context) ->
      {name, meta, context}

    other ->
      raise ArgumentError,
            "expected var! context to expand to an atom, got: #{Macro.to_string(other)}"
  end
end

5. Macro Expanding with Macro.expand

Source: lib/elixir/lib/kernel.ex lines 22462273 (raise), 23192340 (reraise)

What it does: Before generating code, the macro calls Macro.expand(message, __CALLER__) to resolve aliases at compile time. This determines the code path: if the expanded value is an atom (module name), it generates exception-specific code.

Why: Macros receive AST, not values. An alias like MyError is {:__aliases__, _, [:MyError]} in AST form. Expanding it resolves it to the actual module atom, enabling compile-time decisions about what code to generate.

Anti-pattern: Pattern-matching on raw AST shapes without expanding first. This breaks when users pass aliases, module attributes, or other compile-time expressions.

Code example:

defmacro raise(message) do
  erlang_error = fn expr ->
    quote do: :erlang.error(unquote(expr), :none, error_info: %{module: Exception})
  end

  case Macro.expand(message, __CALLER__) do
    atom when is_atom(atom) ->
      # It's a module — generate Module.exception([])
      erlang_error.(quote do: unquote(atom).exception([]))

    _ ->
      # It's a string or expression — wrap in RuntimeError
      erlang_error.(quote do: RuntimeError.exception(unquote(message)))
  end
end

6. assert_no_match_or_guard_scope Pattern

Source: lib/elixir/lib/kernel.ex lines 53845385 (def), 54155416 (defp), 54445445 (defmacro)

What it does: Macros that define module-level constructs (def, defp, defmacro, defmacrop) immediately assert they're not being called inside a guard or match context.

Why: Calling def inside a guard clause makes no sense but would produce a confusing error much later. Failing early with a clear message ("cannot invoke def/2 inside a guard") is better than a cryptic expansion error.

Anti-pattern: Not validating context at the top of macros that are context-sensitive. Let errors surface at the point of misuse, not deep in expansion.

Code example:

defmacro def(call, expr \\ nil) do
  assert_no_match_or_guard_scope(__CALLER__.context, "def/2")
  define(:def, call, expr, __CALLER__)
end

defmacro defmacro(call, expr \\ nil) do
  assert_no_match_or_guard_scope(__CALLER__.context, "defmacro/2")
  define(:defmacro, call, expr, __CALLER__)
end

7. Protocol Definition as a Macro (defprotocol)

Source: lib/elixir/lib/kernel.ex lines 57345745, lib/elixir/lib/protocol.ex lines 290318

What it does: defprotocol is a macro that creates a module with auto-generated dispatch functions. Inside the protocol, def is redefined as a macro that generates both a callback spec and the dispatch implementation.

Why: Protocols need complex machinery: type dispatch, consolidation, fallback handling. Wrapping this in macros means users write simple defprotocol + def syntax while the system generates efficient dispatch code.

Anti-pattern: Trying to implement protocol-like dispatch with regular modules and manual case statements. Use defprotocol — it handles consolidation, error messages, and type dispatch.

Code example:

# What the user writes:
defprotocol Size do
  @doc "Calculates the size of a data structure"
  def size(data)
end

# What the protocol's def macro generates internally:
quote generated: true do
  @__functions__ [{name, arity} | @__functions__]

  # Generate a fake definition with the user signature (for docs)
  Kernel.def(unquote(name)(unquote_splicing(args)))

  # Generate the actual dispatch implementation
  Kernel.def unquote(name)(unquote_splicing(call_args)) do
    impl_for!(term).unquote(name)(unquote_splicing(call_args))
  end

  # Copy spec as callback
  Module.spec_to_callback(__MODULE__, {name, arity}) ||
    @callback unquote(name)(unquote_splicing(type_args)) :: term
end

8. @fallback_to_any in Protocols

Source: lib/elixir/lib/inspect.ex line 162, lib/elixir/lib/protocol.ex lines 115131

What it does: Sets @fallback_to_any true inside a protocol definition to enable a default implementation via defimpl Protocol, for: Any.

Why: Some protocols (like Inspect) should work on any value rather than raising. The fallback provides a reasonable default (e.g., inspecting structs generically) while still allowing specific implementations to override.

Anti-pattern: Using @fallback_to_any true when failing explicitly is better. As the docs say: "it makes no sense to say a PID has a size of 0." Only use fallbacks when a generic implementation is genuinely useful.

Code example:

defprotocol Inspect do
  @moduledoc """
  The `Inspect` protocol converts an Elixir data structure into an
  algebra document.
  """

  # Handle structs in Any
  @fallback_to_any true

  @spec inspect(t, Inspect.Opts.t()) ::
          Inspect.Algebra.t() | {Inspect.Algebra.t(), Inspect.Opts.t()}
  def inspect(term, opts)
end

# The fallback implementation:
defimpl Inspect, for: Any do
  # Generic struct inspection using #ModuleName<...> notation
  def inspect(%module{} = struct, opts) do
    # ...
  end
end

9. use/2 as Macro Injection Point

Source: lib/elixir/lib/kernel.ex lines 61306145

What it does: use Module, opts is a macro that requires the module then calls Module.__using__(opts). The __using__/1 macro returns quoted code injected into the caller.

Why: This is Elixir's extension/plugin mechanism. It's explicit (you can see what use does by reading __using__/1), composable (multiple use calls stack), and documented (the admonition convention).

Anti-pattern: Using use when import or alias would suffice. use should be reserved for cases that need module attributes, callbacks, or compile hooks.

Code example:

# The implementation of use/2:
defmacro use(module, opts \\ []) do
  calls =
    Enum.map(expand_aliases(module, __CALLER__), fn
      expanded when is_atom(expanded) ->
        quote do
          require unquote(expanded)
          unquote(expanded).__using__(unquote(opts))
        end
    end)

  quote(do: (unquote_splicing(calls)))
end

# A typical __using__ implementation:
defmodule GenServer do
  defmacro __using__(_opts) do
    quote do
      @behaviour GenServer

      def child_spec(init_arg) do
        # ...default child spec...
      end

      defoverridable child_spec: 1
    end
  end
end

10. Sigil Macros (Pattern for DSL Literals)

Source: lib/elixir/lib/kernel.ex lines 65006850+ (sigil_S, sigil_s, sigil_r, sigil_D, etc.)

What it does: Each sigil (~r, ~D, ~s, etc.) is implemented as a defmacro sigil_X(term, modifiers) that receives the raw string content and modifier characters, then transforms them at compile time.

Why: Sigils provide compile-time validated literals. ~D[2024-01-15] is parsed and validated during compilation — invalid dates won't even compile. The macro pattern means new sigils can be added by any module.

Anti-pattern: Parsing literal values at runtime when they're known at compile time. Sigils shift validation left to compilation.

Code example:

defmacro sigil_D(date_string, modifiers)

defmacro sigil_D({:<<>>, _, [string]}, []) do
  # Parses and validates at compile time
  {{:., _, [Date, :sigil_D]}, _, [{:<<>>, _, [string]}, []]}
end

# Usage:
date = ~D[2024-01-15]  # Compile-time validated Date struct

11. Pipe Operator as a Macro

Source: lib/elixir/lib/kernel.ex line 4509

What it does: The |> pipe operator is a macro that rewrites left |> right into right(left), inserting the left expression as the first argument of the right expression.

Why: It's purely syntactic transformation — there's no runtime dispatch. Being a macro means it's zero-cost at runtime while providing the ergonomic left-to-right reading order.

Anti-pattern: Implementing operator-like syntax as runtime function calls when they could be compile-time transformations.

Code example:

defmacro left |> right do
  [{h, _} | t] = Macro.unpipe({:|>, [], [left, right]})

  fun = fn {x, pos}, acc ->
    Macro.pipe(acc, x, pos)
  end

  :lists.foldl(fun, h, t)
end

# Transforms at compile time:
# "hello" |> String.upcase() |> String.reverse()
# becomes:
# String.reverse(String.upcase("hello"))

12. Macro.generate_unique_arguments for Hygiene

Source: lib/elixir/lib/macro.ex lines 507520

What it does: Macro.generate_unique_arguments(n, context) creates n unique variable AST nodes that won't conflict with any user variables.

Why: When a macro needs to generate variable bindings in quoted code, using generate_unique_arguments guarantees hygiene. The variables get unique counters that can't clash with user-defined names.

Anti-pattern: Using hardcoded variable names in macros (like x, acc, state) which can shadow or be shadowed by user variables.

Code example:

@doc """
Generates a list of `n` unique arguments.

## Examples

    iex> [var1, var2] = Macro.generate_unique_arguments(2, __CALLER__.module)

"""
@doc since: "1.11.3"
@spec generate_unique_arguments(0, context :: atom) :: []
@spec generate_unique_arguments(pos_integer, context) ::
        [{atom, [counter: integer], context}, ...]
      when context: atom