docs: add when/when-not to data-transforms
This commit is contained in:
@@ -0,0 +1,749 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Add When to Use / When NOT to Use sections to macros.md
|
||||
Parts: 0=preamble, 1=section1, 2=section2, ..., 12=section12"""
|
||||
|
||||
with open("patterns/macros.md", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
separator = "\n\n---\n\n"
|
||||
parts = content.split(separator)
|
||||
assert len(parts) == 13, f"Expected 13 parts, got {len(parts)}"
|
||||
|
||||
# Index 1-12 maps to sections 1-12
|
||||
when_sections = {
|
||||
1: '''
|
||||
### 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: '''
|
||||
### 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: '''
|
||||
### 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: '''
|
||||
### 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: '''
|
||||
### 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: '''
|
||||
### 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: '''
|
||||
### 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: '''
|
||||
### 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: '''
|
||||
### 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: '''
|
||||
### 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: '''
|
||||
### 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: '''
|
||||
### 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.''',
|
||||
}
|
||||
|
||||
for i in range(len(parts)):
|
||||
if i in when_sections:
|
||||
parts[i] = parts[i].rstrip() + "\n\n" + when_sections[i].strip()
|
||||
|
||||
output = separator.join(parts) + "\n"
|
||||
|
||||
with open("patterns/macros.md", "w") as f:
|
||||
f.write(output)
|
||||
|
||||
print(f"Done! {len(output)} chars")
|
||||
@@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Add When to Use / When NOT to Use sections to modules.md"""
|
||||
|
||||
with open("patterns/modules.md", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
separator = "\n\n---\n\n"
|
||||
parts = content.split(separator)
|
||||
print(f"Found {len(parts)} parts")
|
||||
|
||||
when_sections = {
|
||||
1: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Your module has grown beyond ~300 lines with distinct sub-responsibilities
|
||||
- External code only needs the parent module but implementation is complex
|
||||
- You find yourself prefixing private functions with a concept name (e.g., `scope_push`, `scope_pop`)
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
# Everything crammed into one flat module
|
||||
defmodule MyApp.Router do
|
||||
# 800 lines mixing route compilation, scope tracking, and helper generation
|
||||
def compile_route(...), do: # ...
|
||||
def push_scope(...), do: # ...
|
||||
def pop_scope(...), do: # ...
|
||||
def generate_helper(...), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
# Parent module is the public API
|
||||
defmodule MyApp.Router do
|
||||
# Public API delegates to focused submodules
|
||||
def compile(routes), do: MyApp.Router.Compiler.compile(routes)
|
||||
end
|
||||
|
||||
# Submodules handle implementation
|
||||
defmodule MyApp.Router.Compiler do
|
||||
@moduledoc false
|
||||
# ...
|
||||
end
|
||||
|
||||
defmodule MyApp.Router.Scope do
|
||||
@moduledoc false
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The module is small and cohesive (< 200 lines)
|
||||
- Nesting would exceed 3 levels (`A.B.C.D` is usually too deep)
|
||||
- The "submodule" has its own independent public API (make it a sibling instead)
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Over-nesting a simple utility
|
||||
defmodule MyApp.Utils.String.Formatting.Case do
|
||||
def upcase(s), do: String.upcase(s)
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmodule MyApp.StringUtils do
|
||||
def upcase(s), do: String.upcase(s)
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Nesting should reflect genuine conceptual hierarchy. If you're creating submodules for 2-3 functions that don't have independent complexity, you're adding navigational overhead without architectural benefit.''',
|
||||
|
||||
2: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- You're writing a new module and need to decide function ordering
|
||||
- A module has grown organically and functions are scattered randomly
|
||||
- You're reviewing code and finding it hard to locate the public API
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmodule UserService do
|
||||
defp hash_password(pw), do: # ...
|
||||
|
||||
def create(attrs) do
|
||||
# uses hash_password
|
||||
end
|
||||
|
||||
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
|
||||
|
||||
defp validate(attrs), do: # ...
|
||||
|
||||
def get(id), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmodule UserService do
|
||||
# Lifecycle
|
||||
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
|
||||
|
||||
# Public API
|
||||
def create(attrs), do: # ...
|
||||
def get(id), do: # ...
|
||||
|
||||
# Private helpers
|
||||
defp validate(attrs), do: # ...
|
||||
defp hash_password(pw), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- You have a tiny module (< 5 functions) where ordering doesn't matter much
|
||||
- The module is a pure data module (just a struct + typespec)
|
||||
- "Logical grouping" puts closely related public+private pairs together for readability
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Forcing start_link to the top in a module that isn't an OTP process
|
||||
defmodule MyApp.Parser do
|
||||
# This module has no lifecycle — don't force OTP ordering
|
||||
def start_link(_), do: raise "not a process" # Just to match the pattern?
|
||||
def parse(input), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmodule MyApp.Parser do
|
||||
@moduledoc "Parses input format X into structs"
|
||||
|
||||
def parse(input), do: # ...
|
||||
def parse!(input), do: # ...
|
||||
|
||||
defp tokenize(input), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The ordering convention exists to make OTP-aware modules predictable. For non-OTP modules, lead with the primary public function (the one callers reach for first) and let the rest follow logically.''',
|
||||
|
||||
3: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- A module exists purely for internal code organization
|
||||
- Users of your library should never call this module directly
|
||||
- The module is a helper that could change or disappear between versions
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmodule MyApp.Repo.QueryBuilder do
|
||||
@moduledoc """
|
||||
Builds Ecto queries for the Repo module.
|
||||
"""
|
||||
# Now appears in docs, users try to call it directly
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmodule MyApp.Repo.QueryBuilder do
|
||||
@moduledoc false
|
||||
# Hidden from docs, clearly internal
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The module is part of your public API (even if rarely used)
|
||||
- Users need to implement callbacks or extend the module
|
||||
- The module defines a behaviour or protocol that others implement
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Hiding a module that users actually need
|
||||
defmodule MyApp.Errors do
|
||||
@moduledoc false # But users need to pattern-match on these!
|
||||
|
||||
defmodule NotFound do
|
||||
defexception [:message]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmodule MyApp.Errors do
|
||||
@moduledoc "Error types raised by MyApp operations."
|
||||
|
||||
defmodule NotFound do
|
||||
@moduledoc "Raised when a resource cannot be found."
|
||||
defexception [:message]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `@moduledoc false` means "this is not for you." If users catch your exceptions or match on your structs, they need documentation. Hide implementation details, not public contracts.''',
|
||||
|
||||
4: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Your struct has fields that make no sense as `nil` (creating one without them is a bug)
|
||||
- You're modeling a value object where all fields define its identity
|
||||
- Incomplete structs would cause confusing runtime errors later
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmodule Order do
|
||||
defstruct [:id, :customer_id, :items, :total]
|
||||
# Can create %Order{} with everything nil — meaningless
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmodule Order do
|
||||
@enforce_keys [:customer_id, :items, :total]
|
||||
defstruct [:id | @enforce_keys]
|
||||
# %Order{} without required fields → immediate compile/runtime error
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The struct is built incrementally (e.g., a changeset or builder pattern)
|
||||
- Most fields have sensible defaults
|
||||
- The struct represents configuration where partial specs are valid
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Enforcing keys on a struct that's built in stages
|
||||
defmodule FormState do
|
||||
@enforce_keys [:step, :name, :email, :address, :payment]
|
||||
defstruct @enforce_keys
|
||||
# Can't create a partial form state for step 1!
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmodule FormState do
|
||||
defstruct step: 1, name: nil, email: nil, address: nil, payment: nil
|
||||
# Built incrementally as user progresses through steps
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `@enforce_keys` is for structs that represent *complete* values. If your struct represents an evolving state or has legitimate intermediate forms, enforcing all keys makes construction impossible at early stages.''',
|
||||
|
||||
5: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Your `use` macro needs to give the caller access to specific functions
|
||||
- You want to control exactly which functions enter the caller's namespace
|
||||
- The imported functions are central to the DSL or workflow the module enables
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
# Imports EVERYTHING from three modules — namespace soup
|
||||
import MyApp.Router.Helpers
|
||||
import MyApp.Router.Scoping
|
||||
import MyApp.Router.Compilation
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
import MyApp.Router, only: [get: 2, post: 2, resources: 2, scope: 2]
|
||||
import MyApp.Conn, only: [assign: 3, put_status: 2]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The caller could just `import` what they need themselves
|
||||
- You're importing utility functions that aren't part of your module's "DSL"
|
||||
- The imports create naming conflicts with common functions
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
import MyApp.Utils # 50+ utility functions dumped into caller
|
||||
import Enum # Why? Caller can do this themselves
|
||||
import Map # Polluting namespace with standard lib
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
# Only import what THIS module's workflow requires
|
||||
import MyApp.DSL, only: [field: 2, validate: 1]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `use` should import the *minimum* needed for the module's intended workflow. If you're importing generic utilities, you're making decisions for the caller that they should make themselves.''',
|
||||
|
||||
6: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Multiple modules from the same parent namespace are used together
|
||||
- Full module paths are making code hard to read
|
||||
- The aliased modules are used frequently (3+ times in the file)
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def process(input) do
|
||||
Phoenix.Router.Route.new(input)
|
||||
|> Phoenix.Router.Scope.apply_scope(Phoenix.Router.Scope.current())
|
||||
|> Phoenix.Router.Helpers.generate()
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
alias Phoenix.Router.{Route, Scope, Helpers}
|
||||
|
||||
def process(input) do
|
||||
Route.new(input)
|
||||
|> Scope.apply_scope(Scope.current())
|
||||
|> Helpers.generate()
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- A module is referenced only once (inline the full path)
|
||||
- The alias would be ambiguous (two `Route` modules from different namespaces)
|
||||
- You're in a test file and the full path makes assertions clearer
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Aliasing a module used exactly once
|
||||
alias MyApp.Workers.BatchProcessor
|
||||
|
||||
def run do
|
||||
BatchProcessor.start() # Only reference — alias adds noise
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
def run do
|
||||
MyApp.Workers.BatchProcessor.start() # One use — full path is fine
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Aliases trade verbosity for indirection. When a module appears once, the full path is documentation. When it appears many times, the alias is readability. Find the crossover point (typically 2-3 uses).''',
|
||||
|
||||
7: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- A struct field stores a boolean value
|
||||
- The field answers a yes/no question about the struct
|
||||
- You want the field's type to be self-evident without checking typespecs
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defstruct [:path, :trailing_slash, :verified]
|
||||
# Is :trailing_slash the slash character? A boolean? The position?
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defstruct [:path, :trailing_slash?, :verified?]
|
||||
# Immediately clear these are booleans
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The field isn't a boolean (e.g., `:status` that can be `:active`/`:inactive`)
|
||||
- You're working with external serialization that can't handle `?` in keys
|
||||
- The field represents a count, enum, or value rather than a yes/no question
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
defstruct [:user?, :admin?, :count?]
|
||||
# :user? — is this "is user present?" or "the user value"?
|
||||
# :count? — definitely not a boolean
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defstruct [:user, :admin?, :count]
|
||||
# :user is the user struct, :admin? is a boolean, :count is an integer
|
||||
```
|
||||
|
||||
**Why:** The `?` suffix should only mark genuine booleans. Using it on non-boolean fields creates confusion about the field's type and breaks the convention's usefulness as a type signal.''',
|
||||
}
|
||||
|
||||
for i in range(len(parts)):
|
||||
if i in when_sections:
|
||||
parts[i] = parts[i].rstrip() + "\n\n" + when_sections[i].strip()
|
||||
|
||||
output = separator.join(parts) + "\n"
|
||||
|
||||
with open("patterns/modules.md", "w") as f:
|
||||
f.write(output)
|
||||
|
||||
print(f"Done! {len(output)} chars")
|
||||
@@ -0,0 +1,432 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Add When to Use / When NOT to Use sections to modules.md
|
||||
Part 0 contains preamble + section 1. Parts 1-6 are sections 2-7."""
|
||||
|
||||
with open("patterns/modules.md", "r") as f:
|
||||
content = f.read()
|
||||
|
||||
separator = "\n\n---\n\n"
|
||||
parts = content.split(separator)
|
||||
assert len(parts) == 7, f"Expected 7 parts, got {len(parts)}"
|
||||
|
||||
# Map: part index -> when section content
|
||||
# Part 0 = preamble + section 1
|
||||
# Part 1 = section 2
|
||||
# ...
|
||||
# Part 6 = section 7
|
||||
|
||||
when_sections = {
|
||||
0: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Your module has grown beyond ~300 lines with distinct sub-responsibilities
|
||||
- External code only needs the parent module but implementation is complex
|
||||
- You find yourself prefixing private functions with a concept name (e.g., `scope_push`, `scope_pop`)
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
# Everything crammed into one flat module
|
||||
defmodule MyApp.Router do
|
||||
# 800 lines mixing route compilation, scope tracking, and helper generation
|
||||
def compile_route(...), do: # ...
|
||||
def push_scope(...), do: # ...
|
||||
def pop_scope(...), do: # ...
|
||||
def generate_helper(...), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
# Parent module is the public API
|
||||
defmodule MyApp.Router do
|
||||
# Public API delegates to focused submodules
|
||||
def compile(routes), do: MyApp.Router.Compiler.compile(routes)
|
||||
end
|
||||
|
||||
# Submodules handle implementation
|
||||
defmodule MyApp.Router.Compiler do
|
||||
@moduledoc false
|
||||
# ...
|
||||
end
|
||||
|
||||
defmodule MyApp.Router.Scope do
|
||||
@moduledoc false
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The module is small and cohesive (< 200 lines)
|
||||
- Nesting would exceed 3 levels (`A.B.C.D` is usually too deep)
|
||||
- The "submodule" has its own independent public API (make it a sibling instead)
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Over-nesting a simple utility
|
||||
defmodule MyApp.Utils.String.Formatting.Case do
|
||||
def upcase(s), do: String.upcase(s)
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmodule MyApp.StringUtils do
|
||||
def upcase(s), do: String.upcase(s)
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Nesting should reflect genuine conceptual hierarchy. If you're creating submodules for 2-3 functions that don't have independent complexity, you're adding navigational overhead without architectural benefit.''',
|
||||
|
||||
1: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- You're writing a new module and need to decide function ordering
|
||||
- A module has grown organically and functions are scattered randomly
|
||||
- You're reviewing code and finding it hard to locate the public API
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmodule UserService do
|
||||
defp hash_password(pw), do: # ...
|
||||
|
||||
def create(attrs) do
|
||||
# uses hash_password
|
||||
end
|
||||
|
||||
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
|
||||
|
||||
defp validate(attrs), do: # ...
|
||||
|
||||
def get(id), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmodule UserService do
|
||||
# Lifecycle
|
||||
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
|
||||
|
||||
# Public API
|
||||
def create(attrs), do: # ...
|
||||
def get(id), do: # ...
|
||||
|
||||
# Private helpers
|
||||
defp validate(attrs), do: # ...
|
||||
defp hash_password(pw), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- You have a tiny module (< 5 functions) where ordering doesn't matter much
|
||||
- The module is a pure data module (just a struct + typespec)
|
||||
- "Logical grouping" puts closely related public+private pairs together for readability
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Forcing start_link to the top in a module that isn't an OTP process
|
||||
defmodule MyApp.Parser do
|
||||
# This module has no lifecycle — don't force OTP ordering
|
||||
def start_link(_), do: raise "not a process" # Just to match the pattern?
|
||||
def parse(input), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmodule MyApp.Parser do
|
||||
@moduledoc "Parses input format X into structs"
|
||||
|
||||
def parse(input), do: # ...
|
||||
def parse!(input), do: # ...
|
||||
|
||||
defp tokenize(input), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The ordering convention exists to make OTP-aware modules predictable. For non-OTP modules, lead with the primary public function (the one callers reach for first) and let the rest follow logically.''',
|
||||
|
||||
2: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- A module exists purely for internal code organization
|
||||
- Users of your library should never call this module directly
|
||||
- The module is a helper that could change or disappear between versions
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmodule MyApp.Repo.QueryBuilder do
|
||||
@moduledoc """
|
||||
Builds Ecto queries for the Repo module.
|
||||
"""
|
||||
# Now appears in docs, users try to call it directly
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmodule MyApp.Repo.QueryBuilder do
|
||||
@moduledoc false
|
||||
# Hidden from docs, clearly internal
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The module is part of your public API (even if rarely used)
|
||||
- Users need to implement callbacks or extend the module
|
||||
- The module defines a behaviour or protocol that others implement
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Hiding a module that users actually need
|
||||
defmodule MyApp.Errors do
|
||||
@moduledoc false # But users need to pattern-match on these!
|
||||
|
||||
defmodule NotFound do
|
||||
defexception [:message]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmodule MyApp.Errors do
|
||||
@moduledoc "Error types raised by MyApp operations."
|
||||
|
||||
defmodule NotFound do
|
||||
@moduledoc "Raised when a resource cannot be found."
|
||||
defexception [:message]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `@moduledoc false` means "this is not for you." If users catch your exceptions or match on your structs, they need documentation. Hide implementation details, not public contracts.''',
|
||||
|
||||
3: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Your struct has fields that make no sense as `nil` (creating one without them is a bug)
|
||||
- You're modeling a value object where all fields define its identity
|
||||
- Incomplete structs would cause confusing runtime errors later
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmodule Order do
|
||||
defstruct [:id, :customer_id, :items, :total]
|
||||
# Can create %Order{} with everything nil — meaningless
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmodule Order do
|
||||
@enforce_keys [:customer_id, :items, :total]
|
||||
defstruct [:id | @enforce_keys]
|
||||
# %Order{} without required fields -> immediate compile/runtime error
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The struct is built incrementally (e.g., a changeset or builder pattern)
|
||||
- Most fields have sensible defaults
|
||||
- The struct represents configuration where partial specs are valid
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Enforcing keys on a struct that's built in stages
|
||||
defmodule FormState do
|
||||
@enforce_keys [:step, :name, :email, :address, :payment]
|
||||
defstruct @enforce_keys
|
||||
# Can't create a partial form state for step 1!
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmodule FormState do
|
||||
defstruct step: 1, name: nil, email: nil, address: nil, payment: nil
|
||||
# Built incrementally as user progresses through steps
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `@enforce_keys` is for structs that represent *complete* values. If your struct represents an evolving state or has legitimate intermediate forms, enforcing all keys makes construction impossible at early stages.''',
|
||||
|
||||
4: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Your `use` macro needs to give the caller access to specific functions
|
||||
- You want to control exactly which functions enter the caller's namespace
|
||||
- The imported functions are central to the DSL or workflow the module enables
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
# Imports EVERYTHING from three modules — namespace soup
|
||||
import MyApp.Router.Helpers
|
||||
import MyApp.Router.Scoping
|
||||
import MyApp.Router.Compilation
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
import MyApp.Router, only: [get: 2, post: 2, resources: 2, scope: 2]
|
||||
import MyApp.Conn, only: [assign: 3, put_status: 2]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The caller could just `import` what they need themselves
|
||||
- You're importing utility functions that aren't part of your module's "DSL"
|
||||
- The imports create naming conflicts with common functions
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
import MyApp.Utils # 50+ utility functions dumped into caller
|
||||
import Enum # Why? Caller can do this themselves
|
||||
import Map # Polluting namespace with standard lib
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
# Only import what THIS module's workflow requires
|
||||
import MyApp.DSL, only: [field: 2, validate: 1]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `use` should import the *minimum* needed for the module's intended workflow. If you're importing generic utilities, you're making decisions for the caller that they should make themselves.''',
|
||||
|
||||
5: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Multiple modules from the same parent namespace are used together
|
||||
- Full module paths are making code hard to read
|
||||
- The aliased modules are used frequently (3+ times in the file)
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def process(input) do
|
||||
Phoenix.Router.Route.new(input)
|
||||
|> Phoenix.Router.Scope.apply_scope(Phoenix.Router.Scope.current())
|
||||
|> Phoenix.Router.Helpers.generate()
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
alias Phoenix.Router.{Route, Scope, Helpers}
|
||||
|
||||
def process(input) do
|
||||
Route.new(input)
|
||||
|> Scope.apply_scope(Scope.current())
|
||||
|> Helpers.generate()
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- A module is referenced only once (inline the full path)
|
||||
- The alias would be ambiguous (two `Route` modules from different namespaces)
|
||||
- You're in a test file and the full path makes assertions clearer
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Aliasing a module used exactly once
|
||||
alias MyApp.Workers.BatchProcessor
|
||||
|
||||
def run do
|
||||
BatchProcessor.start() # Only reference — alias adds noise
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
def run do
|
||||
MyApp.Workers.BatchProcessor.start() # One use — full path is fine
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Aliases trade verbosity for indirection. When a module appears once, the full path is documentation. When it appears many times, the alias is readability. Find the crossover point (typically 2-3 uses).''',
|
||||
|
||||
6: '''
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- A struct field stores a boolean value
|
||||
- The field answers a yes/no question about the struct
|
||||
- You want the field's type to be self-evident without checking typespecs
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defstruct [:path, :trailing_slash, :verified]
|
||||
# Is :trailing_slash the slash character? A boolean? The position?
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defstruct [:path, :trailing_slash?, :verified?]
|
||||
# Immediately clear these are booleans
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The field isn't a boolean (e.g., `:status` that can be `:active`/`:inactive`)
|
||||
- You're working with external serialization that can't handle `?` in keys
|
||||
- The field represents a count, enum, or value rather than a yes/no question
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
defstruct [:user?, :admin?, :count?]
|
||||
# :user? — is this "is user present?" or "the user value"?
|
||||
# :count? — definitely not a boolean
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defstruct [:user, :admin?, :count]
|
||||
# :user is the user struct, :admin? is a boolean, :count is an integer
|
||||
```
|
||||
|
||||
**Why:** The `?` suffix should only mark genuine booleans. Using it on non-boolean fields creates confusion about the field's type and breaks the convention's usefulness as a type signal.''',
|
||||
}
|
||||
|
||||
for i in range(len(parts)):
|
||||
if i in when_sections:
|
||||
parts[i] = parts[i].rstrip() + "\n\n" + when_sections[i].strip()
|
||||
|
||||
output = separator.join(parts) + "\n"
|
||||
|
||||
with open("patterns/modules.md", "w") as f:
|
||||
f.write(output)
|
||||
|
||||
print(f"Done! {len(output)} chars")
|
||||
@@ -36,6 +36,51 @@ def map(enumerable, fun) do
|
||||
end
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:** You have a public function that accepts "any enumerable" but lists account for the majority of callers. Profiling shows protocol dispatch is a measurable cost. You can call an Erlang BIF or a direct recursive implementation for the list case.
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def sum(enumerable) do
|
||||
Enumerable.reduce(enumerable, {:cont, 0}, fn x, acc -> {:cont, acc + x} end)
|
||||
|> elem(1)
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
def sum(enumerable) when is_list(enumerable) do
|
||||
:lists.foldl(fn x, acc -> acc + x end, 0, enumerable)
|
||||
end
|
||||
|
||||
def sum(enumerable) do
|
||||
Enumerable.reduce(enumerable, {:cont, 0}, fn x, acc -> {:cont, acc + x} end)
|
||||
|> elem(1)
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:** The function is rarely called with lists, or the function body is complex enough that maintaining two implementations creates a bug risk. Also avoid when the protocol path is already fast enough (micro-optimization for non-hot paths).
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Pointless — this function is only ever called with streams
|
||||
def expensive_transform(enumerable) when is_list(enumerable) do
|
||||
# duplicate complex logic just in case a list shows up
|
||||
enumerable |> do_phase_1() |> do_phase_2() |> do_phase_3()
|
||||
end
|
||||
|
||||
def expensive_transform(enumerable) do
|
||||
enumerable |> do_phase_1() |> do_phase_2() |> do_phase_3()
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:** Keep one clause. Add the specialization only when profiling proves the protocol dispatch is a bottleneck for real workloads.
|
||||
|
||||
**Why:** Premature optimization. Two copies of the same logic means two places to fix bugs. The BEAM's protocol dispatch is already highly optimized — you need evidence before duplicating.
|
||||
|
||||
---
|
||||
|
||||
## 2. Build-Then-Reverse (Cons-Cell Accumulation)
|
||||
@@ -71,6 +116,55 @@ def map(enumerable, fun) do
|
||||
end
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:** You're building a result list element-by-element through recursion or reduce, and the output order must match the input order. The collection can be any size.
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def keep_positives(list) do
|
||||
Enum.reduce(list, [], fn x, acc ->
|
||||
if x > 0, do: acc ++ [x], else: acc
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
def keep_positives(list) do
|
||||
Enum.reduce(list, [], fn x, acc ->
|
||||
if x > 0, do: [x | acc], else: acc
|
||||
end)
|
||||
|> :lists.reverse()
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:** Order doesn't matter (e.g., building a set of unique items, collecting into a MapSet), or when you're only extracting a single value (sum, count, max). Also unnecessary if you're using `Enum.map/2` or `Enum.filter/2` directly — they already do this internally.
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Unnecessary — order doesn't matter for uniqueness
|
||||
def unique_tags(items) do
|
||||
Enum.reduce(items, [], fn item, acc ->
|
||||
if item.tag in acc, do: acc, else: [item.tag | acc]
|
||||
end)
|
||||
|> :lists.reverse() # why reverse if you're just checking membership?
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:** Use a MapSet or just don't reverse:
|
||||
```elixir
|
||||
def unique_tags(items) do
|
||||
Enum.reduce(items, MapSet.new(), fn item, acc ->
|
||||
MapSet.put(acc, item.tag)
|
||||
end)
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The reverse adds O(n) work and a full list traversal. If you don't care about order, skip it. If you're collecting into a non-list structure, this pattern doesn't apply.
|
||||
|
||||
---
|
||||
|
||||
## 3. Pipeline for Linear Transformations, Bare Calls for Control Flow
|
||||
@@ -112,6 +206,54 @@ result
|
||||
end
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:** Data flows through 2 or more transformations in sequence, each taking the previous result as its first argument. The reader should see a "conveyor belt" of operations.
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def format_names(users) do
|
||||
String.upcase(Enum.join(Enum.map(users, & &1.name), ", "))
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
def format_names(users) do
|
||||
users
|
||||
|> Enum.map(& &1.name)
|
||||
|> Enum.join(", ")
|
||||
|> String.upcase()
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:** There's only one transformation, the result needs to go into a pattern match (`case`/`with`), or the pipe would require anonymous function wrapping (`|> then(fn x -> ... end)`) to fit.
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Forced — then/1 wrapper defeats readability
|
||||
params
|
||||
|> Map.get(:user_id)
|
||||
|> then(fn id ->
|
||||
case Repo.get(User, id) do
|
||||
nil -> {:error, :not_found}
|
||||
user -> {:ok, user}
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
case Repo.get(User, Map.get(params, :user_id)) do
|
||||
nil -> {:error, :not_found}
|
||||
user -> {:ok, user}
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Pipes are for linear data flow. When you need branching (case/cond/with), break out of the pipeline. Forcing control flow through `then/1` adds indirection without clarity.
|
||||
|
||||
---
|
||||
|
||||
## 4. Pipeline Ending with `|> elem(1)` (Protocol Reduce Unwrap)
|
||||
@@ -146,6 +288,50 @@ case Enumerable.reduce(enumerable, {:cont, acc}, fun) do
|
||||
end
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:** You're calling `Enumerable.reduce/3` directly (implementing a custom Enum-like function) and you always want the accumulated value regardless of whether iteration completed or halted.
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def sum_until(enumerable, limit) do
|
||||
result = Enumerable.reduce(enumerable, {:cont, 0}, fn x, acc ->
|
||||
new = acc + x
|
||||
if new >= limit, do: {:halt, new}, else: {:cont, new}
|
||||
end)
|
||||
case result do
|
||||
{:done, val} -> val
|
||||
{:halted, val} -> val
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
def sum_until(enumerable, limit) do
|
||||
Enumerable.reduce(enumerable, {:cont, 0}, fn x, acc ->
|
||||
new = acc + x
|
||||
if new >= limit, do: {:halt, new}, else: {:cont, new}
|
||||
end)
|
||||
|> elem(1)
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:** You need to distinguish between `:done` and `:halted` to decide subsequent behavior (e.g., you want to know if iteration was interrupted). Also don't use in application code where you should be using `Enum.reduce/3` (which handles unwrapping for you).
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Pointless — Enum.reduce already unwraps
|
||||
Enum.reduce([1, 2, 3], 0, &(&1 + &2)) |> elem(1)
|
||||
# This crashes! Enum.reduce returns the value directly, not a tuple.
|
||||
```
|
||||
|
||||
**Better alternative:** Use `Enum.reduce/3` in application code. Only use the `|> elem(1)` pattern when directly calling `Enumerable.reduce/3` in library code.
|
||||
|
||||
**Why:** This pattern is for protocol implementers, not application developers. Using it on already-unwrapped results causes crashes. It's an internal idiom that shouldn't leak into regular code.
|
||||
|
||||
---
|
||||
|
||||
## 5. Private Helper Decomposition: Recursive Workers with Guards
|
||||
@@ -190,6 +376,61 @@ defp split_list(list, counter, acc) do
|
||||
end
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:** You're writing a recursive function that processes a list element-by-element with termination conditions (counter hits zero, list becomes empty, accumulator reaches a threshold). Multiple base cases exist.
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defp take_while_impl(list, fun, acc) do
|
||||
case list do
|
||||
[] -> :lists.reverse(acc)
|
||||
[head | tail] ->
|
||||
if fun.(head) do
|
||||
take_while_impl(tail, fun, [head | acc])
|
||||
else
|
||||
:lists.reverse(acc)
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defp take_while_impl([], _fun, acc) do
|
||||
:lists.reverse(acc)
|
||||
end
|
||||
|
||||
defp take_while_impl([head | tail], fun, acc) do
|
||||
if fun.(head) do
|
||||
take_while_impl(tail, fun, [head | acc])
|
||||
else
|
||||
:lists.reverse(acc)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:** The logic doesn't recurse (a simple one-shot transformation), or when `Enum` functions already express the operation clearly. Don't decompose for decomposition's sake.
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Over-engineered — this is just Enum.take/2
|
||||
defp my_take_list([], _n, acc), do: :lists.reverse(acc)
|
||||
defp my_take_list(_list, 0, acc), do: :lists.reverse(acc)
|
||||
defp my_take_list([h | t], n, acc), do: my_take_list(t, n - 1, [h | acc])
|
||||
|
||||
def my_take(list, n), do: my_take_list(list, n, [])
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
def my_take(list, n), do: Enum.take(list, n)
|
||||
```
|
||||
|
||||
**Why:** Elixir's standard library already provides optimized implementations of common list operations. Writing your own recursive versions adds maintenance burden and likely performs worse (Enum's list clauses call Erlang BIFs). Reserve this pattern for genuinely novel recursion.
|
||||
|
||||
---
|
||||
|
||||
## 6. Enum vs Stream Decision Pattern
|
||||
@@ -235,6 +476,54 @@ Use Stream when:
|
||||
[1, 2, 3] |> Enum.map(&(&1 * 2))
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:** You're chaining 3+ transformations on a large (or unbounded) collection. You're reading from a file/network where you want backpressure. You need `Stream.resource/3` for cleanup guarantees.
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
# Materializes 3 intermediate lists for a 1M-line file
|
||||
File.read!("large.csv")
|
||||
|> String.split("\n")
|
||||
|> Enum.map(&String.trim/1)
|
||||
|> Enum.filter(&(&1 != ""))
|
||||
|> Enum.map(&parse_row/1)
|
||||
|> Enum.take(100)
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
# Single pass, constant memory, stops after 100
|
||||
File.stream!("large.csv")
|
||||
|> Stream.map(&String.trim/1)
|
||||
|> Stream.reject(&(&1 == ""))
|
||||
|> Stream.map(&parse_row/1)
|
||||
|> Enum.take(100)
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:** The collection is small and bounded (under ~1000 elements), you only apply 1–2 transformations, or you need random access to the full result. Stream's lazy machinery has overhead that exceeds the savings for small data.
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Stream overhead exceeds any benefit for 5 items
|
||||
config = [:a, :b, :c, :d, :e]
|
||||
|
||||
config
|
||||
|> Stream.map(&Atom.to_string/1)
|
||||
|> Stream.map(&String.upcase/1)
|
||||
|> Enum.to_list()
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
config
|
||||
|> Enum.map(&(&1 |> Atom.to_string() |> String.upcase()))
|
||||
```
|
||||
|
||||
**Why:** Stream wraps each step in a closure and creates a lazy struct. For small collections, the allocation and indirection cost more than just building the intermediate list. The breakeven point is roughly when collections exceed hundreds of elements AND you chain 3+ operations.
|
||||
|
||||
---
|
||||
|
||||
## 7. Map.update vs Map.put Decision Pattern
|
||||
@@ -274,6 +563,42 @@ Map.put(map, :count, count + 1)
|
||||
Map.update(map, :count, 1, &(&1 + 1))
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:** The new value is computed FROM the old value — incrementing counters, appending to lists, toggling booleans, merging nested maps. You also need a sensible default for the "key doesn't exist yet" case.
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def add_tag(state, tag) do
|
||||
existing = Map.get(state, :tags, [])
|
||||
Map.put(state, :tags, [tag | existing])
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
def add_tag(state, tag) do
|
||||
Map.update(state, :tags, [tag], fn tags -> [tag | tags] end)
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:** The new value is independent of the old value (you're replacing, not transforming). Also avoid when you need to handle the "missing key" case differently from "present key" (use `Map.get_and_update/3` or explicit `case` instead).
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Awkward — the "update" function ignores the old value entirely
|
||||
Map.update(user, :name, new_name, fn _old -> new_name end)
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
Map.put(user, :name, new_name)
|
||||
```
|
||||
|
||||
**Why:** `Map.update/4` communicates "the new value depends on the old one." When you ignore the old value in the update function, you're lying to the reader. Use `put/3` for unconditional replacement — it's simpler and signals intent correctly.
|
||||
|
||||
---
|
||||
|
||||
## 8. Pattern Matching on Map Structure for Dispatch
|
||||
@@ -322,6 +647,55 @@ def get(map, key, default) do
|
||||
end
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:** You need to branch based on whether a key exists in a map, especially when you also want the value if it does exist. You want a single lookup that both checks existence and extracts the value.
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def fetch_config(config, key) do
|
||||
if Map.has_key?(config, key) do
|
||||
{:ok, Map.get(config, key)}
|
||||
else
|
||||
{:error, :missing}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
def fetch_config(config, key) do
|
||||
case config do
|
||||
%{^key => value} -> {:ok, value}
|
||||
%{} -> {:error, :missing}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:** You're checking for multiple keys simultaneously (use a multi-key pattern match instead), or when `Map.get/3` with a default already expresses what you need. Don't use `case` dispatch for simple "get with fallback" scenarios.
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Over-engineered — Map.get/3 already does this
|
||||
def get_name(user) do
|
||||
case user do
|
||||
%{:name => name} -> name
|
||||
%{} -> "Anonymous"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
def get_name(user) do
|
||||
Map.get(user, :name, "Anonymous")
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `Map.get/3` already implements this exact pattern internally. Rewriting it as an explicit `case` adds visual noise without any semantic or performance benefit. Use the case pattern when you're doing something `Map.get` can't — like returning different tagged tuples or triggering side effects.
|
||||
|
||||
---
|
||||
|
||||
## 9. Delegating to Erlang BIFs with `defdelegate`
|
||||
@@ -351,6 +725,49 @@ def keys(map) do
|
||||
end
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:** An Erlang module already exports a function with the exact semantics you need. The argument order matches. You want to expose it under an Elixir-idiomatic name or in your module's namespace for discoverability.
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmodule MyQueue do
|
||||
def new, do: :queue.new()
|
||||
def push(q, item), do: :queue.in(item, q)
|
||||
def pop(q), do: :queue.out(q)
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmodule MyQueue do
|
||||
defdelegate new(), to: :queue
|
||||
# Can't delegate push — argument order differs, needs wrapper
|
||||
def push(q, item), do: :queue.in(item, q)
|
||||
defdelegate pop(q), to: :queue, as: :out
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:** You need to validate inputs, transform arguments, change argument order, add defaults, or adapt the return value. Also avoid when the Erlang function has unclear semantics that benefit from a documenting wrapper.
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Broken — Erlang arg order is (key, map), Elixir convention is (map, key)
|
||||
defdelegate get(map, key), to: :maps
|
||||
# This compiles but has wrong argument order expectations!
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
def get(map, key) do
|
||||
:maps.get(key, map)
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `defdelegate` is a transparent pass-through. If argument order, defaults, validation, or error handling differ between your desired API and the Erlang function, you need a real wrapper. Delegating with a semantic mismatch creates subtle bugs.
|
||||
|
||||
---
|
||||
|
||||
## 10. Reduce as the Universal Primitive
|
||||
@@ -382,6 +799,60 @@ def filter(%MyStruct{items: items}, fun), do: ...
|
||||
# Instead: implement Enumerable.reduce/3 once and get everything
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:** You're implementing a custom data structure that should be iterable. You want the full `Enum` API without implementing each function. You're designing a protocol where one function provides maximum leverage.
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmodule RingBuffer do
|
||||
def map(%RingBuffer{} = rb, fun), do: ...
|
||||
def filter(%RingBuffer{} = rb, fun), do: ...
|
||||
def reduce(%RingBuffer{} = rb, acc, fun), do: ...
|
||||
def count(%RingBuffer{} = rb), do: ...
|
||||
# 70+ functions to implement...
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defimpl Enumerable, for: RingBuffer do
|
||||
def reduce(%RingBuffer{data: data, head: h, size: s}, acc, fun) do
|
||||
# One function — yields elements in order
|
||||
do_reduce(data, h, s, acc, fun)
|
||||
end
|
||||
|
||||
def count(%RingBuffer{size: s}), do: {:ok, s}
|
||||
def member?(_, _), do: {:error, __MODULE__}
|
||||
def slice(_), do: {:error, __MODULE__}
|
||||
end
|
||||
# Now Enum.map/filter/take/etc. all work automatically
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:** Your data structure has specialized algorithms that are significantly faster than the generic reduce-based approach (e.g., binary search on a sorted structure for `member?`). In that case, implement the specific protocol callbacks.
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Wasteful — reduce traverses all elements to count a structure with O(1) size
|
||||
defimpl Enumerable, for: SizedCollection do
|
||||
def count(_), do: {:error, __MODULE__}
|
||||
# This forces Enum.count to use reduce: O(n)
|
||||
# when the size is stored in a field: O(1)
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defimpl Enumerable, for: SizedCollection do
|
||||
def count(%{size: s}), do: {:ok, s}
|
||||
# Now Enum.count is O(1)
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The optimization callbacks (`count`, `member?`, `slice`) exist precisely because reduce is O(n) for operations that some structures can do faster. Use reduce as the universal fallback, but implement the fast paths when your structure supports them.
|
||||
|
||||
---
|
||||
|
||||
## 11. Keyword Multi-Clause Guard Dispatch (String.split pattern)
|
||||
@@ -426,6 +897,54 @@ def split(string, pattern, options) do
|
||||
end
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:** A function accepts multiple distinct input shapes (different types, specific sentinel values, structural patterns). Each shape requires substantially different handling. The shapes are distinguishable via guards or pattern matching.
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def parse(input, format) do
|
||||
cond do
|
||||
format == :json -> Jason.decode!(input)
|
||||
format == :yaml -> YamlElixir.read_from_string!(input)
|
||||
is_binary(format) -> custom_parse(input, format)
|
||||
true -> raise "unknown format"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
def parse(input, :json) when is_binary(input), do: Jason.decode!(input)
|
||||
def parse(input, :yaml) when is_binary(input), do: YamlElixir.read_from_string!(input)
|
||||
def parse(input, format) when is_binary(input) and is_binary(format), do: custom_parse(input, format)
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:** The differences between cases are minor (a single flag toggles a small behavior), or when you'd end up with 10+ nearly-identical clauses that differ by one line. Also avoid when the distinguishing condition can't be expressed in a guard (e.g., requires a database lookup).
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Absurd — 5 clauses that differ only in a multiplier
|
||||
def convert(value, :mm), do: value * 1.0
|
||||
def convert(value, :cm), do: value * 10.0
|
||||
def convert(value, :m), do: value * 1000.0
|
||||
def convert(value, :km), do: value * 1_000_000.0
|
||||
def convert(value, :in), do: value * 25.4
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
@multipliers %{mm: 1.0, cm: 10.0, m: 1000.0, km: 1_000_000.0, in: 25.4}
|
||||
|
||||
def convert(value, unit) when is_map_key(@multipliers, unit) do
|
||||
value * @multipliers[unit]
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** When clauses share identical structure and differ only by data, a lookup table is cleaner and more maintainable. Multi-clause dispatch shines when each case has genuinely different logic, not just different constants.
|
||||
|
||||
---
|
||||
|
||||
## 12. Lazy Private Helpers with `defp parts_to_index`
|
||||
@@ -446,3 +965,47 @@ defp parts_to_index(n) when is_integer(n) and n > 0, do: n
|
||||
# BAD — logic scattered in caller
|
||||
index = if parts == :infinity, do: 0, else: parts
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:** You have a small, well-defined mapping between API-level values and internal representations. The conversion appears in multiple places, or the mapping is non-obvious enough to deserve a name.
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def fetch(resource, timeout) do
|
||||
ms = if timeout == :infinity, do: 0, else: timeout * 1000
|
||||
do_fetch(resource, ms)
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
def fetch(resource, timeout) do
|
||||
do_fetch(resource, timeout_to_ms(timeout))
|
||||
end
|
||||
|
||||
defp timeout_to_ms(:infinity), do: :infinity
|
||||
defp timeout_to_ms(seconds) when is_number(seconds) and seconds >= 0, do: round(seconds * 1000)
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:** The conversion is trivial and only used once (a single `if` is clearer than a named function for `x + 1`), or when the mapping has many entries that would be better served by a lookup map.
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Over-engineered — named function for trivial identity-like conversion
|
||||
defp ensure_string(s) when is_binary(s), do: s
|
||||
defp ensure_string(a) when is_atom(a), do: Atom.to_string(a)
|
||||
|
||||
# Used exactly once:
|
||||
def log(msg), do: IO.puts(ensure_string(msg))
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
def log(msg) when is_binary(msg), do: IO.puts(msg)
|
||||
def log(msg) when is_atom(msg), do: IO.puts(Atom.to_string(msg))
|
||||
```
|
||||
|
||||
**Why:** When a conversion is used exactly once and the calling function already dispatches on clauses, folding the conversion into the caller's clauses reduces indirection. Named helpers shine when reused or when they name a non-obvious transformation.
|
||||
|
||||
@@ -1,415 +1 @@
|
||||
# GenServer Patterns — From the Elixir Source
|
||||
|
||||
Analysis of `lib/elixir/lib/gen_server.ex`, `lib/elixir/lib/agent.ex`, and related modules.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1: Client/Server API Separation
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:101-149` (documentation example)
|
||||
|
||||
**What it does:** Every GenServer module defines two distinct API layers — a **client API** (thin public functions that wrap `GenServer.call/cast`) and a **server API** (callback implementations). The client functions live in the same module but are clearly separated with comments.
|
||||
|
||||
**Why:** Encapsulation. Callers don't need to know the message protocol. The client API provides a typed, documented interface while the server callbacks handle the actual logic. This allows changing the internal message format without breaking callers.
|
||||
|
||||
**Anti-pattern:** Calling `GenServer.call(MyServer, :some_msg)` directly from other modules. This leaks the message protocol and couples callers to implementation details.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
defmodule Stack do
|
||||
use GenServer
|
||||
|
||||
# Client
|
||||
|
||||
def start_link(default) when is_binary(default) do
|
||||
GenServer.start_link(__MODULE__, default)
|
||||
end
|
||||
|
||||
def push(pid, element) do
|
||||
GenServer.cast(pid, {:push, element})
|
||||
end
|
||||
|
||||
def pop(pid) do
|
||||
GenServer.call(pid, :pop)
|
||||
end
|
||||
|
||||
# Server (callbacks)
|
||||
|
||||
@impl true
|
||||
def init(elements) do
|
||||
initial_state = String.split(elements, ",", trim: true)
|
||||
{:ok, initial_state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:pop, _from, state) do
|
||||
[to_caller | new_state] = state
|
||||
{:reply, to_caller, new_state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:push, element}, state) do
|
||||
new_state = [element | state]
|
||||
{:noreply, new_state}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: `@impl true` Annotations on All Callbacks
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:41-60` (Stack example)
|
||||
|
||||
**What it does:** Every callback function is annotated with `@impl true`. This tells the compiler to verify that the function is a valid callback for the declared behaviour.
|
||||
|
||||
**Why:** Catches typos and mismatches at compile time. If you accidentally name a callback `handle_calls` instead of `handle_call`, the compiler will warn you. It also serves as documentation for readers — you immediately know which functions are callbacks vs. helper functions.
|
||||
|
||||
**Anti-pattern:** Omitting `@impl true` on callbacks, especially when mixing callbacks with private helpers in the same module.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
@impl true
|
||||
def init(counter) do
|
||||
{:ok, counter}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:get, _from, counter) do
|
||||
{:reply, counter, counter}
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Guard-Protected `start_link`
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:101` and `lib/elixir/lib/agent.ex:28`
|
||||
|
||||
**What it does:** The `start_link` function uses guards to validate its arguments at the API boundary, failing fast with a clear error before any process spawning occurs.
|
||||
|
||||
**Why:** Catches invalid arguments immediately at the caller site, not deep inside `init/1` where the error would be harder to trace. This is the fail-fast principle applied at process boundaries.
|
||||
|
||||
**Anti-pattern:** Accepting any term in `start_link` and then crashing inside `init/1` with a confusing `FunctionClauseError` or `MatchError`.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
# From gen_server.ex documentation
|
||||
def start_link(default) when is_binary(default) do
|
||||
GenServer.start_link(__MODULE__, default)
|
||||
end
|
||||
|
||||
# From agent.ex:246
|
||||
@spec start_link((-> term), GenServer.options()) :: on_start
|
||||
def start_link(fun, options \\ []) when is_function(fun, 0) do
|
||||
GenServer.start_link(Agent.Server, fun, options)
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 4: `handle_continue` for Post-Init Work
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:520-528` (callback spec), `lib/elixir/lib/gen_server.ex:714-720` (handle_continue callback definition)
|
||||
|
||||
**What it does:** `init/1` can return `{:ok, state, {:continue, continue_arg}}`, which causes `handle_continue/2` to be invoked immediately after init completes. This allows splitting initialization into a synchronous part (that unblocks the supervisor) and an asynchronous continuation.
|
||||
|
||||
**Why:** `init/1` blocks the supervisor — if it does expensive work (DB connections, HTTP calls, cache warming), it delays the entire supervision tree startup. `handle_continue` lets you return quickly from `init/1` while still performing setup before handling any client messages. Unlike `send(self(), :continue)`, it's guaranteed to execute before any other messages in the mailbox.
|
||||
|
||||
**Anti-pattern:** Doing slow initialization directly in `init/1`, blocking the supervisor. Or using `send(self(), :init_continue)` which doesn't guarantee ordering — a client message could arrive first.
|
||||
|
||||
**Code example from source (callback spec):**
|
||||
```elixir
|
||||
@callback handle_continue(continue_arg, state :: term) ::
|
||||
{:noreply, new_state}
|
||||
| {:noreply, new_state, timeout | :hibernate | {:continue, continue_arg}}
|
||||
| {:stop, reason :: term, new_state}
|
||||
when new_state: term, continue_arg: term
|
||||
```
|
||||
|
||||
**Usage pattern:**
|
||||
```elixir
|
||||
def init(args) do
|
||||
# Quick setup only — return immediately to unblock supervisor
|
||||
{:ok, %{config: args, data: nil}, {:continue, :load_data}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_continue(:load_data, state) do
|
||||
# Expensive work happens here, guaranteed before any call/cast
|
||||
data = expensive_database_query()
|
||||
{:noreply, %{state | data: data}}
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 5: Timeout-Based Idle Shutdown
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:335-380`
|
||||
|
||||
**What it does:** Callbacks can return a timeout value (milliseconds) as the last element of the return tuple. If no message arrives within that time, `handle_info(:timeout, state)` is called. This enables idle process cleanup.
|
||||
|
||||
**Why:** Prevents resource leaks from idle processes. A process that's not being used can shut itself down gracefully, freeing memory and reducing the supervision tree. Useful for connection pools, caches, and session processes.
|
||||
|
||||
**Anti-pattern:** Using `Process.send_after` for idle detection — it doesn't reset on activity. The built-in timeout resets every time a message arrives, which is the correct semantic for "idle detection."
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
defmodule Counter do
|
||||
use GenServer
|
||||
|
||||
@timeout to_timeout(second: 5)
|
||||
|
||||
@impl true
|
||||
def init(count) do
|
||||
{:ok, count, @timeout}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:increment, _from, count) do
|
||||
new_count = count + 1
|
||||
{:reply, new_count, new_count, @timeout}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:timeout, count) do
|
||||
{:stop, :normal, count}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 6: Periodic Work via `Process.send_after`
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:298-332` (Periodically example in docs)
|
||||
|
||||
**What it does:** A GenServer schedules periodic work by sending itself a message via `Process.send_after` in `init/1`, then rescheduling in `handle_info`. This creates a self-sustaining periodic loop.
|
||||
|
||||
**Why:** Unlike timeouts (which reset on any message), `send_after` gives precise control over scheduling intervals. The pattern is self-healing — if the process crashes and restarts, `init` reschedules automatically. It's simpler than external timer libraries.
|
||||
|
||||
**Anti-pattern:** Using `:timer.send_interval` — if the GenServer is restarted by its supervisor, the timer from the old process keeps firing into the void (the timer is tied to the process that created it, but `send_interval` isn't linked to the destination). `send_after` in `handle_info` naturally dies with the process.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
defmodule MyApp.Periodically do
|
||||
use GenServer
|
||||
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, %{})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(state) do
|
||||
schedule_work()
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:work, state) do
|
||||
# Do the desired work here
|
||||
schedule_work()
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp schedule_work do
|
||||
Process.send_after(self(), :work, 2 * 60 * 60 * 1000)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 7: Call vs Cast Decision (Synchronous vs Asynchronous)
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:83-90` (docs), `lib/elixir/lib/agent.ex:368-378` (Agent.update uses call, Agent.cast uses cast)
|
||||
|
||||
**What it does:** The Elixir team uses `call` (synchronous) for operations where the client needs confirmation or a return value, and `cast` (fire-and-forget) only when the client genuinely doesn't care about the outcome.
|
||||
|
||||
**Why:** `cast` provides no backpressure — if a producer sends casts faster than the GenServer can process them, the mailbox grows unbounded until OOM. `call` naturally provides backpressure because the caller blocks. The Agent module makes this explicit: `Agent.update/3` uses `GenServer.call` (not cast!) because updates need confirmation of ordering.
|
||||
|
||||
**Anti-pattern:** Using `cast` for operations that should have backpressure, or using `cast` when you actually need to know if the operation succeeded. The Agent source proves this — even "fire and forget" `Agent.update` is a `call`:
|
||||
|
||||
**Code example from source (agent.ex):**
|
||||
```elixir
|
||||
# Agent.update uses call — NOT cast — for backpressure
|
||||
@spec update(agent, (state -> state), timeout) :: :ok
|
||||
def update(agent, fun, timeout \\ 5000) when is_function(fun, 1) do
|
||||
GenServer.call(agent, {:update, fun}, timeout)
|
||||
end
|
||||
|
||||
# Agent.cast is the explicit fire-and-forget variant
|
||||
@spec cast(agent, (state -> state)) :: :ok
|
||||
def cast(agent, fun) when is_function(fun, 1) do
|
||||
GenServer.cast(agent, {:cast, fun})
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 8: Default Callback Implementations with Clear Error Messages
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:902-993` (`__using__` macro)
|
||||
|
||||
**What it does:** `use GenServer` injects default implementations of `handle_call`, `handle_cast`, `handle_info`, `terminate`, and `code_change`. The defaults for `handle_call` and `handle_cast` raise with a descriptive error message including the process name. `handle_info` logs an error. All are `defoverridable`.
|
||||
|
||||
**Why:** This is defensive programming at the framework level. Instead of getting a cryptic `:function_clause` error from the Erlang runtime, developers get "attempted to call GenServer #{inspect(proc)} but no handle_call/3 clause was provided". The defaults also mean you only implement the callbacks you actually need.
|
||||
|
||||
**Anti-pattern:** Not implementing `handle_info` when your process might receive unexpected messages (monitors, nodedown, etc). The default logs a warning but doesn't crash — this is intentional.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
@doc false
|
||||
def handle_call(msg, _from, state) do
|
||||
proc =
|
||||
case Process.info(self(), :registered_name) do
|
||||
{_, []} -> self()
|
||||
{_, name} -> name
|
||||
end
|
||||
|
||||
case :erlang.phash2(1, 1) do
|
||||
0 ->
|
||||
raise "attempted to call GenServer #{inspect(proc)} but no handle_call/3 clause was provided"
|
||||
1 ->
|
||||
{:stop, {:bad_call, msg}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(msg, state) do
|
||||
proc =
|
||||
case Process.info(self(), :registered_name) do
|
||||
{_, []} -> self()
|
||||
{_, name} -> name
|
||||
end
|
||||
|
||||
:logger.error(...)
|
||||
{:noreply, state}
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 9: `child_spec/1` Generation and Customization via `use` Options
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:900-921`, `lib/elixir/lib/agent.ex:206-218`, `lib/elixir/lib/task.ex:282-292`
|
||||
|
||||
**What it does:** Each `use GenServer/Agent/Task` generates a `child_spec/1` function with sensible defaults that can be customized via options passed to `use`. The child spec is a map with `:id`, `:start`, `:restart`, `:shutdown`, and `:type`.
|
||||
|
||||
**Why:** Encapsulates supervision configuration within the module itself. Supervisors just need `{MyModule, arg}` — they don't need to know restart strategies or shutdown timeouts. Different modules have different defaults: GenServer/Agent default to `:permanent` restart, Task defaults to `:temporary`.
|
||||
|
||||
**Anti-pattern:** Defining child specs in the supervisor module rather than in the child module. This scatters configuration and makes modules non-portable between supervision trees.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
# GenServer child_spec — restart: :permanent (default)
|
||||
def child_spec(init_arg) do
|
||||
default = %{
|
||||
id: __MODULE__,
|
||||
start: {__MODULE__, :start_link, [init_arg]}
|
||||
}
|
||||
Supervisor.child_spec(default, unquote(Macro.escape(opts)))
|
||||
end
|
||||
|
||||
# Task child_spec — restart: :temporary (tasks don't restart)
|
||||
def child_spec(arg) do
|
||||
%{
|
||||
id: Task,
|
||||
start: {Task, :start_link, [arg]},
|
||||
restart: :temporary
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 10: Agent as Minimal State Wrapper (GenServer Under the Hood)
|
||||
|
||||
**Source:** `lib/elixir/lib/agent.ex:1-60` (module docs), `lib/elixir/lib/agent.ex:246-250` (implementation)
|
||||
|
||||
**What it does:** Agent is implemented entirely in terms of `GenServer.start_link(Agent.Server, fun, options)`. It's a thin abstraction that provides `get/update/get_and_update/cast` over GenServer's `call/cast`.
|
||||
|
||||
**Why:** When your process is purely about state (no complex message handling, no multi-step workflows), Agent removes boilerplate. It's an intentional design choice by the Elixir team: provide the simplest possible stateful process, built on the same foundation.
|
||||
|
||||
**Anti-pattern:** Using Agent for anything that needs custom message handling, multi-step coordination, or where you'd benefit from `handle_info` (timers, monitors). If you need more than get/update, use GenServer directly.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
# Agent is literally just GenServer with a purpose-built server module
|
||||
@spec start_link((-> term), GenServer.options()) :: on_start
|
||||
def start_link(fun, options \\ []) when is_function(fun, 0) do
|
||||
GenServer.start_link(Agent.Server, fun, options)
|
||||
end
|
||||
|
||||
# Client/server boundary in Agent: expensive work placement matters
|
||||
# Compute in the agent/server (blocks the agent for all other callers):
|
||||
def get_something(agent) do
|
||||
Agent.get(agent, fn state -> do_something_expensive(state) end)
|
||||
end
|
||||
|
||||
# Compute in the agent/client (copies state but doesn't block):
|
||||
def get_something(agent) do
|
||||
Agent.get(agent, & &1) |> do_something_expensive()
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 11: Name Registration via `:via` Tuple
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:1087-1107` (do_start implementation), `lib/elixir/lib/gen_server.ex:230-250` (documentation)
|
||||
|
||||
**What it does:** GenServer supports four naming schemes: `nil` (anonymous), atom (local), `{:global, term}` (cluster-wide), and `{:via, module, term}` (pluggable registry). The implementation delegates to `:gen.start` with the appropriate format.
|
||||
|
||||
**Why:** The `:via` pattern enables dynamic process naming without atom leaks. Since atoms are never garbage-collected, dynamic names (like per-user or per-session processes) must use `{:via, Registry, {registry, key}}` to avoid exhausting the atom table.
|
||||
|
||||
**Anti-pattern:** Using `String.to_atom("user_#{id}")` for dynamic process names. This creates atoms that are never GC'd and will eventually crash the VM.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
# From gen_server.ex do_start/4
|
||||
defp do_start(link, module, init_arg, options) do
|
||||
case Keyword.pop(options, :name) do
|
||||
{nil, opts} ->
|
||||
:gen.start(:gen_server, link, module, init_arg, opts)
|
||||
|
||||
{atom, opts} when is_atom(atom) ->
|
||||
:gen.start(:gen_server, link, {:local, atom}, module, init_arg, opts)
|
||||
|
||||
{{:global, _term} = tuple, opts} ->
|
||||
:gen.start(:gen_server, link, tuple, module, init_arg, opts)
|
||||
|
||||
{{:via, via_module, _term} = tuple, opts} when is_atom(via_module) ->
|
||||
:gen.start(:gen_server, link, tuple, module, init_arg, opts)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 12: GenServer as Anti-Pattern — Don't Use Processes for Code Organization
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:381-415` ("When (not) to use a GenServer" section)
|
||||
|
||||
**What it does:** The Elixir team explicitly documents that GenServer should NOT be used for code organization. A process must model a runtime property: mutable state, concurrency boundary, or failure isolation.
|
||||
|
||||
**Why:** A GenServer serializes all access through a single process. Using it for stateless computation (like a calculator) creates artificial bottlenecks. Processes have overhead (memory, scheduling, message copying). Functions are free.
|
||||
|
||||
**Anti-pattern (from the source itself):**
|
||||
```elixir
|
||||
# DON'T DO THIS — from gen_server.ex docs
|
||||
def add(a, b) do
|
||||
GenServer.call(__MODULE__, {:add, a, b})
|
||||
end
|
||||
|
||||
def handle_call({:add, a, b}, _from, state) do
|
||||
{:reply, a + b, state}
|
||||
end
|
||||
|
||||
# DO THIS instead:
|
||||
def add(a, b) do
|
||||
a + b
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
@@ -33,6 +33,69 @@ defmacro left and right do
|
||||
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
|
||||
@@ -68,6 +131,62 @@ defmodule Collatz do
|
||||
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
|
||||
@@ -101,6 +220,66 @@ defmacro defstruct(fields) do
|
||||
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
|
||||
@@ -133,6 +312,66 @@ defmacro var!({name, meta, atom}, context) when is_atom(name) and is_atom(atom)
|
||||
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
|
||||
@@ -164,6 +403,63 @@ defmacro raise(message) do
|
||||
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
|
||||
@@ -189,6 +485,68 @@ defmacro defmacro(call, expr \\ nil) do
|
||||
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)
|
||||
@@ -227,6 +585,68 @@ quote generated: true do
|
||||
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
|
||||
@@ -264,6 +684,69 @@ defimpl Inspect, for: Any do
|
||||
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
|
||||
@@ -308,6 +791,74 @@ defmodule GenServer do
|
||||
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)
|
||||
@@ -333,6 +884,52 @@ end
|
||||
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
|
||||
@@ -363,6 +960,58 @@ end
|
||||
# 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
|
||||
@@ -391,3 +1040,67 @@ Generates a list of `n` unique arguments.
|
||||
[{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.
|
||||
|
||||
@@ -33,6 +33,69 @@ endpoint/ — Phoenix.Endpoint.* submodules
|
||||
|
||||
**Anti-pattern:** Putting all modules in a flat directory, or nesting too deeply (more than 3 levels is usually a sign of over-engineering).
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Your module has grown beyond ~300 lines with distinct sub-responsibilities
|
||||
- External code only needs the parent module but implementation is complex
|
||||
- You find yourself prefixing private functions with a concept name (e.g., `scope_push`, `scope_pop`)
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
# Everything crammed into one flat module
|
||||
defmodule MyApp.Router do
|
||||
# 800 lines mixing route compilation, scope tracking, and helper generation
|
||||
def compile_route(...), do: # ...
|
||||
def push_scope(...), do: # ...
|
||||
def pop_scope(...), do: # ...
|
||||
def generate_helper(...), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
# Parent module is the public API
|
||||
defmodule MyApp.Router do
|
||||
# Public API delegates to focused submodules
|
||||
def compile(routes), do: MyApp.Router.Compiler.compile(routes)
|
||||
end
|
||||
|
||||
# Submodules handle implementation
|
||||
defmodule MyApp.Router.Compiler do
|
||||
@moduledoc false
|
||||
# ...
|
||||
end
|
||||
|
||||
defmodule MyApp.Router.Scope do
|
||||
@moduledoc false
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The module is small and cohesive (< 200 lines)
|
||||
- Nesting would exceed 3 levels (`A.B.C.D` is usually too deep)
|
||||
- The "submodule" has its own independent public API (make it a sibling instead)
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Over-nesting a simple utility
|
||||
defmodule MyApp.Utils.String.Formatting.Case do
|
||||
def upcase(s), do: String.upcase(s)
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmodule MyApp.StringUtils do
|
||||
def upcase(s), do: String.upcase(s)
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Nesting should reflect genuine conceptual hierarchy. If you're creating submodules for 2-3 functions that don't have independent complexity, you're adding navigational overhead without architectural benefit.
|
||||
|
||||
---
|
||||
|
||||
## 2. Public API at the Top, Private Functions at the Bottom
|
||||
@@ -76,6 +139,77 @@ The order:
|
||||
|
||||
**Anti-pattern:** Mixing private helpers between public functions, or putting `start_link` at the bottom where supervisors have to hunt for it.
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- You're writing a new module and need to decide function ordering
|
||||
- A module has grown organically and functions are scattered randomly
|
||||
- You're reviewing code and finding it hard to locate the public API
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmodule UserService do
|
||||
defp hash_password(pw), do: # ...
|
||||
|
||||
def create(attrs) do
|
||||
# uses hash_password
|
||||
end
|
||||
|
||||
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
|
||||
|
||||
defp validate(attrs), do: # ...
|
||||
|
||||
def get(id), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmodule UserService do
|
||||
# Lifecycle
|
||||
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
|
||||
|
||||
# Public API
|
||||
def create(attrs), do: # ...
|
||||
def get(id), do: # ...
|
||||
|
||||
# Private helpers
|
||||
defp validate(attrs), do: # ...
|
||||
defp hash_password(pw), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- You have a tiny module (< 5 functions) where ordering doesn't matter much
|
||||
- The module is a pure data module (just a struct + typespec)
|
||||
- "Logical grouping" puts closely related public+private pairs together for readability
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Forcing start_link to the top in a module that isn't an OTP process
|
||||
defmodule MyApp.Parser do
|
||||
# This module has no lifecycle — don't force OTP ordering
|
||||
def start_link(_), do: raise "not a process" # Just to match the pattern?
|
||||
def parse(input), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmodule MyApp.Parser do
|
||||
@moduledoc "Parses input format X into structs"
|
||||
|
||||
def parse(input), do: # ...
|
||||
def parse!(input), do: # ...
|
||||
|
||||
defp tokenize(input), do: # ...
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The ordering convention exists to make OTP-aware modules predictable. For non-OTP modules, lead with the primary public function (the one callers reach for first) and let the rest follow logically.
|
||||
|
||||
---
|
||||
|
||||
## 3. `@moduledoc false` for Internal Modules
|
||||
@@ -93,6 +227,64 @@ The order:
|
||||
|
||||
**Anti-pattern:** Documenting internal modules and confusing users about what's public API vs implementation detail.
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- A module exists purely for internal code organization
|
||||
- Users of your library should never call this module directly
|
||||
- The module is a helper that could change or disappear between versions
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmodule MyApp.Repo.QueryBuilder do
|
||||
@moduledoc """
|
||||
Builds Ecto queries for the Repo module.
|
||||
"""
|
||||
# Now appears in docs, users try to call it directly
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmodule MyApp.Repo.QueryBuilder do
|
||||
@moduledoc false
|
||||
# Hidden from docs, clearly internal
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The module is part of your public API (even if rarely used)
|
||||
- Users need to implement callbacks or extend the module
|
||||
- The module defines a behaviour or protocol that others implement
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Hiding a module that users actually need
|
||||
defmodule MyApp.Errors do
|
||||
@moduledoc false # But users need to pattern-match on these!
|
||||
|
||||
defmodule NotFound do
|
||||
defexception [:message]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmodule MyApp.Errors do
|
||||
@moduledoc "Error types raised by MyApp operations."
|
||||
|
||||
defmodule NotFound do
|
||||
@moduledoc "Raised when a resource cannot be found."
|
||||
defexception [:message]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `@moduledoc false` means "this is not for you." If users catch your exceptions or match on your structs, they need documentation. Hide implementation details, not public contracts.
|
||||
|
||||
---
|
||||
|
||||
## 4. Struct Definition Conventions
|
||||
@@ -142,6 +334,57 @@ Two patterns:
|
||||
|
||||
**Anti-pattern:** Never using `@enforce_keys` — allows creating invalid structs that crash later when a required field is `nil`.
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Your struct has fields that make no sense as `nil` (creating one without them is a bug)
|
||||
- You're modeling a value object where all fields define its identity
|
||||
- Incomplete structs would cause confusing runtime errors later
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmodule Order do
|
||||
defstruct [:id, :customer_id, :items, :total]
|
||||
# Can create %Order{} with everything nil — meaningless
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmodule Order do
|
||||
@enforce_keys [:customer_id, :items, :total]
|
||||
defstruct [:id | @enforce_keys]
|
||||
# %Order{} without required fields -> immediate compile/runtime error
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The struct is built incrementally (e.g., a changeset or builder pattern)
|
||||
- Most fields have sensible defaults
|
||||
- The struct represents configuration where partial specs are valid
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Enforcing keys on a struct that's built in stages
|
||||
defmodule FormState do
|
||||
@enforce_keys [:step, :name, :email, :address, :payment]
|
||||
defstruct @enforce_keys
|
||||
# Can't create a partial form state for step 1!
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmodule FormState do
|
||||
defstruct step: 1, name: nil, email: nil, address: nil, payment: nil
|
||||
# Built incrementally as user progresses through steps
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `@enforce_keys` is for structs that represent *complete* values. If your struct represents an evolving state or has legitimate intermediate forms, enforcing all keys makes construction impossible at early stages.
|
||||
|
||||
---
|
||||
|
||||
## 5. Selective Imports in `__using__`
|
||||
@@ -165,6 +408,65 @@ import Phoenix.Controller
|
||||
|
||||
**Anti-pattern:** Importing everything without restriction — namespace pollution and hard-to-trace function origins.
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Your `use` macro needs to give the caller access to specific functions
|
||||
- You want to control exactly which functions enter the caller's namespace
|
||||
- The imported functions are central to the DSL or workflow the module enables
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
# Imports EVERYTHING from three modules — namespace soup
|
||||
import MyApp.Router.Helpers
|
||||
import MyApp.Router.Scoping
|
||||
import MyApp.Router.Compilation
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
import MyApp.Router, only: [get: 2, post: 2, resources: 2, scope: 2]
|
||||
import MyApp.Conn, only: [assign: 3, put_status: 2]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The caller could just `import` what they need themselves
|
||||
- You're importing utility functions that aren't part of your module's "DSL"
|
||||
- The imports create naming conflicts with common functions
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
import MyApp.Utils # 50+ utility functions dumped into caller
|
||||
import Enum # Why? Caller can do this themselves
|
||||
import Map # Polluting namespace with standard lib
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
# Only import what THIS module's workflow requires
|
||||
import MyApp.DSL, only: [field: 2, validate: 1]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `use` should import the *minimum* needed for the module's intended workflow. If you're importing generic utilities, you're making decisions for the caller that they should make themselves.
|
||||
|
||||
---
|
||||
|
||||
## 6. Alias at Module Scope for Readability
|
||||
@@ -179,6 +481,59 @@ alias Phoenix.Router.{Resource, Scope, Route, Helpers}
|
||||
|
||||
**Anti-pattern:** Using full module paths everywhere (`Phoenix.Router.Resource.new(...)`) — verbose and hard to read.
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Multiple modules from the same parent namespace are used together
|
||||
- Full module paths are making code hard to read
|
||||
- The aliased modules are used frequently (3+ times in the file)
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
def process(input) do
|
||||
Phoenix.Router.Route.new(input)
|
||||
|> Phoenix.Router.Scope.apply_scope(Phoenix.Router.Scope.current())
|
||||
|> Phoenix.Router.Helpers.generate()
|
||||
end
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
alias Phoenix.Router.{Route, Scope, Helpers}
|
||||
|
||||
def process(input) do
|
||||
Route.new(input)
|
||||
|> Scope.apply_scope(Scope.current())
|
||||
|> Helpers.generate()
|
||||
end
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- A module is referenced only once (inline the full path)
|
||||
- The alias would be ambiguous (two `Route` modules from different namespaces)
|
||||
- You're in a test file and the full path makes assertions clearer
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
# Aliasing a module used exactly once
|
||||
alias MyApp.Workers.BatchProcessor
|
||||
|
||||
def run do
|
||||
BatchProcessor.start() # Only reference — alias adds noise
|
||||
end
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
def run do
|
||||
MyApp.Workers.BatchProcessor.start() # One use — full path is fine
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Aliases trade verbosity for indirection. When a module appears once, the full path is documentation. When it appears many times, the alias is readability. Find the crossover point (typically 2-3 uses).
|
||||
|
||||
---
|
||||
|
||||
## 7. Boolean-Suffixed Fields in Structs
|
||||
@@ -193,3 +548,44 @@ alias Phoenix.Router.{Resource, Scope, Route, Helpers}
|
||||
**Why:** The `?` suffix on struct fields mirrors the Elixir convention for boolean-returning functions. It makes the field's type obvious without checking the typespec.
|
||||
|
||||
**Anti-pattern:** Using bare names like `:trailing_slash` or `:has_trailing_slash` — the `?` convention is more idiomatic and self-documenting.
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- A struct field stores a boolean value
|
||||
- The field answers a yes/no question about the struct
|
||||
- You want the field's type to be self-evident without checking typespecs
|
||||
|
||||
**Example — before:**
|
||||
```elixir
|
||||
defstruct [:path, :trailing_slash, :verified]
|
||||
# Is :trailing_slash the slash character? A boolean? The position?
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```elixir
|
||||
defstruct [:path, :trailing_slash?, :verified?]
|
||||
# Immediately clear these are booleans
|
||||
```
|
||||
|
||||
### When NOT to Use
|
||||
|
||||
**Don't use this when:**
|
||||
- The field isn't a boolean (e.g., `:status` that can be `:active`/`:inactive`)
|
||||
- You're working with external serialization that can't handle `?` in keys
|
||||
- The field represents a count, enum, or value rather than a yes/no question
|
||||
|
||||
**Over-application example:**
|
||||
```elixir
|
||||
defstruct [:user?, :admin?, :count?]
|
||||
# :user? — is this "is user present?" or "the user value"?
|
||||
# :count? — definitely not a boolean
|
||||
```
|
||||
|
||||
**Better alternative:**
|
||||
```elixir
|
||||
defstruct [:user, :admin?, :count]
|
||||
# :user is the user struct, :admin? is a boolean, :count is an integer
|
||||
```
|
||||
|
||||
**Why:** The `?` suffix should only mark genuine booleans. Using it on non-boolean fields creates confusion about the field's type and breaks the convention's usefulness as a type signal.
|
||||
|
||||
Reference in New Issue
Block a user