- 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"
37 KiB
Macros Patterns
Patterns extracted from the Elixir standard library source code.
Contents
- Context-Aware Macros (CALLER.context)
- defguard — Macro for Guard-Safe Expressions
- quote + unquote for Code Generation
- var! for Breaking Hygiene
- Macro Expanding with Macro.expand
- assert_no_match_or_guard_scope Pattern
- Protocol Definition as a Macro (defprotocol)
- @fallback_to_any in Protocols
- use/2 as Macro Injection Point
- Sigil Macros (Pattern for DSL Literals)
- Pipe Operator as a Macro
- Macro.generate_unique_arguments for Hygiene
1. Context-Aware Macros (CALLER.context)
Source: 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:
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:
# 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:
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
defguardwould suffice for guard-only usage
Over-application example:
# 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:
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
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
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:
# 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:
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:
# 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:
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 (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
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:
# 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:
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:
# 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:
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
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
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:
# 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:
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:
# 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:
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 (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:
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:
# 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:
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:
# 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:
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 (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:
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:
# 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:
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
defguardor 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:
# 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:
# 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, 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:
# 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:
# 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:
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:
# 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:
# 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, 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:
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:
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:
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:
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:
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
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
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:
# 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:
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:
importoraliasis all you need (no module attributes, no callbacks)- The module doesn't need to inject code — just provides functions
- You're using
useto inject large amounts of invisible code that surprises users
Over-application example:
# 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:
# 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+ (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
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:
# Runtime parsing — fails at runtime, no compile-time validation
def deadline do
Date.from_iso8601!("2024-13-45") # Explodes at runtime
end
Example — after:
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:
# 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:
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
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"))
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:
# Deeply nested function calls — read inside-out
String.trim(String.downcase(String.replace(input, "_", " ")))
Example — after:
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:
# 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:
# 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
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
When to Use
Triggers:
- Your macro generates variable bindings in quoted code
- You need
nvariables for a generated function clause or pattern - The macro expands in user code where variable names could clash
Example — before:
# 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:
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 = ... endis hygienic by default)
Over-application example:
# 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:
# 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.expandbefore 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__/1pattern (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)