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.
This commit is contained in:
Aaron Weiker
2026-04-29 22:50:12 -07:00
commit 4ea9a884aa
16 changed files with 4857 additions and 0 deletions
+393
View File
@@ -0,0 +1,393 @@
# 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
```