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

394 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:**
```elixir
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:**
```elixir
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:**
```elixir
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:**
```elixir
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:**
```elixir
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:**
```elixir
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:**
```elixir
# 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:**
```elixir
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 `require`s 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:**
```elixir
# 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:**
```elixir
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:**
```elixir
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:**
```elixir
@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
```