10218813d3
- Add ## Contents and ## Decision Tree to all 10 existing pattern files - Fix embed_as/1 semantics inversion in types.md (:self → :dump) - Fix fabricated __meta__.changes reference in changesets.md - Fix default primary key type (:integer → :id) in schemas.md - Combine @impl subsections into single "Minimal Callback Annotation"
1138 lines
37 KiB
Markdown
1138 lines
37 KiB
Markdown
# Macros Patterns
|
||
|
||
Patterns extracted from the Elixir standard library source code.
|
||
|
||
## Contents
|
||
|
||
1. [Context-Aware Macros (__CALLER__.context)](#1-context-aware-macros-__caller__context)
|
||
2. [defguard — Macro for Guard-Safe Expressions](#2-defguard--macro-for-guard-safe-expressions)
|
||
3. [quote + unquote for Code Generation](#3-quote--unquote-for-code-generation)
|
||
4. [var! for Breaking Hygiene](#4-var-for-breaking-hygiene)
|
||
5. [Macro Expanding with Macro.expand](#5-macro-expanding-with-macroexpand)
|
||
6. [assert_no_match_or_guard_scope Pattern](#6-assert_no_match_or_guard_scope-pattern)
|
||
7. [Protocol Definition as a Macro (defprotocol)](#7-protocol-definition-as-a-macro-defprotocol)
|
||
8. [@fallback_to_any in Protocols](#8-fallback_to_any-in-protocols)
|
||
9. [use/2 as Macro Injection Point](#9-use2-as-macro-injection-point)
|
||
10. [Sigil Macros (Pattern for DSL Literals)](#10-sigil-macros-pattern-for-dsl-literals)
|
||
11. [Pipe Operator as a Macro](#11-pipe-operator-as-a-macro)
|
||
12. [Macro.generate_unique_arguments for Hygiene](#12-macrogenerate_unique_arguments-for-hygiene)
|
||
|
||
---
|
||
|
||
## 1. Context-Aware Macros (__CALLER__.context)
|
||
|
||
**Source:** [lib/elixir/lib/kernel.ex#L2032](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/kernel.ex#L2032) (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
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- A macro must behave differently in guards vs normal code
|
||
- You're writing an operator-like macro that users will put in guard clauses
|
||
- The same syntax needs different compilation strategies per context
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
# Macro that only works in normal code, crashes in guards
|
||
defmacro my_or(left, right) do
|
||
quote do
|
||
case unquote(left) do
|
||
x when x in [false, nil] -> unquote(right)
|
||
x -> x
|
||
end
|
||
end
|
||
end
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
defmacro my_or(left, right) do
|
||
case __CALLER__.context do
|
||
nil -> quote do
|
||
case unquote(left) do
|
||
x when x in [false, nil] -> unquote(right)
|
||
x -> x
|
||
end
|
||
end
|
||
:guard -> quote(do: :erlang.orelse(unquote(left), unquote(right)))
|
||
:match -> raise ArgumentError, "cannot use or/2 in match"
|
||
end
|
||
end
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- Your macro will never be used in guards (most macros)
|
||
- A simple `defguard` would suffice for guard-only usage
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# Checking __CALLER__.context in a macro that just generates module-level code
|
||
defmacro define_schema(fields) do
|
||
case __CALLER__.context do
|
||
nil -> generate_schema(fields)
|
||
:guard -> raise "can't use in guard" # Unnecessary
|
||
:match -> raise "can't use in match" # Unnecessary
|
||
end
|
||
end
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
defmacro define_schema(fields) do
|
||
generate_schema(fields)
|
||
end
|
||
```
|
||
|
||
**Why:** Context-checking adds complexity. Only use it when there's a legitimate code path for guards or matches. Module-level macros like schema definitions will never appear in guard context.
|
||
|
||
---
|
||
|
||
## 2. defguard — Macro for Guard-Safe Expressions
|
||
|
||
**Source:** [lib/elixir/lib/kernel.ex#L5889](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/kernel.ex#L5889)
|
||
|
||
**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
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- You need a guard-safe check that combines multiple guard expressions
|
||
- Multiple modules share the same guard condition
|
||
- The guard logic is complex enough to warrant a name
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
# Repeating guard logic everywhere
|
||
def process(n) when is_integer(n) and rem(n, 2) == 0 do
|
||
# handle even...
|
||
end
|
||
|
||
def validate(n) when is_integer(n) and rem(n, 2) == 0 do
|
||
# validate even...
|
||
end
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
defmodule Guards do
|
||
defguard is_even(n) when is_integer(n) and rem(n, 2) == 0
|
||
end
|
||
|
||
import Guards
|
||
|
||
def process(n) when is_even(n), do: # ...
|
||
def validate(n) when is_even(n), do: # ...
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- The check isn't guard-safe (calls functions, uses try/rescue, etc.)
|
||
- The guard is used in one place and is already readable
|
||
- You need runtime logic like database lookups in the check
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# Trying to use defguard for something that needs runtime state
|
||
defguard is_admin(user_id) when user_id in @admin_ids
|
||
# @admin_ids is a module attribute — frozen at compile time!
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
def admin?(user_id), do: user_id in load_admin_ids()
|
||
|
||
def process(request) do
|
||
if admin?(request.user_id), do: # ...
|
||
end
|
||
```
|
||
|
||
**Why:** `defguard` expressions are evaluated at compile time with only BIF access. If your "guard" needs runtime data, config, or database state, it's not a guard — it's a function.
|
||
|
||
---
|
||
|
||
## 3. quote + unquote for Code Generation
|
||
|
||
**Source:** [lib/elixir/lib/kernel.ex#L5624](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/kernel.ex#L5624) (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
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- A macro receives arguments that should be evaluated once in the expansion
|
||
- You're generating code that interpolates compile-time values into runtime code
|
||
- The macro argument has side effects or is expensive to compute
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
# unquote(fields) evaluated twice
|
||
defmacro setup(fields) do
|
||
quote do
|
||
validated = validate(unquote(fields))
|
||
@fields unquote(fields) # fields expression runs again!
|
||
end
|
||
end
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
defmacro setup(fields) do
|
||
quote bind_quoted: [fields: fields] do
|
||
validated = validate(fields)
|
||
@fields fields # Same bound value, no double-evaluation
|
||
end
|
||
end
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- You need to unquote into a pattern match position (bind_quoted can't do this)
|
||
- The expression is a simple literal or variable with no side effects
|
||
- You're building complex AST where you need fine-grained unquote placement
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# bind_quoted fails in pattern positions
|
||
defmacro match_field(field, value) do
|
||
quote bind_quoted: [field: field, value: value] do
|
||
def handle(%{field => result}) when result == value do
|
||
result
|
||
end
|
||
end
|
||
end
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
defmacro match_field(field, value) do
|
||
quote do
|
||
def handle(%{unquote(field) => result}) when result == unquote(value) do
|
||
result
|
||
end
|
||
end
|
||
end
|
||
```
|
||
|
||
**Why:** `bind_quoted` wraps values in regular variable bindings, which can't appear in pattern match positions. When you need to inject into patterns or guards, use direct `unquote`.
|
||
|
||
---
|
||
|
||
## 4. var! for Breaking Hygiene
|
||
|
||
**Source:** [lib/elixir/lib/kernel.ex#L4884](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/kernel.ex#L4884)
|
||
|
||
**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
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- A macro must inject code that references a variable from the caller's scope
|
||
- Building test/assertion macros where you need access to the test binding
|
||
- Module-level macros that accumulate into a module attribute the caller defines
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
# Hygienic variable — creates a NEW binding, doesn't access caller's
|
||
defmacro assert_stored(key) do
|
||
quote do
|
||
assert store[unquote(key)] # 'store' is hygienic — undefined in caller!
|
||
end
|
||
end
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
defmacro assert_stored(key) do
|
||
quote do
|
||
assert var!(store)[unquote(key)] # Accesses caller's 'store' variable
|
||
end
|
||
end
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- You can pass the value as a macro argument instead
|
||
- The macro doesn't need to reference caller-scope variables
|
||
- You're creating new bindings (just let hygiene work naturally)
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# Using var! when the value could just be passed as an argument
|
||
defmacro double_it do
|
||
quote do
|
||
var!(x) * 2 # Assumes caller has 'x' — fragile!
|
||
end
|
||
end
|
||
|
||
# Caller must have 'x' defined
|
||
x = 5
|
||
double_it() # => 10
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
defmacro double_it(value) do
|
||
quote do
|
||
unquote(value) * 2
|
||
end
|
||
end
|
||
|
||
double_it(5) # => 10, explicit, no hidden dependencies
|
||
```
|
||
|
||
**Why:** Every `var!` creates an invisible contract between macro and caller. Prefer explicit arguments. Reserve `var!` for cases where the contract is part of the documented API (like ExUnit's test context).
|
||
|
||
---
|
||
|
||
## 5. Macro Expanding with Macro.expand
|
||
|
||
**Source:** [lib/elixir/lib/kernel.ex#L2246](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/kernel.ex#L2246) (raise), 2319–2340 (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
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- Your macro receives user input that could be an alias, module attribute, or compile-time expression
|
||
- You need to make compile-time decisions based on what type of thing was passed
|
||
- Distinguishing between module names and runtime expressions in macro arguments
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
# Trying to pattern-match on the raw AST — breaks with aliases
|
||
defmacro wrap_error(module) when is_atom(module) do
|
||
# Never matches! Aliases are {:__aliases__, _, [...]} in AST
|
||
quote do: %{__exception__: true, __struct__: unquote(module)}
|
||
end
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
defmacro wrap_error(module_or_msg) do
|
||
case Macro.expand(module_or_msg, __CALLER__) do
|
||
atom when is_atom(atom) ->
|
||
quote do: unquote(atom).exception([])
|
||
_ ->
|
||
quote do: RuntimeError.exception(unquote(module_or_msg))
|
||
end
|
||
end
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- The macro argument is always a literal (string, number)
|
||
- You don't need compile-time branching on the argument's type
|
||
- The argument should remain unevaluated (lazy evaluation semantics)
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# Expanding an argument that's meant to be a runtime expression
|
||
defmacro log(message) do
|
||
expanded = Macro.expand(message, __CALLER__)
|
||
quote do
|
||
Logger.info(unquote(expanded))
|
||
end
|
||
end
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
defmacro log(message) do
|
||
quote do
|
||
Logger.info(unquote(message)) # Let it evaluate at runtime
|
||
end
|
||
end
|
||
```
|
||
|
||
**Why:** `Macro.expand` resolves compile-time constructs (aliases, module attributes). Expanding function calls or complex expressions can produce confusing results. Only expand when you need to determine the *type* of the argument at compile time.
|
||
|
||
---
|
||
|
||
## 6. assert_no_match_or_guard_scope Pattern
|
||
|
||
**Source:** [lib/elixir/lib/kernel.ex#L5384](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/kernel.ex#L5384) (def), 5415–5416 (defp), 5444–5445 (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
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- Your macro defines module-level constructs (functions, types, modules)
|
||
- The macro makes no sense inside a guard or pattern match
|
||
- Early failure with a clear message prevents confusing downstream errors
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
# No context validation — weird error deep in expansion
|
||
defmacro defroute(path, handler) do
|
||
quote do
|
||
@routes [{unquote(path), unquote(handler)} | @routes]
|
||
end
|
||
end
|
||
|
||
# User accidentally writes:
|
||
def check(x) when defroute("/foo", Foo), do: x # Cryptic error
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
defmacro defroute(path, handler) do
|
||
assert_no_match_or_guard_scope(__CALLER__.context, "defroute/2")
|
||
quote do
|
||
@routes [{unquote(path), unquote(handler)} | @routes]
|
||
end
|
||
end
|
||
|
||
# Now gives: "cannot invoke defroute/2 inside a guard"
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- Your macro IS designed to work in guards (use `defguard` or handle context)
|
||
- Your macro is designed to work in match context (like custom patterns)
|
||
- The macro is simple enough that misuse produces a clear error naturally
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# Adding guard assertion to a macro that could legitimately work anywhere
|
||
defmacro debug(expr) do
|
||
assert_no_match_or_guard_scope(__CALLER__.context, "debug/1")
|
||
quote do
|
||
IO.inspect(unquote(expr), label: unquote(Macro.to_string(expr)))
|
||
end
|
||
end
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
# Let it work anywhere it naturally can
|
||
defmacro debug(expr) do
|
||
quote do
|
||
IO.inspect(unquote(expr), label: unquote(Macro.to_string(expr)))
|
||
end
|
||
end
|
||
```
|
||
|
||
**Why:** Only add context assertions when misuse would produce confusing errors. If your macro naturally fails with a clear error in the wrong context, the assertion adds noise without value.
|
||
|
||
---
|
||
|
||
## 7. Protocol Definition as a Macro (defprotocol)
|
||
|
||
**Source:** [lib/elixir/lib/kernel.ex#L5734](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/kernel.ex#L5734), [lib/elixir/lib/protocol.ex#L290](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/protocol.ex#L290)
|
||
|
||
**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
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- You're building a type system or dispatch mechanism that operates on open-ended types
|
||
- Users need to add behavior for their own types without modifying your library
|
||
- You need dynamic dispatch based on the data type with compile-time consolidation
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
# Manual dispatch via case — closed, must modify to extend
|
||
def serialize(data) do
|
||
case data do
|
||
%User{} -> serialize_user(data)
|
||
%Post{} -> serialize_post(data)
|
||
_ -> raise "don't know how to serialize"
|
||
end
|
||
end
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
defprotocol Serializable do
|
||
@doc "Converts a data structure to a wire format"
|
||
def serialize(data)
|
||
end
|
||
|
||
defimpl Serializable, for: User do
|
||
def serialize(%User{name: name, email: email}), do: %{name: name, email: email}
|
||
end
|
||
|
||
# Anyone can add implementations for their own types
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- You have a closed set of types that won't be extended by users
|
||
- Simple pattern matching or behaviours solve the problem
|
||
- The dispatch doesn't depend on the first argument's type
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# Protocol for internal types that will never be extended
|
||
defprotocol InternalFormat do
|
||
def format(thing)
|
||
end
|
||
|
||
defimpl InternalFormat, for: [Map, List, BitString] do
|
||
# Only ever these three types, controlled by us
|
||
end
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
# Simple function with pattern matching — less machinery
|
||
def format(%{} = map), do: # ...
|
||
def format(list) when is_list(list), do: # ...
|
||
def format(binary) when is_binary(binary), do: # ...
|
||
```
|
||
|
||
**Why:** Protocols add indirection and compilation complexity (consolidation). If the type set is closed and you control all implementations, pattern matching is simpler, faster, and easier to understand.
|
||
|
||
---
|
||
|
||
## 8. @fallback_to_any in Protocols
|
||
|
||
**Source:** [lib/elixir/lib/inspect.ex#L162](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/inspect.ex#L162), [lib/elixir/lib/protocol.ex#L115](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/protocol.ex#L115)
|
||
|
||
**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
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- A protocol should handle ANY value rather than crashing on unknown types
|
||
- The generic behavior is genuinely useful (inspect, encode, display)
|
||
- You want a safe default that users can override for specific types
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
defprotocol Displayable do
|
||
def display(term)
|
||
end
|
||
|
||
# Every new struct crashes until someone adds an implementation:
|
||
# ** (Protocol.UndefinedError) protocol Displayable not implemented for %MyStruct{}
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
defprotocol Displayable do
|
||
@fallback_to_any true
|
||
def display(term)
|
||
end
|
||
|
||
defimpl Displayable, for: Any do
|
||
def display(term), do: inspect(term) # Reasonable fallback
|
||
end
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- The protocol operation doesn't make sense for arbitrary types
|
||
- Silently returning a default would hide bugs
|
||
- You want to force implementors to think about their implementation
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
defprotocol Saveable do
|
||
@fallback_to_any true
|
||
def save(data, repo)
|
||
end
|
||
|
||
defimpl Saveable, for: Any do
|
||
def save(_data, _repo), do: :ok # Silently does nothing!
|
||
end
|
||
|
||
# Dangerous: %UnknownStruct{} |> Saveable.save(repo) succeeds but saves nothing
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
defprotocol Saveable do
|
||
# No fallback — forces explicit implementation
|
||
def save(data, repo)
|
||
end
|
||
|
||
# Clear error on missing implementation:
|
||
# ** (Protocol.UndefinedError) protocol Saveable not implemented for %Foo{}
|
||
```
|
||
|
||
**Why:** Fallbacks that silently succeed are a bug factory. Use `@fallback_to_any` only when the default behavior is *genuinely useful* (like `Inspect`), not when "do nothing" masks errors.
|
||
|
||
---
|
||
|
||
## 9. use/2 as Macro Injection Point
|
||
|
||
**Source:** [lib/elixir/lib/kernel.ex#L6130](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/kernel.ex#L6130)
|
||
|
||
**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
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- Your module needs to inject behaviours, default function implementations, or compile hooks
|
||
- Users need a one-line "opt in" that sets up complex module infrastructure
|
||
- The setup requires `@behaviour`, `@before_compile`, `defoverridable`, or module attributes
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
# User must remember all the boilerplate
|
||
defmodule MyWorker do
|
||
@behaviour GenServer
|
||
|
||
def child_spec(init_arg) do
|
||
%{id: __MODULE__, start: {__MODULE__, :start_link, [init_arg]}}
|
||
end
|
||
|
||
def init(state), do: {:ok, state}
|
||
# ... more defaults ...
|
||
end
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
defmodule MyWorker do
|
||
use GenServer
|
||
|
||
# All boilerplate injected, just implement what you need
|
||
def init(state), do: {:ok, state}
|
||
end
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- `import` or `alias` is all you need (no module attributes, no callbacks)
|
||
- The module doesn't need to inject code — just provides functions
|
||
- You're using `use` to inject large amounts of invisible code that surprises users
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# use for something that should just be import
|
||
defmodule MyHelpers do
|
||
defmacro __using__(_opts) do
|
||
quote do
|
||
import MyHelpers # That's literally all it does
|
||
end
|
||
end
|
||
end
|
||
|
||
# User writes: use MyHelpers
|
||
# When they could just write: import MyHelpers
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
# Just tell users to import directly
|
||
defmodule MyHelpers do
|
||
def format_date(date), do: # ...
|
||
def format_money(amount), do: # ...
|
||
end
|
||
|
||
# In user's module:
|
||
import MyHelpers
|
||
```
|
||
|
||
**Why:** `use` implies "this module needs setup that goes beyond importing functions." If all you're doing is importing, `use` adds a layer of indirection that obscures what's happening. Reserve `use` for genuine module setup.
|
||
|
||
---
|
||
|
||
## 10. Sigil Macros (Pattern for DSL Literals)
|
||
|
||
**Source:** [lib/elixir/lib/kernel.ex#L6500](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/kernel.ex#L6500)+ (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
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- You have compile-time-known literal values that benefit from validation at compile time
|
||
- A domain has a specific syntax for literals (dates, regex, URIs, colors)
|
||
- You want zero runtime parsing cost for constant values
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
# Runtime parsing — fails at runtime, no compile-time validation
|
||
def deadline do
|
||
Date.from_iso8601!("2024-13-45") # Explodes at runtime
|
||
end
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
def deadline do
|
||
~D[2024-13-45] # Compile error! Invalid date caught immediately
|
||
end
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- Values come from runtime input (user data, config files, databases)
|
||
- The syntax doesn't provide meaningful compile-time validation
|
||
- A regular function or struct literal is equally clear
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# Sigil for something with no compile-time validation benefit
|
||
defmacro sigil_u({:<<>>, _, [string]}, []) do
|
||
quote do: String.upcase(unquote(string))
|
||
end
|
||
|
||
name = ~u"hello" # Just uppercases a string... why not String.upcase("hello")?
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
name = String.upcase("hello")
|
||
```
|
||
|
||
**Why:** Sigils shine when they validate or transform at compile time in ways that prevent runtime errors. A sigil that just wraps a function call without validation adds syntax without value.
|
||
|
||
---
|
||
|
||
## 11. Pipe Operator as a Macro
|
||
|
||
**Source:** [lib/elixir/lib/kernel.ex#L4509](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/kernel.ex#L4509)
|
||
|
||
**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"))
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- You want a zero-cost syntactic transformation (no runtime dispatch)
|
||
- The transformation is purely structural (rewriting argument positions)
|
||
- An operator or DSL benefits from left-to-right readability
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
# Deeply nested function calls — read inside-out
|
||
String.trim(String.downcase(String.replace(input, "_", " ")))
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
input
|
||
|> String.replace("_", " ")
|
||
|> String.downcase()
|
||
|> String.trim()
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- The pipe has only one step (just call the function directly)
|
||
- The piped value isn't the first argument (requires anonymous function wrappers)
|
||
- You're piping into a macro that needs special AST handling
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# Single-step pipe — adds noise
|
||
user
|
||
|> Map.get(:name)
|
||
|
||
# Piping where the value isn't first argument
|
||
data
|
||
|> Jason.encode!()
|
||
|> send_to(socket) # Is this send_to(encoded, socket)? Unclear.
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
# Single step — just call it
|
||
name = Map.get(user, :name)
|
||
|
||
# When argument position is unclear, break the pipe
|
||
encoded = data |> build_map() |> Jason.encode!()
|
||
send_to(socket, encoded) # Clear which arg is which
|
||
```
|
||
|
||
**Why:** Pipes optimize for readability of *sequential transformations*. When the data doesn't flow naturally as the first argument, or there's only one step, the pipe adds syntactic overhead without improving clarity.
|
||
|
||
---
|
||
|
||
## 12. Macro.generate_unique_arguments for Hygiene
|
||
|
||
**Source:** [lib/elixir/lib/macro.ex#L507](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/macro.ex#L507)
|
||
|
||
**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
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- Your macro generates variable bindings in quoted code
|
||
- You need `n` variables for a generated function clause or pattern
|
||
- The macro expands in user code where variable names could clash
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
# Hardcoded variable names — can clash with caller's variables
|
||
defmacro curry(fun, arity) do
|
||
args = Enum.map(1..arity, fn i -> Macro.var(:"arg\#{i}", __MODULE__) end)
|
||
quote do
|
||
fn unquote_splicing(args) -> unquote(fun).(unquote_splicing(args)) end
|
||
end
|
||
end
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
defmacro curry(fun, arity) do
|
||
args = Macro.generate_unique_arguments(arity, __CALLER__.module)
|
||
quote do
|
||
fn unquote_splicing(args) -> unquote(fun).(unquote_splicing(args)) end
|
||
end
|
||
end
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- You're using `bind_quoted` (it handles hygiene for you)
|
||
- The variable is accessed via `var!` (intentionally unhygienic)
|
||
- You only need one variable (a simple `quote do: var = ... end` is hygienic by default)
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# Using generate_unique_arguments for a single binding
|
||
defmacro time_it(expr) do
|
||
[start] = Macro.generate_unique_arguments(1, __CALLER__.module)
|
||
quote do
|
||
unquote(start) = System.monotonic_time()
|
||
result = unquote(expr)
|
||
IO.puts("Took \#{System.monotonic_time() - unquote(start)}")
|
||
result
|
||
end
|
||
end
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
# Regular quote hygiene handles single variables fine
|
||
defmacro time_it(expr) do
|
||
quote do
|
||
start = System.monotonic_time()
|
||
result = unquote(expr)
|
||
IO.puts("Took \#{System.monotonic_time() - start}")
|
||
result
|
||
end
|
||
end
|
||
```
|
||
|
||
**Why:** Variables created in `quote` are already hygienic by default — they can't clash with caller variables. `generate_unique_arguments` is needed when you're generating *multiple* variables dynamically (e.g., function parameters for a generated clause) where you need distinct names that also interoperate correctly.
|
||
|
||
## Decision Tree
|
||
|
||
- If a macro must behave differently in guards vs normal code → check `__CALLER__.context` (Pattern 1)
|
||
- If you need a reusable, compile-time-validated guard expression → use `defguard` (Pattern 2)
|
||
- If a macro argument might have side effects or be expensive → use `quote bind_quoted:` to evaluate once (Pattern 3)
|
||
- If a macro must reference a variable in the caller's scope → use `var!` sparingly (Pattern 4)
|
||
- If the macro receives input that could be an alias or module attribute → expand with `Macro.expand` before branching (Pattern 5)
|
||
- If your macro defines module-level constructs and should never appear in guards → assert context at the top (Pattern 6)
|
||
- If you need open-ended type dispatch that external code can extend → use `defprotocol` (Pattern 7)
|
||
- If a protocol should handle any value rather than raising on unknown types → use `@fallback_to_any true` (Pattern 8)
|
||
- If a module needs injected behaviours, attributes, or compile hooks → use the `use/2` + `__using__/1` pattern (Pattern 9)
|
||
- If you have compile-time-known literals that benefit from validation → define a sigil macro (Pattern 10)
|
||
- If you need a zero-cost syntactic transformation (argument rewriting) → implement as a macro like `|>` (Pattern 11)
|
||
|
||
<!-- PATTERN_COMPLETE -->
|