Files
aweiker 10218813d3 docs: backfill TOC + decision trees, fix review findings
- 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"
2026-05-01 22:13:35 -07:00

37 KiB
Raw Permalink Blame History

Macros Patterns

Patterns extracted from the Elixir standard library source code.

Contents

  1. Context-Aware Macros (CALLER.context)
  2. defguard — Macro for Guard-Safe Expressions
  3. quote + unquote for Code Generation
  4. var! for Breaking Hygiene
  5. Macro Expanding with Macro.expand
  6. assert_no_match_or_guard_scope Pattern
  7. Protocol Definition as a Macro (defprotocol)
  8. @fallback_to_any in Protocols
  9. use/2 as Macro Injection Point
  10. Sigil Macros (Pattern for DSL Literals)
  11. Pipe Operator as a Macro
  12. 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 defguard would 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), 23192340 (reraise)

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

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

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

Code example:

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

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

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

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), 54155416 (defp), 54445445 (defmacro)

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

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

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

Code example:

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

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

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 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:

# 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:

  • 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:

# 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 n variables 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 = ... end is 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.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)