docs: idiomatic Elixir and Phoenix patterns with source citations
Extracted patterns, conventions, and code smells directly from the Elixir and Phoenix source code with file path and line number citations. Covers: GenServer, error handling, data transforms, process design, testing, documentation, typespecs, macros, behaviours, module organization, Phoenix-specific patterns, framework deviations, and anti-patterns.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# Idiomatic Elixir & Phoenix Patterns
|
||||
|
||||
Patterns, conventions, and code smells extracted directly from the Elixir and Phoenix source code — with citations to specific files and line numbers.
|
||||
|
||||
This is not opinion. This is what the source actually does.
|
||||
|
||||
## Source Versions
|
||||
|
||||
- **Elixir:** `main` branch (cloned 2026-04-29)
|
||||
- **Phoenix:** `main` branch (cloned 2026-04-29)
|
||||
|
||||
## Structure
|
||||
|
||||
### Core Patterns (`patterns/`)
|
||||
- [GenServer Patterns](patterns/genserver.md) — Client/server separation, callbacks, state design
|
||||
- [Error Handling](patterns/error-handling.md) — Error tuples, raise vs return, `with` chains
|
||||
- [Data Transforms](patterns/data-transforms.md) — Pipelines, Enum/Stream idioms, reduce patterns
|
||||
- [Process Design](patterns/process-design.md) — Supervision trees, process lifecycle, naming
|
||||
- [Testing](patterns/testing.md) — ExUnit patterns, assertions, test organization
|
||||
- [Documentation](patterns/documentation.md) — @moduledoc, @doc, @spec conventions
|
||||
- [Typespecs](patterns/typespecs.md) — Type definitions, opaque types, when to use what
|
||||
- [Macros](patterns/macros.md) — Macro patterns, hygiene, compile-time work
|
||||
- [Behaviours](patterns/behaviours.md) — Behaviour design, callbacks, optional callbacks
|
||||
- [Modules](patterns/modules.md) — Module organization, naming, structure
|
||||
|
||||
### Phoenix Patterns (`phoenix/`)
|
||||
- [Phoenix Patterns](phoenix/patterns.md) — Endpoint, Router, Controller, Channel, PubSub
|
||||
- [Phoenix Deviations](phoenix/deviations.md) — Where Phoenix differs from Elixir core
|
||||
|
||||
### Comparison (`comparison/`)
|
||||
- [Elixir vs Phoenix](comparison/elixir-vs-phoenix.md) — Side-by-side comparison of approaches
|
||||
|
||||
### Code Smells (`smells/`)
|
||||
- [Anti-Patterns](smells/anti-patterns.md) — Things the source avoids and why
|
||||
- [Common Mistakes](smells/common-mistakes.md) — What "bad Elixir" looks like
|
||||
|
||||
## Philosophy
|
||||
|
||||
Every pattern here is backed by a source citation. If a pattern can't point to where the Elixir or Phoenix team actually does it, it doesn't belong here.
|
||||
|
||||
## Contributing
|
||||
|
||||
Found a pattern worth adding? Open a PR with:
|
||||
1. The pattern name
|
||||
2. The exact file path and line range
|
||||
3. Why it matters
|
||||
4. What the anti-pattern looks like
|
||||
@@ -0,0 +1,117 @@
|
||||
# Elixir Core vs Phoenix: Side-by-Side Comparison
|
||||
|
||||
How the same concepts are approached differently (or similarly) between Elixir core and Phoenix.
|
||||
|
||||
## Process Lifecycle
|
||||
|
||||
| Aspect | Elixir Core | Phoenix |
|
||||
|--------|-------------|---------|
|
||||
| **Default restart** | `:permanent` (GenServer, Supervisor) | `:temporary` (Channel) |
|
||||
| **Hibernation** | Not set by default | 15s idle → hibernate (Channel) |
|
||||
| **Process identity** | Registry `:via` tuples | Topic-based (channels identified by topic) |
|
||||
| **Supervision** | Direct supervisor reference | Endpoint supervisor manages all |
|
||||
|
||||
**Source (Elixir):** `lib/elixir/lib/gen_server.ex:843-848` (child_spec without restart = defaults to :permanent)
|
||||
**Source (Phoenix):** `lib/phoenix/channel.ex:470-475` (explicit restart: :temporary)
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Aspect | Elixir Core | Phoenix |
|
||||
|--------|-------------|---------|
|
||||
| **Exception design** | Minimal struct fields | HTTP-aware (`plug_status`) |
|
||||
| **Bang functions** | `File.read!` raises | `broadcast!` raises |
|
||||
| **Failure response** | `{:error, reason}` tuple | `{:error, reason}` + HTTP status |
|
||||
| **Recovery** | Supervisor restart | Client reconnection |
|
||||
|
||||
**Source (Elixir):** Standard tagged tuples throughout (`lib/elixir/lib/agent.ex:210`)
|
||||
**Source (Phoenix):** `lib/phoenix/router.ex:7-8` (NoRouteError with plug_status: 404)
|
||||
|
||||
---
|
||||
|
||||
## Behaviour Design
|
||||
|
||||
| Aspect | Elixir Core | Phoenix |
|
||||
|--------|-------------|---------|
|
||||
| **Required callbacks** | Most are optional | Only `join/3` required (Channel) |
|
||||
| **`__using__` generates** | `child_spec/1` + `@behaviour` | child_spec + behaviour + config + imports |
|
||||
| **Configuration** | Via `use Module, opts` | Via `use Module, opts` + module attributes |
|
||||
| **Before-compile** | Rarely used | Heavily used (routes, intercepts) |
|
||||
|
||||
**Source (Elixir):** `lib/elixir/lib/gen_server.ex:834-851`
|
||||
**Source (Phoenix):** `lib/phoenix/channel.ex:450-500`
|
||||
|
||||
---
|
||||
|
||||
## Macro Usage
|
||||
|
||||
| Aspect | Elixir Core | Phoenix |
|
||||
|--------|-------------|---------|
|
||||
| **Philosophy** | Minimal, prefer functions | Justified by performance |
|
||||
| **`__using__`** | Generates 1-2 functions | Generates functions + sets up DSL |
|
||||
| **DSL creation** | Avoided (except Kernel/SpecialForms) | Embraced (Router DSL) |
|
||||
| **Attribute accumulation** | Rare | Central pattern (routes, sockets) |
|
||||
|
||||
**Source (Elixir):** `lib/elixir/lib/gen_server.ex:834` — simple `__using__`
|
||||
**Source (Phoenix):** `lib/phoenix/router.ex:263-290` — complex DSL setup with attribute accumulation
|
||||
|
||||
---
|
||||
|
||||
## Module Organization
|
||||
|
||||
| Aspect | Elixir Core | Phoenix |
|
||||
|--------|-------------|---------|
|
||||
| **File naming** | `gen_server.ex` (snake_case) | `controller.ex` (snake_case) |
|
||||
| **Nesting** | 2 levels max (`Task.Supervised`) | 2-3 levels (`Phoenix.Channel.Server`) |
|
||||
| **Internal modules** | `@moduledoc false` | `@moduledoc false` |
|
||||
| **Public API** | Functions on the main module | Functions + macros on the main module |
|
||||
|
||||
Both follow the same convention: public API on the parent module, implementation details in nested submodules with `@moduledoc false`.
|
||||
|
||||
---
|
||||
|
||||
## State Management
|
||||
|
||||
| Aspect | Elixir Core | Phoenix |
|
||||
|--------|-------------|---------|
|
||||
| **Agent** | Simple state, function-based access | Socket assigns (`assign/2`) |
|
||||
| **GenServer** | Full control, handle_call/cast/info | Channel handles (same callbacks) |
|
||||
| **State shape** | Any term (developer's choice) | `%Socket{}` struct (framework-defined) |
|
||||
| **State access** | Direct in callbacks | Via `socket.assigns` |
|
||||
|
||||
**Source (Elixir):** `lib/elixir/lib/agent.ex:62-82` (compute in server vs client)
|
||||
**Source (Phoenix):** `lib/phoenix/channel.ex:463-464` (`import Phoenix.Socket, only: [assign: 3, assign: 2]`)
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
| Aspect | Elixir Core | Phoenix |
|
||||
|--------|-------------|---------|
|
||||
| **Moduledoc size** | Very large (GenServer: 530 lines) | Large (Router: ~260 lines) |
|
||||
| **Examples** | Doctests (verified by tests) | Examples in docs (not always doctests) |
|
||||
| **Admonitions** | Info blocks for `use` | Info blocks for `use` |
|
||||
| **Guides** | Linked from moduledoc | Linked from moduledoc |
|
||||
| **Deprecation** | `@doc deprecated: "Use X instead"` | Inline comments (TODO markers) |
|
||||
|
||||
Both use the same documentation infrastructure (ExDoc), but Elixir core tends toward more exhaustive docs (GenServer's moduledoc is essentially a tutorial).
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
| Aspect | Elixir Core | Phoenix |
|
||||
|--------|-------------|---------|
|
||||
| **Compile-time** | Module attributes | `Application.compile_env` |
|
||||
| **Runtime** | Application env / init args | `config/2` callback + Application env |
|
||||
| **Per-instance** | Options to `start_link` | Endpoint config per environment |
|
||||
|
||||
**Source (Phoenix):** `lib/phoenix/endpoint.ex:422-430` (compile-time config checking)
|
||||
|
||||
```elixir
|
||||
var!(code_reloading?) =
|
||||
Application.compile_env(@otp_app, [__MODULE__, :code_reloader], false)
|
||||
```
|
||||
|
||||
This pattern — reading config at compile time and validating it against runtime — is Phoenix-specific. Elixir core reads config only at runtime.
|
||||
@@ -0,0 +1,196 @@
|
||||
# Behaviour Patterns
|
||||
|
||||
How behaviours are designed, implemented, and used in Elixir core and Phoenix.
|
||||
|
||||
## 1. Behaviour Definition with `@callback`
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:577-812` (all callback definitions)
|
||||
|
||||
```elixir
|
||||
@callback init(init_arg :: term) ::
|
||||
{:ok, state}
|
||||
| {:ok, state, timeout | :hibernate | {:continue, continue_arg}}
|
||||
| :ignore
|
||||
| {:stop, reason :: any}
|
||||
|
||||
@callback handle_call(request :: term, from, state :: term) ::
|
||||
{:reply, reply, new_state}
|
||||
| {:noreply, new_state}
|
||||
| {:stop, reason, reply, new_state}
|
||||
| {:stop, reason, new_state}
|
||||
when reply: term, new_state: term, reason: term
|
||||
```
|
||||
|
||||
**Why:** Callbacks with full type unions document every valid return. Named parameters (`init_arg`, `request`, `state`) serve as documentation. The `when` clause defines type variables used across the union.
|
||||
|
||||
**Anti-pattern:** Defining callbacks with `@callback handle_call(term, term, term) :: term` — provides zero guidance to implementors.
|
||||
|
||||
---
|
||||
|
||||
## 2. `@optional_callbacks` for Extensibility
|
||||
|
||||
**Source:** `lib/phoenix/channel.ex:442-448`
|
||||
|
||||
```elixir
|
||||
@optional_callbacks handle_in: 3,
|
||||
handle_out: 3,
|
||||
handle_info: 2,
|
||||
handle_call: 3,
|
||||
handle_cast: 2,
|
||||
code_change: 3,
|
||||
terminate: 2
|
||||
```
|
||||
|
||||
**Why:** Only `join/3` is required for a Channel. Everything else has sensible defaults. This keeps the minimum implementation surface small — a Channel that just joins and broadcasts needs only one function.
|
||||
|
||||
**Anti-pattern:** Making all callbacks required when most have reasonable defaults — forces implementors to write boilerplate they don't need.
|
||||
|
||||
---
|
||||
|
||||
## 3. `@behaviour` Declaration in `__using__`
|
||||
|
||||
**Source:** `lib/phoenix/channel.ex:450-453`
|
||||
|
||||
```elixir
|
||||
defmacro __using__(opts \\ []) do
|
||||
quote do
|
||||
opts = unquote(opts)
|
||||
@behaviour unquote(__MODULE__)
|
||||
@on_definition unquote(__MODULE__)
|
||||
@before_compile unquote(__MODULE__)
|
||||
...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:836`
|
||||
|
||||
```elixir
|
||||
quote location: :keep, bind_quoted: [opts: opts] do
|
||||
@behaviour GenServer
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Setting `@behaviour` inside `use` means users get compile-time warnings about missing callbacks automatically. They don't need to know about the behaviour mechanism — `use Phoenix.Channel` handles it.
|
||||
|
||||
**Anti-pattern:** Requiring users to manually add both `use MyModule` AND `@behaviour MyModule`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Default Implementations via `defoverridable`
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:849`
|
||||
|
||||
```elixir
|
||||
def child_spec(init_arg) do
|
||||
default = %{
|
||||
id: __MODULE__,
|
||||
start: {__MODULE__, :start_link, [init_arg]}
|
||||
}
|
||||
Supervisor.child_spec(default, unquote(Macro.escape(opts)))
|
||||
end
|
||||
|
||||
defoverridable child_spec: 1
|
||||
```
|
||||
|
||||
**Why:** `defoverridable` provides a working default that users CAN customize but don't HAVE to. The generated function works for the 90% case. The 10% can override it.
|
||||
|
||||
**Anti-pattern:** Not using `defoverridable` — users who need custom behavior must bypass the `use` macro entirely.
|
||||
|
||||
---
|
||||
|
||||
## 5. Phoenix Channel: Behaviour + Process + Protocol
|
||||
|
||||
**Source:** `lib/phoenix/channel.ex:364-448` (full callback set)
|
||||
|
||||
The Channel behaviour combines:
|
||||
1. **Required callback:** `join/3` (authorization gate)
|
||||
2. **Optional callbacks:** `handle_in/3`, `handle_info/2`, etc. (event handlers)
|
||||
3. **Process semantics:** Each channel is a GenServer (line 476-479)
|
||||
4. **Configuration via module attributes:** `@phoenix_log_join`, `@phoenix_hibernate_after`
|
||||
|
||||
```elixir
|
||||
# From __using__ — configures the process
|
||||
@phoenix_hibernate_after Keyword.get(opts, :hibernate_after, 15_000)
|
||||
@phoenix_shutdown Keyword.get(opts, :shutdown, 5000)
|
||||
|
||||
def child_spec(init_arg) do
|
||||
%{
|
||||
id: __MODULE__,
|
||||
start: {__MODULE__, :start_link, [init_arg]},
|
||||
shutdown: @phoenix_shutdown,
|
||||
restart: :temporary
|
||||
}
|
||||
end
|
||||
|
||||
def start_link(triplet) do
|
||||
GenServer.start_link(Phoenix.Channel.Server, triplet,
|
||||
hibernate_after: @phoenix_hibernate_after
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The Channel behaviour demonstrates layering — it's a behaviour (compile-time contract), a process (runtime entity), and configurable (via options to `use`). Each concern is handled by the appropriate mechanism.
|
||||
|
||||
**Anti-pattern:** Trying to encode runtime configuration in the behaviour contract itself, or conflating compile-time and runtime concerns.
|
||||
|
||||
---
|
||||
|
||||
## 6. Callback Documentation Pattern
|
||||
|
||||
**Source:** `lib/phoenix/channel.ex:350-363` (join callback doc)
|
||||
|
||||
```elixir
|
||||
@doc """
|
||||
Handle channel joins by `topic`.
|
||||
|
||||
...
|
||||
|
||||
## Example
|
||||
|
||||
def join("room:lobby", payload, socket) do
|
||||
if authorized?(payload) do
|
||||
{:ok, socket}
|
||||
else
|
||||
{:error, %{reason: "unauthorized"}}
|
||||
end
|
||||
end
|
||||
"""
|
||||
@callback join(topic :: binary, payload :: payload, socket :: Socket.t()) ::
|
||||
{:ok, Socket.t()}
|
||||
| {:ok, reply :: payload, Socket.t()}
|
||||
| {:error, reason :: map}
|
||||
```
|
||||
|
||||
**Why:** Every callback gets:
|
||||
1. A `@doc` explaining when it's called and what it should do
|
||||
2. A concrete example
|
||||
3. The full type spec with all valid returns
|
||||
|
||||
This trio (doc + example + spec) gives implementors everything they need.
|
||||
|
||||
**Anti-pattern:** Defining callbacks without documentation — implementors have to read source code to understand when callbacks fire.
|
||||
|
||||
---
|
||||
|
||||
## 7. Phoenix.Endpoint: Behaviour as Interface Contract
|
||||
|
||||
**Source:** `lib/phoenix/endpoint.ex:408`
|
||||
|
||||
```elixir
|
||||
defmacro __using__(opts) do
|
||||
quote do
|
||||
@behaviour Phoenix.Endpoint
|
||||
|
||||
unquote(config(opts))
|
||||
unquote(pubsub())
|
||||
unquote(plug())
|
||||
unquote(server())
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The Endpoint uses `@behaviour` to define what an endpoint MUST provide (like `config/2`), then `__using__` generates the common implementation. The behaviour is the interface; the macro provides the default implementation.
|
||||
|
||||
**Anti-pattern:** Using only a behaviour without a `use` macro when significant boilerplate is required — forces every implementor to write the same code.
|
||||
@@ -0,0 +1,448 @@
|
||||
# Data Transform & Pipeline Patterns in Elixir Core
|
||||
|
||||
Patterns extracted from Elixir's standard library source code.
|
||||
|
||||
---
|
||||
|
||||
## 1. List-Specialized Clause Before Protocol Dispatch
|
||||
|
||||
**Source:** `lib/elixir/lib/enum.ex` lines 1723–1733
|
||||
|
||||
**What it does:** Every public Enum function defines a `when is_list(enumerable)` clause first, then a generic fallback that uses the Enumerable protocol.
|
||||
|
||||
```elixir
|
||||
def map(enumerable, fun) when is_list(enumerable) do
|
||||
:lists.map(fun, enumerable)
|
||||
end
|
||||
|
||||
def map(first..last//step, fun) do
|
||||
map_range(first, last, step, fun)
|
||||
end
|
||||
|
||||
def map(enumerable, fun) do
|
||||
reduce(enumerable, [], R.map(fun)) |> :lists.reverse()
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Lists are by far the most common enumerable. Matching them first avoids protocol dispatch overhead entirely (direct Erlang BIF call). The range clause is a further optimization for a common case. The generic clause handles all other enumerables through the protocol.
|
||||
|
||||
**Anti-pattern:** A single clause that always goes through protocol dispatch:
|
||||
```elixir
|
||||
# BAD — forces protocol overhead even for lists
|
||||
def map(enumerable, fun) do
|
||||
Enumerable.reduce(enumerable, {:cont, []}, fn x, acc ->
|
||||
{:cont, [fun.(x) | acc]}
|
||||
end) |> elem(1) |> :lists.reverse()
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Build-Then-Reverse (Cons-Cell Accumulation)
|
||||
|
||||
**Source:** `lib/elixir/lib/enum.ex` lines 1124, 1733, 2697
|
||||
|
||||
**What it does:** Accumulates results by prepending to a list (`[x | acc]`), then reverses at the end.
|
||||
|
||||
```elixir
|
||||
# filter (line 1124)
|
||||
def filter(enumerable, fun) do
|
||||
reduce(enumerable, [], R.filter(fun)) |> :lists.reverse()
|
||||
end
|
||||
|
||||
# map (line 1733)
|
||||
def map(enumerable, fun) do
|
||||
reduce(enumerable, [], R.map(fun)) |> :lists.reverse()
|
||||
end
|
||||
|
||||
# reject (line 2697)
|
||||
def reject(enumerable, fun) do
|
||||
reduce(enumerable, [], R.reject(fun)) |> :lists.reverse()
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Prepending to a linked list is O(1); appending is O(n). Building reversed then flipping once is O(n) total. Appending each element would be O(n²).
|
||||
|
||||
**Anti-pattern:** Appending to the end of a list in a loop:
|
||||
```elixir
|
||||
# BAD — O(n²)
|
||||
def map(enumerable, fun) do
|
||||
reduce(enumerable, [], fn x, acc -> acc ++ [fun.(x)] end)
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Pipeline for Linear Transformations, Bare Calls for Control Flow
|
||||
|
||||
**Source:** `lib/elixir/lib/enum.ex` lines 1684–1685, 1551, vs 496–502
|
||||
|
||||
**What it does:** Elixir core uses `|>` when data flows linearly through 2+ transformations. It does NOT use `|>` for single-step operations or when the first argument is computed by a `case`/`if`/`with`.
|
||||
|
||||
```elixir
|
||||
# Pipeline: data flows through multiple transforms (line 1684-1685)
|
||||
def map_join(enumerable, joiner \\ "", mapper) do
|
||||
enumerable
|
||||
|> map(&entry_to_string(mapper.(&1)))
|
||||
|> intersperse(joiner)
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
# NO pipeline: single step or control flow (line 496-502)
|
||||
def at(enumerable, index, default \\ nil) when is_integer(index) do
|
||||
case slice_forward(enumerable, index, 1, 1) do
|
||||
[value] -> value
|
||||
[] -> default
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Pipelines communicate "data flows through transformations." Using them for a single function call or wrapping around control flow obscures intent rather than clarifying it.
|
||||
|
||||
**Anti-pattern:** Pipelines for single operations or wrapping control flow:
|
||||
```elixir
|
||||
# BAD — single step, no pipeline needed
|
||||
list |> Enum.reverse()
|
||||
|
||||
# BAD — control flow awkwardly forced into a pipe
|
||||
result
|
||||
|> case do
|
||||
{:ok, x} -> x
|
||||
:error -> nil
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Pipeline Ending with `|> elem(1)` (Protocol Reduce Unwrap)
|
||||
|
||||
**Source:** `lib/elixir/lib/enum.ex` lines 363, 403, 433, 468, 725, 1022, 2676
|
||||
|
||||
**What it does:** When calling `Enumerable.reduce/3` directly, the result is always `{:done | :halted | :suspended, acc}`. Core extracts the accumulator with `|> elem(1)`.
|
||||
|
||||
```elixir
|
||||
# all?/1 (line 363)
|
||||
def all?(enumerable) do
|
||||
Enumerable.reduce(enumerable, {:cont, true}, fn entry, _ ->
|
||||
if entry, do: {:cont, true}, else: {:halt, false}
|
||||
end)
|
||||
|> elem(1)
|
||||
end
|
||||
|
||||
# reduce/3 (line 2676)
|
||||
def reduce(enumerable, acc, fun) do
|
||||
Enumerable.reduce(enumerable, {:cont, acc}, fun) |> elem(1)
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The protocol returns tagged tuples for the state machine (cont/halt/suspend). End users don't need the tag — only the accumulated value. `|> elem(1)` is the idiomatic unwrap.
|
||||
|
||||
**Anti-pattern:** Using `case` when you don't care about the tag:
|
||||
```elixir
|
||||
# BAD — unnecessary pattern match when you always want the value
|
||||
case Enumerable.reduce(enumerable, {:cont, acc}, fun) do
|
||||
{:done, result} -> result
|
||||
{:halted, result} -> result
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Private Helper Decomposition: Recursive Workers with Guards
|
||||
|
||||
**Source:** `lib/elixir/lib/enum.ex` lines 4975–4995, 5025–5039
|
||||
|
||||
**What it does:** Complex operations are split into a public entry point (with validation guards) and a private recursive worker function. The worker uses pattern matching on structure (empty list, head|tail) and guards on counters.
|
||||
|
||||
```elixir
|
||||
# Public entry: validates, delegates (line 890-904)
|
||||
def drop(enumerable, amount)
|
||||
when is_list(enumerable) and is_integer(amount) and amount >= 0 do
|
||||
drop_list(enumerable, amount)
|
||||
end
|
||||
|
||||
# Private worker: pattern matches on structure (line 4975-4983)
|
||||
defp split_list([head | tail], counter, acc) when counter > 0 do
|
||||
split_list(tail, counter - 1, [head | acc])
|
||||
end
|
||||
|
||||
defp split_list(list, 0, acc) do
|
||||
{:lists.reverse(acc), list}
|
||||
end
|
||||
|
||||
defp split_list([], _, acc) do
|
||||
{:lists.reverse(acc), []}
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Separating validation from recursion keeps each clause focused. Guards in function heads enable the BEAM to optimize dispatch with jump tables. No runtime `if`/`cond` needed.
|
||||
|
||||
**Anti-pattern:** Mixing validation, edge cases, and recursion in a single function with internal conditionals:
|
||||
```elixir
|
||||
# BAD — one big function with nested ifs
|
||||
defp split_list(list, counter, acc) do
|
||||
if counter > 0 and list != [] do
|
||||
[head | tail] = list
|
||||
split_list(tail, counter - 1, [head | acc])
|
||||
else
|
||||
{:lists.reverse(acc), list}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Enum vs Stream Decision Pattern
|
||||
|
||||
**Source:** `lib/elixir/lib/stream.ex` lines 1–80 (module docs), `lib/elixir/lib/enum.ex`
|
||||
|
||||
**What it does:** Enum functions are eager (materialize intermediate lists). Stream functions are lazy (build computation recipes). Core uses Stream for:
|
||||
- Infinite sequences (`cycle`, `iterate`, `repeatedly`)
|
||||
- Resource management (`resource/3`)
|
||||
- Composing transformations to execute in a single pass
|
||||
|
||||
```elixir
|
||||
# Stream: builds a recipe, zero computation until consumed (stream.ex ~line 490)
|
||||
def map(enum, fun) when is_function(fun, 1) do
|
||||
lazy(enum, fn f1 -> R.map(fun, f1) end)
|
||||
end
|
||||
|
||||
# Enum: immediately materializes the result (enum.ex line 1723)
|
||||
def map(enumerable, fun) when is_list(enumerable) do
|
||||
:lists.map(fun, enumerable)
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** From Stream docs (lines 37–41): "When chaining many operations with `Enum`, intermediate lists are created, while `Stream` creates a recipe of computations that are executed at a later moment."
|
||||
|
||||
Use Enum when:
|
||||
- You need the full result now
|
||||
- The collection is small/bounded
|
||||
- You only chain 1–2 operations
|
||||
|
||||
Use Stream when:
|
||||
- The collection is large or infinite
|
||||
- You chain many transformations
|
||||
- You need resource cleanup (file handles, network)
|
||||
- You want single-pass processing
|
||||
|
||||
**Anti-pattern:** Using Stream for small bounded collections (overhead of the lazy machinery exceeds any benefit):
|
||||
```elixir
|
||||
# BAD — Stream overhead for trivial transform
|
||||
[1, 2, 3] |> Stream.map(&(&1 * 2)) |> Enum.to_list()
|
||||
|
||||
# GOOD — just use Enum
|
||||
[1, 2, 3] |> Enum.map(&(&1 * 2))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Map.update vs Map.put Decision Pattern
|
||||
|
||||
**Source:** `lib/elixir/lib/map.ex` lines 670–700
|
||||
|
||||
**What it does:** `Map.update/4` transforms an existing value based on its current state. `Map.put/3` unconditionally sets a value regardless of current state.
|
||||
|
||||
```elixir
|
||||
# Map.update/4 (line 682-693): transform based on current value
|
||||
def update(map, key, default, fun) when is_function(fun, 1) do
|
||||
case map do
|
||||
%{^key => value} ->
|
||||
%{map | key => fun.(value)}
|
||||
%{} ->
|
||||
put(map, key, default)
|
||||
other ->
|
||||
:erlang.error({:badmap, other}, [map, key, default, fun])
|
||||
end
|
||||
end
|
||||
|
||||
# Map.put/3 (line 636): unconditional set
|
||||
def put(map, key, value) do
|
||||
:maps.put(key, value, map)
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** `update/4` is for when the new value depends on the old value (counters, appending to nested lists). `put/3` is for when you know the exact new value regardless of what was there.
|
||||
|
||||
**Anti-pattern:** Using `get` + `put` when `update` expresses intent:
|
||||
```elixir
|
||||
# BAD — two lookups, unclear intent
|
||||
count = Map.get(map, :count, 0)
|
||||
Map.put(map, :count, count + 1)
|
||||
|
||||
# GOOD — single lookup, clear intent
|
||||
Map.update(map, :count, 1, &(&1 + 1))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Pattern Matching on Map Structure for Dispatch
|
||||
|
||||
**Source:** `lib/elixir/lib/map.ex` lines 398, 509, 586
|
||||
|
||||
**What it does:** Map functions use `case map do %{^key => value} -> ...` to dispatch on whether a key exists, rather than calling `has_key?` + conditional.
|
||||
|
||||
```elixir
|
||||
# Map.get/3 (line 586-594)
|
||||
def get(map, key, default \\ nil) do
|
||||
case map do
|
||||
%{^key => value} ->
|
||||
value
|
||||
%{} ->
|
||||
default
|
||||
other ->
|
||||
:erlang.error({:badmap, other}, [map, key, default])
|
||||
end
|
||||
end
|
||||
|
||||
# Map.put_new/3 (line 398-407)
|
||||
def put_new(map, key, value) do
|
||||
case map do
|
||||
%{^key => _value} ->
|
||||
map
|
||||
%{} ->
|
||||
put(map, key, value)
|
||||
other ->
|
||||
:erlang.error({:badmap, other})
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Pattern matching with `%{^key => value}` does the lookup AND extraction in one step. The `%{}` clause (empty map pattern) matches any map where the key is NOT present. The `other` clause provides a clear error for non-maps. This is both more efficient and more readable than `if Map.has_key?(map, key)`.
|
||||
|
||||
**Anti-pattern:**
|
||||
```elixir
|
||||
# BAD — double lookup, less clear
|
||||
def get(map, key, default) do
|
||||
if Map.has_key?(map, key) do
|
||||
:maps.get(key, map)
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Delegating to Erlang BIFs with `defdelegate`
|
||||
|
||||
**Source:** `lib/elixir/lib/map.ex` lines 127, 143, 159, 173
|
||||
|
||||
**What it does:** When an Erlang function already does exactly what's needed, Elixir delegates directly rather than wrapping.
|
||||
|
||||
```elixir
|
||||
@spec keys(map) :: [key]
|
||||
defdelegate keys(map), to: :maps
|
||||
|
||||
@spec values(map) :: [value]
|
||||
defdelegate values(map), to: :maps
|
||||
|
||||
@spec merge(map, map) :: map
|
||||
defdelegate merge(map1, map2), to: :maps
|
||||
```
|
||||
|
||||
**Why:** Zero overhead — the compiler inlines these. No point wrapping an Erlang BIF just to have an Elixir wrapper when the semantics are identical. The `@compile {:inline, ...}` annotation on line 115 makes this explicit.
|
||||
|
||||
**Anti-pattern:** Wrapping without adding value:
|
||||
```elixir
|
||||
# BAD — pointless wrapper
|
||||
def keys(map) do
|
||||
:maps.keys(map)
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Reduce as the Universal Primitive
|
||||
|
||||
**Source:** `lib/elixir/lib/enum.ex` lines 19–21, 2660–2676
|
||||
|
||||
**What it does:** Nearly every Enum operation is built on top of `reduce`. The Enumerable protocol's core function is `reduce/3`. Everything else (`count`, `member?`, `slice`) is an optimization hint.
|
||||
|
||||
```elixir
|
||||
# From the protocol docs (line 19-21):
|
||||
def map(enumerable, fun) do
|
||||
reducer = fn x, acc -> {:cont, [fun.(x) | acc]} end
|
||||
Enumerable.reduce(enumerable, {:cont, []}, reducer) |> elem(1) |> :lists.reverse()
|
||||
end
|
||||
|
||||
# The actual reduce/3 (line 2676):
|
||||
def reduce(enumerable, acc, fun) do
|
||||
Enumerable.reduce(enumerable, {:cont, acc}, fun) |> elem(1)
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Reduce is the most general iteration primitive. By building all operations on reduce, any data structure that implements `Enumerable.reduce/3` automatically gets the full `Enum` API. This is the protocol + reduce = universal composability pattern.
|
||||
|
||||
**Anti-pattern:** Implementing each Enum function independently for each data structure:
|
||||
```elixir
|
||||
# BAD — reimplementing map for each type
|
||||
def map(%MyStruct{items: items}, fun), do: ...
|
||||
def filter(%MyStruct{items: items}, fun), do: ...
|
||||
# Instead: implement Enumerable.reduce/3 once and get everything
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Keyword Multi-Clause Guard Dispatch (String.split pattern)
|
||||
|
||||
**Source:** `lib/elixir/lib/string.ex` lines 516–563
|
||||
|
||||
**What it does:** Functions with many input shapes use multiple `def` clauses with guards to dispatch, handling each case distinctly rather than using internal `cond`/`case`.
|
||||
|
||||
```elixir
|
||||
def split(string, %Regex{} = pattern, options) when is_binary(string) and is_list(options) do
|
||||
Regex.split(pattern, string, options)
|
||||
end
|
||||
|
||||
def split(string, "", options) when is_binary(string) and is_list(options) do
|
||||
# special case: split by empty string (grapheme-by-grapheme)
|
||||
...
|
||||
end
|
||||
|
||||
def split(string, [], options) when is_binary(string) and is_list(options) do
|
||||
# empty pattern list: no splitting
|
||||
...
|
||||
end
|
||||
|
||||
def split(string, pattern, options) when is_binary(string) and is_list(options) do
|
||||
# general binary pattern case
|
||||
...
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Each clause has a single responsibility. The BEAM compiler generates efficient dispatch for these patterns. Adding a new case is additive (new clause) rather than modifying existing logic.
|
||||
|
||||
**Anti-pattern:** One function with nested conditionals:
|
||||
```elixir
|
||||
# BAD — all cases mashed into one body
|
||||
def split(string, pattern, options) do
|
||||
cond do
|
||||
is_struct(pattern, Regex) -> ...
|
||||
pattern == "" -> ...
|
||||
pattern == [] -> ...
|
||||
true -> ...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Lazy Private Helpers with `defp parts_to_index`
|
||||
|
||||
**Source:** `lib/elixir/lib/string.ex` lines 562–563
|
||||
|
||||
**What it does:** Tiny private helpers that convert between API-level concepts and implementation-level values use single-line `defp` with guards.
|
||||
|
||||
```elixir
|
||||
defp parts_to_index(:infinity), do: 0
|
||||
defp parts_to_index(n) when is_integer(n) and n > 0, do: n
|
||||
```
|
||||
|
||||
**Why:** Clear, self-documenting dispatch. Each case is one line. No branching logic in the caller. The function name explains the conversion.
|
||||
|
||||
**Anti-pattern:** Inline conditional in the caller:
|
||||
```elixir
|
||||
# BAD — logic scattered in caller
|
||||
index = if parts == :infinity, do: 0, else: parts
|
||||
```
|
||||
@@ -0,0 +1,328 @@
|
||||
# Documentation Patterns
|
||||
|
||||
Patterns extracted from the Elixir standard library source code.
|
||||
|
||||
---
|
||||
|
||||
## 1. @moduledoc with Structured Sections
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex` lines 6–100+, `lib/logger/lib/logger.ex` lines 6–200+
|
||||
|
||||
**What it does:** `@moduledoc` uses a clear hierarchical structure with `##` sections covering: overview, examples, configuration, and detailed behavioral explanations. GenServer covers "Example", "Client / Server APIs", "How to supervise", "Name registration", "Timeouts", "Debugging". Logger covers "Levels", "Message", "Metadata", "Configuration" (with subsections for Boot/Compile/Runtime).
|
||||
|
||||
**Why:** Long moduledocs are effectively reference manuals. Structured sections let users jump to what they need. The pattern establishes a convention: start with "what is this" → "quick example" → "detailed topics."
|
||||
|
||||
**Anti-pattern:** Writing a single unstructured wall of text, or documenting only the happy path without covering error handling, configuration, and advanced usage.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
defmodule GenServer do
|
||||
@moduledoc """
|
||||
A behaviour module for implementing the server of a client-server relation.
|
||||
|
||||
A GenServer is a process like any other Elixir process...
|
||||
|
||||
## Example
|
||||
|
||||
The GenServer behaviour abstracts the common client-server interaction...
|
||||
|
||||
defmodule Stack do
|
||||
use GenServer
|
||||
# ...
|
||||
end
|
||||
|
||||
## How to supervise
|
||||
|
||||
A GenServer is most commonly started under a supervision tree...
|
||||
|
||||
## Name registration
|
||||
|
||||
...
|
||||
|
||||
## Timeouts
|
||||
|
||||
...
|
||||
|
||||
## Debugging with the :sys module
|
||||
|
||||
...
|
||||
"""
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. @doc with Sections and Examples
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 315–335 (abs/1), `lib/logger/lib/logger.ex` lines 536–540
|
||||
|
||||
**What it does:** `@doc` for individual functions includes: a brief one-liner, allowed-in-guards note (via `@doc guard: true`), explanation of edge cases, and `## Examples` with doctests.
|
||||
|
||||
**Why:** Each function doc is self-contained. A developer reading docs shouldn't need to look elsewhere for basic usage. Guard-eligible functions are annotated structurally so tools can surface this.
|
||||
|
||||
**Anti-pattern:** Docs that say "see module documentation" without providing any local context. Also: examples without `iex>` that can't be tested automatically.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@doc """
|
||||
Returns all the available levels.
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
@spec levels() :: [level(), ...]
|
||||
def levels(), do: @levels
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. @doc since: Version Annotation
|
||||
|
||||
**Source:** `lib/logger/lib/logger.ex` lines 539, 576, 813, 824, 831, `lib/elixir/lib/kernel.ex` line 5163+
|
||||
|
||||
**What it does:** Attaches `since: "X.Y.Z"` metadata to `@doc` indicating the Elixir version that introduced the function.
|
||||
|
||||
**Why:** Users working with multiple Elixir versions need to know API availability. ExDoc renders this prominently. The pattern is universal in the standard library for any function added after 1.0.
|
||||
|
||||
**Anti-pattern:** Adding new public functions without `@since` annotation, leaving version compatibility ambiguous.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@doc since: "1.15.0"
|
||||
@spec default_formatter(formatter_opts) :: {module, :logger.formatter_config()}
|
||||
def default_formatter(overrides \\ []) when is_list(overrides) do
|
||||
# ...
|
||||
end
|
||||
|
||||
@doc since: "1.14.0"
|
||||
def binary_slice(binary, start, size)
|
||||
when is_binary(binary) and is_integer(start) and is_integer(size) and size >= 0 do
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. @doc guard: true Metadata
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 329, 408, 428, 452, etc.
|
||||
|
||||
**What it does:** Functions/macros usable in guard clauses are annotated with `@doc guard: true` metadata, separate from the doc string itself.
|
||||
|
||||
**Why:** This is machine-readable metadata. ExDoc and IEx can filter/display guard-eligible functions differently. It's orthogonal to the prose documentation.
|
||||
|
||||
**Anti-pattern:** Mentioning "allowed in guards" only in the doc string text, which tools can't parse programmatically.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@doc """
|
||||
Returns the absolute value of `number`.
|
||||
|
||||
...
|
||||
|
||||
## Examples
|
||||
|
||||
iex> abs(-1)
|
||||
1
|
||||
iex> abs(1)
|
||||
1
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec abs(number) :: number
|
||||
def abs(number) when is_number(number), do: ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. @doc false — Hiding from Documentation
|
||||
|
||||
**Source:** `lib/elixir/lib/inspect.ex` lines 410, 417; implicit via `@impl true`
|
||||
|
||||
**What it does:** `@doc false` explicitly hides a function from documentation generators. Also, using `@impl true` automatically sets `@doc false` (unless `@doc` is explicitly provided).
|
||||
|
||||
**Why:** Not all public functions are part of the user-facing API. Protocol implementations, internal helpers that must be public for technical reasons, and overridable defaults should be hidden from docs.
|
||||
|
||||
**Anti-pattern:** Leaving documentation on functions that are public only for technical reasons (e.g., protocol dispatch functions), confusing users about what to call.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
# Explicit hiding:
|
||||
@doc false
|
||||
def __protocol__(:module), do: __MODULE__
|
||||
|
||||
# Implicit via @impl:
|
||||
@impl true # automatically sets @doc false
|
||||
def init(counter) do
|
||||
{:ok, counter}
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. @moduledoc false — Hiding Modules
|
||||
|
||||
**Source:** Common pattern in internal modules (not shown in top-level files but documented in `Module`)
|
||||
|
||||
**What it does:** `@moduledoc false` makes an entire module invisible to documentation tools.
|
||||
|
||||
**Why:** Internal implementation modules (e.g., `MyApp.Repo.Migrations.Internal`) exist for code organization but shouldn't appear in user-facing docs.
|
||||
|
||||
**Anti-pattern:** Leaving `@moduledoc` undeclared on internal modules — ExDoc will show them with an empty documentation page, confusing users.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
defmodule MyApp.Internal.Helper do
|
||||
@moduledoc false
|
||||
# This module exists but won't appear in docs
|
||||
def helper_function(x), do: x * 2
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Mermaid Diagrams in Documentation
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex` lines 14–20
|
||||
|
||||
**What it does:** Embeds Mermaid diagram syntax directly in `@moduledoc` to illustrate architectural patterns (client-server message flow).
|
||||
|
||||
**Why:** Visual diagrams communicate architecture faster than prose. ExDoc renders Mermaid natively, so documentation can include live-rendered flowcharts.
|
||||
|
||||
**Anti-pattern:** Describing complex interaction patterns only in prose when a diagram would be clearer.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@moduledoc """
|
||||
A behaviour module for implementing the server of a client-server relation.
|
||||
|
||||
```mermaid
|
||||
graph BT
|
||||
C(Client #3) ~~~ B(Client #2) ~~~ A(Client #1)
|
||||
A & B & C -->|request| GenServer
|
||||
GenServer -.->|reply| A & B & C
|
||||
```
|
||||
|
||||
## Example
|
||||
...
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Admonition Blocks in Documentation
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex` lines 88–95, `lib/elixir/lib/supervisor.ex` lines 34–38
|
||||
|
||||
**What it does:** Uses markdown admonition syntax (`> #### Title {: .info}`) to highlight important callouts — especially for `use ModuleName` behavior documentation.
|
||||
|
||||
**Why:** Admonitions draw attention to critical information that might otherwise be buried in prose. The `.info`, `.warning`, `.neutral` classes provide visual differentiation.
|
||||
|
||||
**Anti-pattern:** Burying critical behavioral information (like what `use` does) in regular paragraphs that users skim over.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@moduledoc """
|
||||
...
|
||||
|
||||
> #### `use GenServer` {: .info}
|
||||
>
|
||||
> When you `use GenServer`, the `GenServer` module will
|
||||
> set `@behaviour GenServer` and define a `child_spec/1`
|
||||
> function, so your module can be used as a child
|
||||
> in a supervision tree.
|
||||
|
||||
...
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. @doc deprecated: Soft Deprecation
|
||||
|
||||
**Source:** `lib/elixir/lib/module.ex` lines 163–180
|
||||
|
||||
**What it does:** Uses `@doc deprecated: "Use X instead"` for soft deprecation (documentation-only warning) vs. `@deprecated "reason"` for hard deprecation (compiler warning).
|
||||
|
||||
**Why:** Two levels of deprecation serve different needs. Soft deprecation signals "we recommend the alternative" without breaking builds. Hard deprecation actively warns at compile time.
|
||||
|
||||
**Anti-pattern:** Using `@deprecated` for things that aren't truly deprecated yet (just discouraged), creating noise in build outputs.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
# Soft deprecation — docs only, no compiler warning:
|
||||
@doc deprecated: "Use Kernel.length/1 instead"
|
||||
def size(keyword) do
|
||||
length(keyword)
|
||||
end
|
||||
|
||||
# Hard deprecation — compiler warning emitted:
|
||||
@deprecated "Use Kernel.length/1 instead"
|
||||
def size(keyword) do
|
||||
length(keyword)
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Callback Documentation Convention
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex` lines 584–646 (handle_call docs)
|
||||
|
||||
**What it does:** Each `@callback` is preceded by a comprehensive `@doc` that explains: what triggers the callback, what the parameters mean, every possible return value and its effect, when the callback is optional, and cross-references to related callbacks.
|
||||
|
||||
**Why:** Callback documentation is the primary teaching tool for behaviour implementers. Since users must write these functions, the docs must be complete enough to implement from.
|
||||
|
||||
**Anti-pattern:** Documenting callbacks with only a one-liner, forcing users to read source code or external guides.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@doc """
|
||||
Invoked to handle synchronous `call/3` messages. `call/3` will block until a
|
||||
reply is received (unless the call times out or nodes are disconnected).
|
||||
|
||||
`request` is the request message sent by a `call/3`, `from` is a 2-tuple
|
||||
containing the caller's PID and a term that uniquely identifies the call, and
|
||||
`state` is the current state of the `GenServer`.
|
||||
|
||||
Returning `{:reply, reply, new_state}` sends the response `reply` to the
|
||||
caller and continues the loop with new state `new_state`.
|
||||
|
||||
Returning `{:noreply, new_state}` does not send a response to the caller and
|
||||
continues the loop with new state `new_state`. The response must be sent with
|
||||
`reply/2`.
|
||||
|
||||
...
|
||||
|
||||
This callback is optional. If one is not implemented, the server will fail
|
||||
if a call is performed against it.
|
||||
"""
|
||||
@callback handle_call(request :: term, from, state :: term) ::
|
||||
{:reply, reply, new_state}
|
||||
| ...
|
||||
when reply: term, new_state: term, reason: term
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Documentation with Link References (c: and t: prefixes)
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex` throughout the @moduledoc
|
||||
|
||||
**What it does:** Uses ExDoc link syntax like `c:init/1` (callback reference), `t:server/0` (type reference), and backtick function references like `` `start_link/2` `` to create navigable cross-references.
|
||||
|
||||
**Why:** Documentation is a hypertext. Cross-linking lets users navigate between related concepts. The `c:` and `t:` prefixes disambiguate between functions, callbacks, and types with the same name.
|
||||
|
||||
**Anti-pattern:** Mentioning related functions/types by name without linking them, requiring users to manually search.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@moduledoc """
|
||||
...
|
||||
`c:init/1` transforms our initial argument to the initial state for the
|
||||
GenServer. `c:handle_call/3` fires when the server receives a synchronous
|
||||
`pop` message...
|
||||
|
||||
Each call to `GenServer.call/3` results in a message
|
||||
that must be handled by the `c:handle_call/3` callback in the GenServer.
|
||||
...
|
||||
"""
|
||||
```
|
||||
@@ -0,0 +1,495 @@
|
||||
# Error Handling Patterns in Elixir Core
|
||||
|
||||
Patterns extracted from Elixir's standard library source code.
|
||||
|
||||
---
|
||||
|
||||
## 1. The `with` Macro — Normalized Error Clauses
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel/special_forms.ex` lines 1600–1715 (docs + definition)
|
||||
|
||||
**What it does:** The `with` macro chains pattern-matched steps where each `<-` clause returns a normalized error shape. When a step fails to match, the non-matched value falls through (or hits `else`).
|
||||
|
||||
```elixir
|
||||
# From the with docs (special_forms.ex line 1689-1707):
|
||||
# GOOD — each helper returns a normalized {:error, reason} shape
|
||||
with :ok <- validate_extension(path),
|
||||
:ok <- validate_exists(path) do
|
||||
backup_path = path <> ".backup"
|
||||
File.cp!(path, backup_path)
|
||||
{:ok, backup_path}
|
||||
end
|
||||
|
||||
defp validate_extension(path) do
|
||||
if Path.extname(path) == ".ex", do: :ok, else: {:error, :invalid_extension}
|
||||
end
|
||||
|
||||
defp validate_exists(path) do
|
||||
if File.exists?(path), do: :ok, else: {:error, :missing_file}
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The docs explicitly warn (line 1672) against reconstructing return types in `else` blocks. Instead, normalize each step so the unmatched value is already meaningful. This makes `else` optional — the error value itself is the result.
|
||||
|
||||
**Anti-pattern:** Matching raw return values in else and reconstructing meaning:
|
||||
```elixir
|
||||
# BAD — from the docs (line 1660-1678)
|
||||
with ".ex" <- Path.extname(path),
|
||||
true <- File.exists?(path) do
|
||||
{:ok, path}
|
||||
else
|
||||
binary when is_binary(binary) ->
|
||||
{:error, :invalid_extension}
|
||||
false ->
|
||||
{:error, :missing_file}
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Real-World `with` — Multi-Step Fallible Operations
|
||||
|
||||
**Source:** `lib/elixir/lib/exception.ex` lines 251–285 (`blame_mfa/4`)
|
||||
|
||||
**What it does:** Uses `with` to chain 5+ fallible steps where any failure should produce `:error`. Each step's pattern is an exact match.
|
||||
|
||||
```elixir
|
||||
defp blame_mfa(module, function, arity, call_args) do
|
||||
with [_ | _] = path <- :code.which(module),
|
||||
{:ok, {_, [debug_info: debug_info]}} <- :beam_lib.chunks(path, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} <- debug_info,
|
||||
{:ok, %{definitions: defs}} <- backend.debug_info(:elixir_v1, module, data, []),
|
||||
{_, kind, _, clauses} <- List.keyfind(defs, {function, arity}, 0) do
|
||||
# ... process clauses ...
|
||||
{:ok, kind, clauses}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Five sequential operations where each can fail differently. Without `with`, this would be 5 nested `case` statements. The `else _ -> :error` collapses all failure modes into one — appropriate here because callers only care success vs failure, not which step failed.
|
||||
|
||||
**Anti-pattern:** Deeply nested case statements:
|
||||
```elixir
|
||||
# BAD — pyramid of doom
|
||||
case :code.which(module) do
|
||||
[_ | _] = path ->
|
||||
case :beam_lib.chunks(path, [:debug_info]) do
|
||||
{:ok, {_, [debug_info: debug_info]}} ->
|
||||
case debug_info do
|
||||
{:debug_info_v1, backend, data} ->
|
||||
# ... and so on
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Another `with` — Error Info Extraction
|
||||
|
||||
**Source:** `lib/elixir/lib/exception.ex` lines 2695–2720 (`error_info/3` in ErlangError)
|
||||
|
||||
**What it does:** Chains pattern matching on a stacktrace and error_info map to extract formatted error details.
|
||||
|
||||
```elixir
|
||||
defp error_info(erl_exception, stacktrace, default_reason) do
|
||||
with [{module, fun, args_or_arity, opts} | tail] <- stacktrace,
|
||||
%{} = error_info <- opts[:error_info] do
|
||||
error_module = Map.get(error_info, :module, module)
|
||||
error_fun = Map.get(error_info, :function, :format_error)
|
||||
# ... format the error ...
|
||||
cond do
|
||||
map_size(args_errors) > 0 ->
|
||||
{:ok, reason, IO.iodata_to_binary([":\n\n" | Enum.map(args_errors, &arg_error/1)])}
|
||||
general = extra[:general] ->
|
||||
{:ok, reason, ": " <> IO.chardata_to_string(general)}
|
||||
true ->
|
||||
:error
|
||||
end
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The stacktrace might be empty or the opts might not contain `:error_info`. Using `with` makes the "happy path" clear while gracefully falling through to `:error` for any mismatch.
|
||||
|
||||
---
|
||||
|
||||
## 4. `{:ok, value}` / `:error` Convention (Map.fetch)
|
||||
|
||||
**Source:** `lib/elixir/lib/map.ex` lines 290–309
|
||||
|
||||
**What it does:** `Map.fetch/2` returns `{:ok, value}` on success and bare `:error` on failure. No reason atom, because the failure mode is obvious (key not found).
|
||||
|
||||
```elixir
|
||||
@spec fetch(map, key) :: {:ok, value} | :error
|
||||
def fetch(map, key), do: :maps.find(key, map)
|
||||
```
|
||||
|
||||
**Why:** The `:error` atom alone (not `{:error, reason}`) is idiomatic when there's only one possible failure mode. It's lighter to pattern match and compose with `with`:
|
||||
|
||||
```elixir
|
||||
with {:ok, width} <- Map.fetch(opts, "width"),
|
||||
{:ok, height} <- Map.fetch(opts, "height") do
|
||||
{:ok, width * height}
|
||||
end
|
||||
# Returns :error if either key is missing
|
||||
```
|
||||
|
||||
**Anti-pattern:** Returning `{:error, :not_found}` when there's only one failure mode:
|
||||
```elixir
|
||||
# BAD — unnecessary tuple wrapping for a single failure mode
|
||||
def fetch(map, key) do
|
||||
case :maps.find(key, map) do
|
||||
{:ok, value} -> {:ok, value}
|
||||
:error -> {:error, :not_found} # pointless — what else could it be?
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Bang Functions: Raise on Error (`fetch!` vs `fetch`)
|
||||
|
||||
**Source:** `lib/elixir/lib/map.ex` lines 311–380
|
||||
|
||||
**What it does:** The `!` suffix convention means "raises on failure instead of returning an error tuple." The non-bang version is for when the caller wants to handle the error.
|
||||
|
||||
```elixir
|
||||
# Non-bang: returns {:ok, value} | :error — caller decides
|
||||
@spec fetch(map, key) :: {:ok, value} | :error
|
||||
def fetch(map, key), do: :maps.find(key, map)
|
||||
|
||||
# Bang: raises KeyError — caller expects success
|
||||
@spec fetch!(map, key) :: value
|
||||
def fetch!(map, key), do: :maps.get(key, map)
|
||||
```
|
||||
|
||||
**Why:** Two audiences exist:
|
||||
1. Code that needs to handle missing keys gracefully → use `fetch/2`
|
||||
2. Code where missing key is a bug (preconditions guarantee it exists) → use `fetch!/2`
|
||||
|
||||
The bang version communicates "I assert this will succeed; if it doesn't, it's a program error."
|
||||
|
||||
**Anti-pattern:** Always using bang functions and rescuing:
|
||||
```elixir
|
||||
# BAD — using exceptions for control flow
|
||||
try do
|
||||
Map.fetch!(map, key)
|
||||
rescue
|
||||
KeyError -> default_value
|
||||
end
|
||||
|
||||
# GOOD — use the non-bang version
|
||||
case Map.fetch(map, key) do
|
||||
{:ok, value} -> value
|
||||
:error -> default_value
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Exception Structure: `defexception` Fields
|
||||
|
||||
**Source:** `lib/elixir/lib/exception.ex` lines 2250–2500 (exception definitions)
|
||||
|
||||
**What it does:** Exceptions carry meaningful fields beyond just `:message`. The `message/1` callback generates a human-readable string from those fields.
|
||||
|
||||
```elixir
|
||||
# File.Error — stores structured data, formats on demand
|
||||
defexception [:reason, :path, action: ""]
|
||||
|
||||
@impl true
|
||||
def message(%{action: action, reason: reason, path: path}) do
|
||||
formatted =
|
||||
case {action, reason} do
|
||||
{"remove directory", :eexist} -> "directory is not empty"
|
||||
_ -> IO.iodata_to_binary(:file.format_error(reason))
|
||||
end
|
||||
"could not #{action} #{inspect(path)}: #{formatted}"
|
||||
end
|
||||
|
||||
# KeyError — carries the key AND the term for context
|
||||
defexception [:key, :term, :message]
|
||||
|
||||
# Enum.OutOfBoundsError — carries index + enumerable
|
||||
defexception [:enumerable, :index, :message]
|
||||
```
|
||||
|
||||
**Why:** Fields enable programmatic inspection of errors (pattern matching in `rescue`, blame callbacks). The `message/1` callback is only called when the error needs to be displayed. Storing structured data costs nothing until you actually need the string.
|
||||
|
||||
**Anti-pattern:** Storing only a pre-formatted message string:
|
||||
```elixir
|
||||
# BAD — can't programmatically inspect the error
|
||||
defexception message: "something went wrong"
|
||||
raise MyError, "file not found: #{path}"
|
||||
# Caller can't extract `path` from the exception
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Custom `exception/1` Callback for Ergonomic Raising
|
||||
|
||||
**Source:** `lib/elixir/lib/exception.ex` lines 2255–2270 (UnicodeConversionError)
|
||||
|
||||
**What it does:** Override `exception/1` to accept raw values (not just keyword lists) and build the struct with a meaningful message.
|
||||
|
||||
```elixir
|
||||
defmodule UnicodeConversionError do
|
||||
defexception [:encoded, :message]
|
||||
|
||||
def exception(opts) do
|
||||
%UnicodeConversionError{
|
||||
encoded: Keyword.fetch!(opts, :encoded),
|
||||
message: "#{Keyword.fetch!(opts, :kind)} #{detail(Keyword.fetch!(opts, :rest))}"
|
||||
}
|
||||
end
|
||||
|
||||
defp detail(rest) when is_binary(rest) do
|
||||
"encoding starting at #{inspect(rest)}"
|
||||
end
|
||||
|
||||
defp detail([h | _]) when is_integer(h) do
|
||||
"code point #{h}"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The default `exception/1` just merges keywords into the struct. Custom callbacks let you compute messages from raw data, validate required fields (with `fetch!`), and transform inputs. This keeps `raise` calls clean:
|
||||
```elixir
|
||||
raise UnicodeConversionError, encoded: data, kind: "invalid", rest: <<0xFF>>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. `raise` Macro Internals: Compile-Time Type Resolution
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 2246–2294
|
||||
|
||||
**What it does:** The `raise` macro inspects the argument at compile time to determine if it's a string, binary expression, atom (module), or existing exception struct, generating optimized code for each case.
|
||||
|
||||
```elixir
|
||||
defmacro raise(message) do
|
||||
message =
|
||||
case not is_binary(message) and bootstrapped?(Macro) do
|
||||
true -> Macro.expand(message, __CALLER__)
|
||||
false -> message
|
||||
end
|
||||
|
||||
erlang_error = fn x ->
|
||||
quote do
|
||||
:erlang.error(unquote(x), :none, error_info: %{module: Exception})
|
||||
end
|
||||
end
|
||||
|
||||
case message do
|
||||
message when is_binary(message) ->
|
||||
erlang_error.(quote do: RuntimeError.exception(unquote(message)))
|
||||
{:<<>>, _, _} = message ->
|
||||
erlang_error.(quote do: RuntimeError.exception(unquote(message)))
|
||||
alias when is_atom(alias) ->
|
||||
erlang_error.(quote do: unquote(alias).exception([]))
|
||||
_ ->
|
||||
erlang_error.(quote do: Kernel.Utils.raise(unquote(message)))
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** By resolving at compile time, the generated code avoids runtime dispatch to figure out what kind of exception to create. String → RuntimeError, atom → that module's exception, struct → re-raise as-is.
|
||||
|
||||
---
|
||||
|
||||
## 9. Error Normalization: Erlang → Elixir Exception Translation
|
||||
|
||||
**Source:** `lib/elixir/lib/exception.ex` lines 2530–2680 (`ErlangError.normalize/2`)
|
||||
|
||||
**What it does:** Translates raw Erlang error reasons (atoms and tuples) into proper Elixir exception structs with helpful messages.
|
||||
|
||||
```elixir
|
||||
def normalize(:badarg, stacktrace) do
|
||||
case error_info(:badarg, stacktrace, "errors were found at the given arguments") do
|
||||
{:ok, reason, details} -> %ArgumentError{message: reason <> details}
|
||||
:error -> %ArgumentError{}
|
||||
end
|
||||
end
|
||||
|
||||
def normalize(:badarith, _stacktrace), do: %ArithmeticError{}
|
||||
|
||||
def normalize({:badarity, {fun, args}}, _stacktrace) do
|
||||
%BadArityError{function: fun, args: args}
|
||||
end
|
||||
|
||||
def normalize({:badkey, key}, stacktrace) do
|
||||
term =
|
||||
case stacktrace do
|
||||
[{Map, :get_and_update!, [map, _, _], _} | _] -> map
|
||||
[{Map, :update!, [map, _, _], _} | _] -> map
|
||||
[{:maps, :update, [_, _, map], _} | _] -> map
|
||||
[{:maps, :get, [_, map], _} | _] -> map
|
||||
[{:erlang, :map_get, [_, map], _} | _] -> map
|
||||
_ -> nil
|
||||
end
|
||||
%KeyError{key: key, term: term}
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Erlang errors are bare atoms/tuples. Elixir provides rich context (the map that was accessed, the function that was called). The stacktrace inspection to extract `term` from `{:badkey, key}` errors is a particularly clever pattern — it looks at what function was on top of the stack to find the map argument.
|
||||
|
||||
---
|
||||
|
||||
## 10. `blame/2` Callback: Enriching Exceptions After the Fact
|
||||
|
||||
**Source:** `lib/elixir/lib/exception.ex` lines 2200–2215 (KeyError.blame)
|
||||
|
||||
**What it does:** The optional `blame/2` callback enriches an exception with additional context that's expensive to compute (like "did you mean?" suggestions).
|
||||
|
||||
```elixir
|
||||
@impl true
|
||||
def blame(exception, stacktrace) do
|
||||
%{term: term, key: key} = exception
|
||||
message = message(key, term)
|
||||
|
||||
if is_atom(key) and (map_with_atom_keys_only?(term) or Keyword.keyword?(term)) do
|
||||
hint = did_you_mean(key, available_keys(term))
|
||||
message = message <> IO.iodata_to_binary(hint)
|
||||
{%{exception | message: message}, stacktrace}
|
||||
else
|
||||
{%{exception | message: message}, stacktrace}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Computing string distance for "did you mean?" is expensive. It only makes sense when displaying the error to a human (in IEx, crash reports). The `blame/2` callback is called lazily by `Exception.blame/3`, not on every raise. This keeps the hot path fast.
|
||||
|
||||
---
|
||||
|
||||
## 11. Guards for Type Dispatch in Error Handling
|
||||
|
||||
**Source:** `lib/elixir/lib/exception.ex` lines 2530–2550, `lib/elixir/lib/map.ex` lines 586–594
|
||||
|
||||
**What it does:** Guards (`when is_map(term)`, `when is_list(term)`) dispatch to different error handling or normalization logic without using conditionals.
|
||||
|
||||
```elixir
|
||||
# Keyword.pop_first — uses :lists.keytake, pattern matches the result
|
||||
def pop_first(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keytake(key, 1, keywords) do
|
||||
{:value, {^key, value}, rest} -> {value, rest}
|
||||
false -> {default, keywords}
|
||||
end
|
||||
end
|
||||
|
||||
# ErlangError.normalize — pattern matches erlang error shapes
|
||||
def normalize({:badkey, key, map}, _stacktrace) when is_map(map) do
|
||||
%KeyError{key: key, term: map}
|
||||
end
|
||||
|
||||
def normalize({:badkey, key, term}, _stacktrace) do
|
||||
# non-map term gets a more detailed message
|
||||
message = "key #{inspect(key)} not found in: #{inspect(term)}..."
|
||||
%KeyError{key: key, term: term, message: message}
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The guard `when is_map(map)` lets the BEAM dispatch directly to the right clause without entering the function body. When the same error has different handling based on the term type, guards make each path explicit and independently testable.
|
||||
|
||||
---
|
||||
|
||||
## 12. The `:error` / `{:error, reason}` Convention Split
|
||||
|
||||
**Source:** `lib/elixir/lib/map.ex` (Map.fetch → `:error`), `lib/elixir/lib/exception.ex` lines 2695–2720 (`error_info` → `{:ok, ...} | :error`)
|
||||
|
||||
**What it does:** Elixir has two error return conventions:
|
||||
1. **`:error`** alone — when there's only one failure mode (Map.fetch, Access)
|
||||
2. **`{:error, reason}`** — when the caller needs to distinguish failure modes (File.read, GenServer)
|
||||
|
||||
```elixir
|
||||
# Convention 1: bare :error (only one failure mode possible)
|
||||
@spec fetch(map, key) :: {:ok, value} | :error
|
||||
def fetch(map, key), do: :maps.find(key, map)
|
||||
|
||||
# Convention 2: {:error, reason} (multiple failure modes)
|
||||
# From File module (not shown in source but referenced):
|
||||
@spec read(Path.t()) :: {:ok, binary} | {:error, posix}
|
||||
```
|
||||
|
||||
**Why:** The convention matches information content. If you can't add information to a bare `:error`, don't wrap it in a tuple. If the reason matters (`:enoent` vs `:eacces`), use the tuple form. The `with` macro works elegantly with both:
|
||||
|
||||
```elixir
|
||||
# Bare :error falls through with as the unmatched value
|
||||
with {:ok, val} <- Map.fetch(map, :key), do: val
|
||||
# Returns :error
|
||||
|
||||
# Tuple error falls through with its reason intact
|
||||
with {:ok, content} <- File.read(path), do: content
|
||||
# Returns {:error, :enoent}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. `reduce_while` — Early Exit Without Exceptions
|
||||
|
||||
**Source:** `lib/elixir/lib/enum.ex` lines 2660–2676
|
||||
|
||||
**What it does:** `reduce_while` uses `{:cont, acc}` / `{:halt, acc}` tuples as the reducer's return value to signal continuation or early termination.
|
||||
|
||||
```elixir
|
||||
@spec reduce_while(t, any, (element, any -> {:cont, any} | {:halt, any})) :: any
|
||||
def reduce_while(enumerable, acc, fun) do
|
||||
Enumerable.reduce(enumerable, {:cont, acc}, fun) |> elem(1)
|
||||
end
|
||||
|
||||
# Usage (from docs):
|
||||
Enum.reduce_while(1..100, 0, fn x, acc ->
|
||||
if x < 3, do: {:cont, acc + x}, else: {:halt, acc}
|
||||
end)
|
||||
#=> 3
|
||||
```
|
||||
|
||||
**Why:** This is control flow without exceptions. The tagged tuples `{:cont, ...}` and `{:halt, ...}` communicate intent to the enumeration machinery. This is more efficient than `throw`/`catch` for expected early exits and more composable than exceptions.
|
||||
|
||||
**Anti-pattern:** Using exceptions for expected early termination:
|
||||
```elixir
|
||||
# BAD — exceptions for flow control
|
||||
try do
|
||||
Enum.reduce(1..100, 0, fn x, acc ->
|
||||
if x >= 3, do: throw(acc), else: acc + x
|
||||
end)
|
||||
catch
|
||||
:throw, value -> value
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Three-Tier Error Strategy in Map Operations
|
||||
|
||||
**Source:** `lib/elixir/lib/map.ex` lines 290–430
|
||||
|
||||
**What it does:** Map provides three variants for key operations, each with different error semantics:
|
||||
|
||||
| Function | Missing Key Behavior | Use Case |
|
||||
|----------|---------------------|----------|
|
||||
| `Map.get/3` | Returns default | Optional keys, config with fallbacks |
|
||||
| `Map.fetch/2` | Returns `:error` | Composable error handling, `with` chains |
|
||||
| `Map.fetch!/2` | Raises `KeyError` | Assertions, keys guaranteed to exist |
|
||||
|
||||
```elixir
|
||||
# Tier 1: Silent default (line 586)
|
||||
def get(map, key, default \\ nil) # → value | default
|
||||
|
||||
# Tier 2: Error value (line 290)
|
||||
def fetch(map, key) # → {:ok, value} | :error
|
||||
|
||||
# Tier 3: Raise (line 311)
|
||||
def fetch!(map, key) # → value | raise KeyError
|
||||
```
|
||||
|
||||
**Why:** Different call sites have different error handling needs. Library code that composes operations uses `fetch`. Application code with known preconditions uses `fetch!`. UI/config code that needs defaults uses `get`. The three-tier pattern provides the right tool for each situation.
|
||||
|
||||
**Anti-pattern:** Only providing the bang version and forcing callers to rescue:
|
||||
```elixir
|
||||
# BAD — only one option, forces try/rescue for non-exceptional cases
|
||||
def get_config(key) do
|
||||
Map.fetch!(config, key)
|
||||
end
|
||||
```
|
||||
@@ -0,0 +1,415 @@
|
||||
# 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
|
||||
```
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
# Macros Patterns
|
||||
|
||||
Patterns extracted from the Elixir standard library source code.
|
||||
|
||||
---
|
||||
|
||||
## 1. Context-Aware Macros (__CALLER__.context)
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 2032–2067 (or/and operators)
|
||||
|
||||
**What it does:** Macros check `__CALLER__.context` to determine if they're being invoked in a guard, match, or normal context, and generate different code accordingly.
|
||||
|
||||
**Why:** Elixir guards have restricted syntax (no function calls, only BIFs). A macro like `or` must emit `:erlang.orelse/2` in guards but can use a `case` expression in normal code. Context-awareness lets one macro serve multiple contexts correctly.
|
||||
|
||||
**Anti-pattern:** Writing macros that only work in one context and crash confusingly in others, or ignoring guard context entirely.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
defmacro left or right do
|
||||
case __CALLER__.context do
|
||||
nil -> build_boolean_check(:or, left, true, right)
|
||||
:match -> invalid_match!(:or)
|
||||
:guard -> quote(do: :erlang.orelse(unquote(left), unquote(right)))
|
||||
end
|
||||
end
|
||||
|
||||
defmacro left and right do
|
||||
case __CALLER__.context do
|
||||
nil -> build_boolean_check(:and, left, right, false)
|
||||
:match -> invalid_match!(:and)
|
||||
:guard -> quote(do: :erlang.andalso(unquote(left), unquote(right)))
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. defguard — Macro for Guard-Safe Expressions
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 5889–5966
|
||||
|
||||
**What it does:** `defguard` creates a public macro that the compiler verifies is valid in guard clauses. It raises at compile time if the guard body uses non-guard-safe expressions.
|
||||
|
||||
**Why:** Regular macros have no guard validation. `defguard` provides compile-time safety: you can't accidentally create a "guard" that uses `IO.puts` or `Enum.map`. The result can be used both in guards and normal code.
|
||||
|
||||
**Anti-pattern:** Defining guard-like functions with `defmacro` and no validation. Users will discover at runtime (or never) that the macro isn't guard-safe.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
defmodule Integer.Guards do
|
||||
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
|
||||
end
|
||||
|
||||
defmodule Collatz do
|
||||
import Integer.Guards
|
||||
|
||||
def converge(n) when n > 0, do: step(n, 0)
|
||||
|
||||
defp step(1, step_count), do: step_count
|
||||
|
||||
defp step(n, step_count) when is_even(n) do
|
||||
step(div(n, 2), step_count + 1)
|
||||
end
|
||||
|
||||
defp step(n, step_count) do
|
||||
step(3 * n + 1, step_count + 1)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. quote + unquote for Code Generation
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 5624–5640 (defstruct)
|
||||
|
||||
**What it does:** `quote bind_quoted: [fields: fields]` captures the macro argument into a variable available inside the quoted block. `unquote` injects computed values back into the AST.
|
||||
|
||||
**Why:** `bind_quoted` is preferred over raw `unquote` for macro arguments because it evaluates the expression exactly once and binds it to a variable. This prevents double-evaluation bugs and makes the generated code clearer.
|
||||
|
||||
**Anti-pattern:** Using `unquote(expr)` multiple times in a quote block when `expr` has side effects or is expensive — it will be evaluated multiple times in the expansion.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
defmacro defstruct(fields) do
|
||||
quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do
|
||||
{struct, derive, escaped_struct, kv, body} =
|
||||
Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__)
|
||||
|
||||
case derive do
|
||||
[] -> :ok
|
||||
_ -> Protocol.__derive__(derive, __MODULE__, __ENV__)
|
||||
end
|
||||
|
||||
def __struct__(), do: unquote(escaped_struct)
|
||||
def __struct__(unquote(kv)), do: unquote(body)
|
||||
|
||||
Kernel.Utils.announce_struct(__MODULE__)
|
||||
struct
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. var! for Breaking Hygiene
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 4884–4901
|
||||
|
||||
**What it does:** `var!` marks a variable inside `quote` as unhygienic — it will refer to the variable in the *caller's* scope rather than creating a new hygienic binding.
|
||||
|
||||
**Why:** Macro hygiene prevents accidental variable capture. But sometimes you *want* to reference the caller's variables (e.g., `Kernel.var!(example) = 1` in tests, or injecting into module scope). `var!` is the explicit escape hatch.
|
||||
|
||||
**Anti-pattern:** Using `var!` casually. Every use breaks hygiene and creates implicit coupling between macro and caller. Prefer passing values through macro arguments instead.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
defmacro var!(var, context \\ nil)
|
||||
|
||||
defmacro var!({name, meta, atom}, context) when is_atom(name) and is_atom(atom) do
|
||||
# Remove counter and force them to be vars
|
||||
meta = :lists.keydelete(:counter, 1, meta)
|
||||
meta = :lists.keystore(:if_undefined, 1, meta, {:if_undefined, :raise})
|
||||
|
||||
case Macro.expand(context, __CALLER__) do
|
||||
context when is_atom(context) ->
|
||||
{name, meta, context}
|
||||
|
||||
other ->
|
||||
raise ArgumentError,
|
||||
"expected var! context to expand to an atom, got: #{Macro.to_string(other)}"
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Macro Expanding with Macro.expand
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 2246–2273 (raise), 2319–2340 (reraise)
|
||||
|
||||
**What it does:** Before generating code, the macro calls `Macro.expand(message, __CALLER__)` to resolve aliases at compile time. This determines the code path: if the expanded value is an atom (module name), it generates exception-specific code.
|
||||
|
||||
**Why:** Macros receive AST, not values. An alias like `MyError` is `{:__aliases__, _, [:MyError]}` in AST form. Expanding it resolves it to the actual module atom, enabling compile-time decisions about what code to generate.
|
||||
|
||||
**Anti-pattern:** Pattern-matching on raw AST shapes without expanding first. This breaks when users pass aliases, module attributes, or other compile-time expressions.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
defmacro raise(message) do
|
||||
erlang_error = fn expr ->
|
||||
quote do: :erlang.error(unquote(expr), :none, error_info: %{module: Exception})
|
||||
end
|
||||
|
||||
case Macro.expand(message, __CALLER__) do
|
||||
atom when is_atom(atom) ->
|
||||
# It's a module — generate Module.exception([])
|
||||
erlang_error.(quote do: unquote(atom).exception([]))
|
||||
|
||||
_ ->
|
||||
# It's a string or expression — wrap in RuntimeError
|
||||
erlang_error.(quote do: RuntimeError.exception(unquote(message)))
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. assert_no_match_or_guard_scope Pattern
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 5384–5385 (def), 5415–5416 (defp), 5444–5445 (defmacro)
|
||||
|
||||
**What it does:** Macros that define module-level constructs (def, defp, defmacro, defmacrop) immediately assert they're not being called inside a guard or match context.
|
||||
|
||||
**Why:** Calling `def` inside a guard clause makes no sense but would produce a confusing error much later. Failing early with a clear message ("cannot invoke def/2 inside a guard") is better than a cryptic expansion error.
|
||||
|
||||
**Anti-pattern:** Not validating context at the top of macros that are context-sensitive. Let errors surface at the point of misuse, not deep in expansion.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
defmacro def(call, expr \\ nil) do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "def/2")
|
||||
define(:def, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
defmacro defmacro(call, expr \\ nil) do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "defmacro/2")
|
||||
define(:defmacro, call, expr, __CALLER__)
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Protocol Definition as a Macro (defprotocol)
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 5734–5745, `lib/elixir/lib/protocol.ex` lines 290–318
|
||||
|
||||
**What it does:** `defprotocol` is a macro that creates a module with auto-generated dispatch functions. Inside the protocol, `def` is redefined as a macro that generates both a callback spec and the dispatch implementation.
|
||||
|
||||
**Why:** Protocols need complex machinery: type dispatch, consolidation, fallback handling. Wrapping this in macros means users write simple `defprotocol` + `def` syntax while the system generates efficient dispatch code.
|
||||
|
||||
**Anti-pattern:** Trying to implement protocol-like dispatch with regular modules and manual case statements. Use `defprotocol` — it handles consolidation, error messages, and type dispatch.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
# What the user writes:
|
||||
defprotocol Size do
|
||||
@doc "Calculates the size of a data structure"
|
||||
def size(data)
|
||||
end
|
||||
|
||||
# What the protocol's def macro generates internally:
|
||||
quote generated: true do
|
||||
@__functions__ [{name, arity} | @__functions__]
|
||||
|
||||
# Generate a fake definition with the user signature (for docs)
|
||||
Kernel.def(unquote(name)(unquote_splicing(args)))
|
||||
|
||||
# Generate the actual dispatch implementation
|
||||
Kernel.def unquote(name)(unquote_splicing(call_args)) do
|
||||
impl_for!(term).unquote(name)(unquote_splicing(call_args))
|
||||
end
|
||||
|
||||
# Copy spec as callback
|
||||
Module.spec_to_callback(__MODULE__, {name, arity}) ||
|
||||
@callback unquote(name)(unquote_splicing(type_args)) :: term
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. @fallback_to_any in Protocols
|
||||
|
||||
**Source:** `lib/elixir/lib/inspect.ex` line 162, `lib/elixir/lib/protocol.ex` lines 115–131
|
||||
|
||||
**What it does:** Sets `@fallback_to_any true` inside a protocol definition to enable a default implementation via `defimpl Protocol, for: Any`.
|
||||
|
||||
**Why:** Some protocols (like `Inspect`) should work on *any* value rather than raising. The fallback provides a reasonable default (e.g., inspecting structs generically) while still allowing specific implementations to override.
|
||||
|
||||
**Anti-pattern:** Using `@fallback_to_any true` when failing explicitly is better. As the docs say: "it makes no sense to say a PID has a size of 0." Only use fallbacks when a generic implementation is genuinely useful.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
defprotocol Inspect do
|
||||
@moduledoc """
|
||||
The `Inspect` protocol converts an Elixir data structure into an
|
||||
algebra document.
|
||||
"""
|
||||
|
||||
# Handle structs in Any
|
||||
@fallback_to_any true
|
||||
|
||||
@spec inspect(t, Inspect.Opts.t()) ::
|
||||
Inspect.Algebra.t() | {Inspect.Algebra.t(), Inspect.Opts.t()}
|
||||
def inspect(term, opts)
|
||||
end
|
||||
|
||||
# The fallback implementation:
|
||||
defimpl Inspect, for: Any do
|
||||
# Generic struct inspection using #ModuleName<...> notation
|
||||
def inspect(%module{} = struct, opts) do
|
||||
# ...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. use/2 as Macro Injection Point
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 6130–6145
|
||||
|
||||
**What it does:** `use Module, opts` is a macro that `require`s the module then calls `Module.__using__(opts)`. The `__using__/1` macro returns quoted code injected into the caller.
|
||||
|
||||
**Why:** This is Elixir's extension/plugin mechanism. It's explicit (you can see what `use` does by reading `__using__/1`), composable (multiple `use` calls stack), and documented (the admonition convention).
|
||||
|
||||
**Anti-pattern:** Using `use` when `import` or `alias` would suffice. `use` should be reserved for cases that need module attributes, callbacks, or compile hooks.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
# The implementation of use/2:
|
||||
defmacro use(module, opts \\ []) do
|
||||
calls =
|
||||
Enum.map(expand_aliases(module, __CALLER__), fn
|
||||
expanded when is_atom(expanded) ->
|
||||
quote do
|
||||
require unquote(expanded)
|
||||
unquote(expanded).__using__(unquote(opts))
|
||||
end
|
||||
end)
|
||||
|
||||
quote(do: (unquote_splicing(calls)))
|
||||
end
|
||||
|
||||
# A typical __using__ implementation:
|
||||
defmodule GenServer do
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
@behaviour GenServer
|
||||
|
||||
def child_spec(init_arg) do
|
||||
# ...default child spec...
|
||||
end
|
||||
|
||||
defoverridable child_spec: 1
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Sigil Macros (Pattern for DSL Literals)
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 6500–6850+ (sigil_S, sigil_s, sigil_r, sigil_D, etc.)
|
||||
|
||||
**What it does:** Each sigil (`~r`, `~D`, `~s`, etc.) is implemented as a `defmacro sigil_X(term, modifiers)` that receives the raw string content and modifier characters, then transforms them at compile time.
|
||||
|
||||
**Why:** Sigils provide compile-time validated literals. `~D[2024-01-15]` is parsed and validated during compilation — invalid dates won't even compile. The macro pattern means new sigils can be added by any module.
|
||||
|
||||
**Anti-pattern:** Parsing literal values at runtime when they're known at compile time. Sigils shift validation left to compilation.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
defmacro sigil_D(date_string, modifiers)
|
||||
|
||||
defmacro sigil_D({:<<>>, _, [string]}, []) do
|
||||
# Parses and validates at compile time
|
||||
{{:., _, [Date, :sigil_D]}, _, [{:<<>>, _, [string]}, []]}
|
||||
end
|
||||
|
||||
# Usage:
|
||||
date = ~D[2024-01-15] # Compile-time validated Date struct
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Pipe Operator as a Macro
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` line 4509
|
||||
|
||||
**What it does:** The `|>` pipe operator is a macro that rewrites `left |> right` into `right(left)`, inserting the left expression as the first argument of the right expression.
|
||||
|
||||
**Why:** It's purely syntactic transformation — there's no runtime dispatch. Being a macro means it's zero-cost at runtime while providing the ergonomic left-to-right reading order.
|
||||
|
||||
**Anti-pattern:** Implementing operator-like syntax as runtime function calls when they could be compile-time transformations.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
defmacro left |> right do
|
||||
[{h, _} | t] = Macro.unpipe({:|>, [], [left, right]})
|
||||
|
||||
fun = fn {x, pos}, acc ->
|
||||
Macro.pipe(acc, x, pos)
|
||||
end
|
||||
|
||||
:lists.foldl(fun, h, t)
|
||||
end
|
||||
|
||||
# Transforms at compile time:
|
||||
# "hello" |> String.upcase() |> String.reverse()
|
||||
# becomes:
|
||||
# String.reverse(String.upcase("hello"))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Macro.generate_unique_arguments for Hygiene
|
||||
|
||||
**Source:** `lib/elixir/lib/macro.ex` lines 507–520
|
||||
|
||||
**What it does:** `Macro.generate_unique_arguments(n, context)` creates `n` unique variable AST nodes that won't conflict with any user variables.
|
||||
|
||||
**Why:** When a macro needs to generate variable bindings in quoted code, using `generate_unique_arguments` guarantees hygiene. The variables get unique counters that can't clash with user-defined names.
|
||||
|
||||
**Anti-pattern:** Using hardcoded variable names in macros (like `x`, `acc`, `state`) which can shadow or be shadowed by user variables.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@doc """
|
||||
Generates a list of `n` unique arguments.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> [var1, var2] = Macro.generate_unique_arguments(2, __CALLER__.module)
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@spec generate_unique_arguments(0, context :: atom) :: []
|
||||
@spec generate_unique_arguments(pos_integer, context) ::
|
||||
[{atom, [counter: integer], context}, ...]
|
||||
when context: atom
|
||||
```
|
||||
@@ -0,0 +1,195 @@
|
||||
# Module Organization Patterns
|
||||
|
||||
How modules are structured, named, and organized in Elixir core and Phoenix.
|
||||
|
||||
## 1. One Module per Concept, Nested for Sub-Concepts
|
||||
|
||||
**Source:** `lib/elixir/lib/` directory structure
|
||||
|
||||
```
|
||||
gen_server.ex — GenServer (the main module)
|
||||
task.ex — Task (the main module)
|
||||
task/supervised.ex — Task.Supervised (internal implementation)
|
||||
supervisor.ex — Supervisor (the main module)
|
||||
```
|
||||
|
||||
**Source:** `lib/phoenix/` directory structure
|
||||
|
||||
```
|
||||
channel.ex — Phoenix.Channel
|
||||
channel/ — Phoenix.Channel.* submodules
|
||||
server.ex — Phoenix.Channel.Server
|
||||
router.ex — Phoenix.Router
|
||||
router/ — Phoenix.Router.* submodules
|
||||
route.ex — Phoenix.Router.Route
|
||||
scope.ex — Phoenix.Router.Scope
|
||||
resource.ex — Phoenix.Router.Resource
|
||||
endpoint.ex — Phoenix.Endpoint
|
||||
endpoint/ — Phoenix.Endpoint.* submodules
|
||||
supervisor.ex — Phoenix.Endpoint.Supervisor
|
||||
```
|
||||
|
||||
**Why:** The parent module is the public API. Submodules handle implementation details. File layout mirrors module hierarchy — `Phoenix.Router.Route` lives in `router/route.ex`.
|
||||
|
||||
**Anti-pattern:** Putting all modules in a flat directory, or nesting too deeply (more than 3 levels is usually a sign of over-engineering).
|
||||
|
||||
---
|
||||
|
||||
## 2. Public API at the Top, Private Functions at the Bottom
|
||||
|
||||
**Source:** `lib/elixir/lib/agent.ex` (full module structure)
|
||||
|
||||
```elixir
|
||||
defmodule Agent do
|
||||
@moduledoc "..."
|
||||
|
||||
# Types
|
||||
@type on_start :: ...
|
||||
@type name :: ...
|
||||
@type agent :: ...
|
||||
@type state :: ...
|
||||
|
||||
# child_spec (for supervisors)
|
||||
def child_spec(arg) do ... end
|
||||
|
||||
# __using__ macro
|
||||
defmacro __using__(opts) do ... end
|
||||
|
||||
# Public API
|
||||
def start_link(fun, options \\ []) do ... end
|
||||
def start(fun, options \\ []) do ... end
|
||||
def get(agent, fun, timeout \\ 5000) do ... end
|
||||
def get_and_update(agent, fun, timeout \\ 5000) do ... end
|
||||
def update(agent, fun, timeout \\ 5000) do ... end
|
||||
def cast(agent, fun) do ... end
|
||||
def stop(agent, reason \\ :normal, timeout \\ :infinity) do ... end
|
||||
end
|
||||
```
|
||||
|
||||
The order:
|
||||
1. `@moduledoc`
|
||||
2. Types
|
||||
3. `child_spec` / `__using__`
|
||||
4. `start_link` / `start` (lifecycle)
|
||||
5. Public API functions (alphabetical or logical grouping)
|
||||
6. `stop` (lifecycle end)
|
||||
|
||||
**Anti-pattern:** Mixing private helpers between public functions, or putting `start_link` at the bottom where supervisors have to hunt for it.
|
||||
|
||||
---
|
||||
|
||||
## 3. `@moduledoc false` for Internal Modules
|
||||
|
||||
**Source:** `lib/phoenix/router/route.ex:5-7`
|
||||
|
||||
```elixir
|
||||
# This module defines the Route struct that is used
|
||||
# throughout Phoenix's router. This struct is private
|
||||
# as it contains internal routing information.
|
||||
@moduledoc false
|
||||
```
|
||||
|
||||
**Why:** Internal modules that exist for code organization but aren't part of the public API get `@moduledoc false`. They won't appear in generated documentation.
|
||||
|
||||
**Anti-pattern:** Documenting internal modules and confusing users about what's public API vs implementation detail.
|
||||
|
||||
---
|
||||
|
||||
## 4. Struct Definition Conventions
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:279-296`
|
||||
|
||||
```elixir
|
||||
@enforce_keys [:mfa, :owner, :pid, :ref]
|
||||
defstruct @enforce_keys
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
mfa: mfa(),
|
||||
owner: pid(),
|
||||
pid: pid() | nil,
|
||||
ref: ref()
|
||||
}
|
||||
```
|
||||
|
||||
**Source:** `lib/phoenix/router/route.ex:30-46`
|
||||
|
||||
```elixir
|
||||
defstruct [
|
||||
:verb,
|
||||
:line,
|
||||
:kind,
|
||||
:path,
|
||||
:hosts,
|
||||
:plug,
|
||||
:plug_opts,
|
||||
:helper,
|
||||
:private,
|
||||
:pipe_through,
|
||||
:assigns,
|
||||
:metadata,
|
||||
:trailing_slash?,
|
||||
:warn_on_verify?
|
||||
]
|
||||
|
||||
@type t :: %Route{}
|
||||
```
|
||||
|
||||
Two patterns:
|
||||
1. **Task:** `@enforce_keys` + minimal struct — all fields required, enforced at creation
|
||||
2. **Route:** All optional fields listed — flexible construction
|
||||
|
||||
**Why:** Use `@enforce_keys` when a struct is meaningless without certain fields. Omit it for structs built incrementally.
|
||||
|
||||
**Anti-pattern:** Never using `@enforce_keys` — allows creating invalid structs that crash later when a required field is `nil`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Selective Imports in `__using__`
|
||||
|
||||
**Source:** `lib/phoenix/channel.ex:463-464`
|
||||
|
||||
```elixir
|
||||
import unquote(__MODULE__)
|
||||
import Phoenix.Socket, only: [assign: 3, assign: 2]
|
||||
```
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:271-275`
|
||||
|
||||
```elixir
|
||||
import Phoenix.Router
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller
|
||||
```
|
||||
|
||||
**Why:** The `use` macro sets up the module's environment — importing the functions you'll need. Phoenix.Channel imports `assign` from Socket because channels work with sockets constantly.
|
||||
|
||||
**Anti-pattern:** Importing everything without restriction — namespace pollution and hard-to-trace function origins.
|
||||
|
||||
---
|
||||
|
||||
## 6. Alias at Module Scope for Readability
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:268`
|
||||
|
||||
```elixir
|
||||
alias Phoenix.Router.{Resource, Scope, Route, Helpers}
|
||||
```
|
||||
|
||||
**Why:** Multi-alias reduces repetition and groups related modules. The curly-brace syntax makes it clear these all share a parent namespace.
|
||||
|
||||
**Anti-pattern:** Using full module paths everywhere (`Phoenix.Router.Resource.new(...)`) — verbose and hard to read.
|
||||
|
||||
---
|
||||
|
||||
## 7. Boolean-Suffixed Fields in Structs
|
||||
|
||||
**Source:** `lib/phoenix/router/route.ex:43-44`
|
||||
|
||||
```elixir
|
||||
:trailing_slash?,
|
||||
:warn_on_verify?
|
||||
```
|
||||
|
||||
**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.
|
||||
@@ -0,0 +1,538 @@
|
||||
# Process Design Patterns — From the Elixir Source
|
||||
|
||||
Analysis of `lib/elixir/lib/supervisor.ex`, `lib/elixir/lib/dynamic_supervisor.ex`, `lib/elixir/lib/task.ex`, `lib/elixir/lib/task/supervisor.ex`, `lib/elixir/lib/process.ex`, and `lib/elixir/lib/registry.ex`.
|
||||
|
||||
---
|
||||
|
||||
## Pattern 1: Static vs Dynamic Supervision — Choose the Right Tool
|
||||
|
||||
**Source:** `lib/elixir/lib/supervisor.ex:1-20` vs `lib/elixir/lib/dynamic_supervisor.ex:1-20`
|
||||
|
||||
**What it does:** Elixir provides two distinct supervisor types:
|
||||
- `Supervisor` — for **static** children known at compile time, started in a defined order
|
||||
- `DynamicSupervisor` — for children started **on demand** at runtime, with no ordering guarantees
|
||||
|
||||
**Why:** Static supervisors guarantee startup order (critical for dependencies like "DB pool must start before web server"). Dynamic supervisors optimize for scale — they can hold millions of children using efficient data structures and shut down concurrently.
|
||||
|
||||
**Anti-pattern:** Using a `Supervisor` when children are created dynamically (e.g., one process per WebSocket connection). You'll hit performance issues and ordering semantics you don't need. Conversely, using `DynamicSupervisor` for fixed infrastructure (DB pool, PubSub) loses startup order guarantees.
|
||||
|
||||
**Code example from source (dynamic_supervisor.ex:1-15):**
|
||||
```elixir
|
||||
# DynamicSupervisor docs explain the distinction:
|
||||
# "The Supervisor module was designed to handle mostly static children
|
||||
# that are started in the given order when the supervisor starts. A
|
||||
# DynamicSupervisor starts with no children. Instead, children are
|
||||
# started on demand via start_child/2 and there is no ordering between
|
||||
# children."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2: PartitionSupervisor for Scalability
|
||||
|
||||
**Source:** `lib/elixir/lib/dynamic_supervisor.ex:60-95` and `lib/elixir/lib/task/supervisor.ex:35-65`
|
||||
|
||||
**What it does:** Both `DynamicSupervisor` and `Task.Supervisor` document the same scalability pattern: when a single supervisor becomes a bottleneck, wrap it in a `PartitionSupervisor` which starts N instances (one per core by default) and routes via a key.
|
||||
|
||||
**Why:** A supervisor is a single process. Under heavy `start_child` load, it serializes all spawn operations. PartitionSupervisor distributes the load across multiple supervisor processes, using `self()` as the routing key to ensure each caller consistently hits the same partition.
|
||||
|
||||
**Anti-pattern:** Creating your own load-balancing logic for supervisors, or just accepting the bottleneck. The standard library provides this pattern explicitly.
|
||||
|
||||
**Code example from source (dynamic_supervisor.ex):**
|
||||
```elixir
|
||||
# Instead of a single DynamicSupervisor:
|
||||
children = [
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor,
|
||||
name: MyApp.DynamicSupervisors}
|
||||
]
|
||||
|
||||
# Start children through the partition supervisor:
|
||||
DynamicSupervisor.start_child(
|
||||
{:via, PartitionSupervisor, {MyApp.DynamicSupervisors, self()}},
|
||||
{Counter, 0}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 3: Supervision Strategies — Choosing the Right Restart Behavior
|
||||
|
||||
**Source:** `lib/elixir/lib/supervisor.ex:315-345` (Strategies section)
|
||||
|
||||
**What it does:** Three strategies model three dependency patterns:
|
||||
- `:one_for_one` — independent children (crash of A doesn't affect B)
|
||||
- `:one_for_all` — tightly coupled children (if one fails, all state is inconsistent)
|
||||
- `:rest_for_one` — sequential dependencies (children started after the crashed one depend on it)
|
||||
|
||||
**Why:** These map directly to runtime dependency graphs. A connection pool and its consumers are `:rest_for_one` — consumers can't work without the pool. Multiple independent request handlers are `:one_for_one`. Workers sharing a cache are `:one_for_all` — stale cache state after a crash could cause inconsistency.
|
||||
|
||||
**Anti-pattern:** Defaulting to `:one_for_one` everywhere without thinking about dependencies. If process B depends on process A's state and A crashes, B will be working with stale assumptions.
|
||||
|
||||
**Code example from source (supervisor.ex docs):**
|
||||
```elixir
|
||||
# Independent workers — one crash doesn't affect others
|
||||
Supervisor.start_link(children, strategy: :one_for_one)
|
||||
|
||||
# Tightly coupled — all must restart together for consistency
|
||||
Supervisor.start_link(children, strategy: :one_for_all)
|
||||
|
||||
# Sequential dependency — later children depend on earlier ones
|
||||
Supervisor.start_link(children, strategy: :rest_for_one)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 4: Restart Intensity (`max_restarts` / `max_seconds`)
|
||||
|
||||
**Source:** `lib/elixir/lib/supervisor.ex:309-313`, `lib/elixir/lib/dynamic_supervisor.ex:730-758` (implementation)
|
||||
|
||||
**What it does:** Supervisors track restart frequency. If a child exceeds `max_restarts` within `max_seconds`, the supervisor itself shuts down (escalating the failure to its parent). Defaults: 3 restarts in 5 seconds.
|
||||
|
||||
**Why:** Prevents infinite restart loops that waste CPU and mask bugs. If a child keeps crashing within seconds, it's a systemic problem that the current supervisor level can't fix. Escalating to the parent allows a higher-level strategy to respond (perhaps restarting the entire subsystem with fresh state).
|
||||
|
||||
**Anti-pattern:** Setting `max_restarts` extremely high to "prevent crashes." This hides bugs and wastes resources. Let supervisors escalate — that's the point of the hierarchy.
|
||||
|
||||
**Code example from source (dynamic_supervisor.ex internal logic):**
|
||||
```elixir
|
||||
defp add_restart(state) do
|
||||
%{max_seconds: max_seconds, max_restarts: max_restarts, restarts: restarts} = state
|
||||
|
||||
now = :erlang.monotonic_time(1)
|
||||
restarts = add_restart([now | restarts], now, max_seconds)
|
||||
state = %{state | restarts: restarts}
|
||||
|
||||
if length(restarts) <= max_restarts do
|
||||
{:ok, state}
|
||||
else
|
||||
{:shutdown, state}
|
||||
end
|
||||
end
|
||||
|
||||
defp add_restart(restarts, now, period) do
|
||||
for then <- restarts, now <= then + period, do: then
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 5: Restart Values — `:permanent` vs `:transient` vs `:temporary`
|
||||
|
||||
**Source:** `lib/elixir/lib/supervisor.ex:130-152` (Restart values section)
|
||||
|
||||
**What it does:** Three restart policies control what happens when a child terminates:
|
||||
- `:permanent` — always restart (default for GenServer/Agent/Supervisor)
|
||||
- `:transient` — restart only on abnormal exit (not `:normal`, `:shutdown`, `{:shutdown, term}`)
|
||||
- `:temporary` — never restart (default for Task)
|
||||
|
||||
**Why:** Different processes have different lifecycle expectations. A database pool should always be running (`:permanent`). A task that computes a value and exits is done when it's done (`:temporary`). A connection process should restart on crash but not on graceful disconnect (`:transient`).
|
||||
|
||||
**Anti-pattern:** Making everything `:permanent`. If a one-shot task keeps restarting, it'll trigger restart intensity limits and take down the supervisor.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
# Task defaults to :temporary — intentional one-shot work
|
||||
# (from task.ex:282)
|
||||
def child_spec(arg) do
|
||||
%{
|
||||
id: Task,
|
||||
start: {Task, :start_link, [arg]},
|
||||
restart: :temporary
|
||||
}
|
||||
end
|
||||
|
||||
# Customize via use:
|
||||
use GenServer, restart: :transient
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 6: Automatic Shutdown for Pipeline Supervisors
|
||||
|
||||
**Source:** `lib/elixir/lib/supervisor.ex:349-375` (Automatic shutdown section)
|
||||
|
||||
**What it does:** Supervisors support `:auto_shutdown` which terminates the supervisor when significant children exit. Options: `:any_significant` (first significant child exits → shutdown) or `:all_significant` (all significant children must exit → shutdown).
|
||||
|
||||
**Why:** Models pipeline/workflow patterns where a supervisor's purpose is tied to its children's work. If all significant workers finish, the supervisor should clean up. This is useful for batch processing supervisors or connection-scoped process groups.
|
||||
|
||||
**Anti-pattern:** Manually monitoring children and calling `Supervisor.stop/1`. The automatic shutdown mechanism handles this cleanly within OTP semantics.
|
||||
|
||||
**Code example (from docs):**
|
||||
```elixir
|
||||
# Only :transient and :temporary children can be marked significant
|
||||
children = [
|
||||
Supervisor.child_spec({BatchWorker, args}, significant: true, restart: :transient)
|
||||
]
|
||||
|
||||
Supervisor.start_link(children,
|
||||
strategy: :one_for_one,
|
||||
auto_shutdown: :all_significant
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 7: Task.async/await for Concurrent Value Computation
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:1-20` and `lib/elixir/lib/task.ex:300-340`
|
||||
|
||||
**What it does:** `Task.async` spawns a linked, monitored process and returns a `%Task{}` struct. `Task.await` blocks until the result arrives or times out. This is the canonical pattern for "compute a value concurrently."
|
||||
|
||||
**Why:** Tasks provide structured concurrency — the caller is linked to the task, so failures propagate naturally. The monitor reference enables safe await with timeout. This is explicit and composable unlike raw `spawn_link` + receive.
|
||||
|
||||
**Anti-pattern:** Using `spawn_link` + `send` + `receive` for one-shot concurrent computation. You lose: error propagation, structured monitoring, timeout handling, and the caller-tracking metadata that tasks provide.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
task = Task.async(fn -> do_some_work() end)
|
||||
res = do_some_other_work()
|
||||
res + Task.await(task)
|
||||
```
|
||||
|
||||
Key constraint from docs: "If you start an async, you **must await**. This is either done by calling `Task.await/2` or `Task.yield/2` followed by `Task.shutdown/2`."
|
||||
|
||||
---
|
||||
|
||||
## Pattern 8: Task.Supervisor.async_nolink for Fault-Tolerant Task Execution
|
||||
|
||||
**Source:** `lib/elixir/lib/task/supervisor.ex:240-320` (async_nolink docs with GenServer example)
|
||||
|
||||
**What it does:** Unlike `Task.async`, `async_nolink` spawns a task that is NOT linked to the caller. The caller monitors it and handles success/failure via `handle_info`. This prevents a task crash from killing the caller.
|
||||
|
||||
**Why:** In a GenServer, you often want to spawn work that might fail without taking down the server. The pattern: spawn with `async_nolink`, receive the result as `{ref, answer}`, and handle failure as `{:DOWN, ref, :process, _pid, reason}`.
|
||||
|
||||
**Anti-pattern:** Using `Task.async` inside a GenServer when the task might fail. The link means the GenServer crashes too. Use `async_nolink` + `handle_info` for resilient concurrent work.
|
||||
|
||||
**Code example from source (task/supervisor.ex):**
|
||||
```elixir
|
||||
defmodule MyApp.Server do
|
||||
use GenServer
|
||||
|
||||
def handle_call(:start_task, _from, %{ref: nil} = state) do
|
||||
task =
|
||||
Task.Supervisor.async_nolink(MyApp.TaskSupervisor, fn ->
|
||||
# potentially failing work
|
||||
end)
|
||||
|
||||
{:reply, :ok, %{state | ref: task.ref}}
|
||||
end
|
||||
|
||||
# Task completed successfully
|
||||
def handle_info({ref, answer}, %{ref: ref} = state) do
|
||||
Process.demonitor(ref, [:flush])
|
||||
{:noreply, %{state | ref: nil}}
|
||||
end
|
||||
|
||||
# Task failed
|
||||
def handle_info({:DOWN, ref, :process, _pid, _reason}, %{ref: ref} = state) do
|
||||
{:noreply, %{state | ref: nil}}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 9: Task Supervisor as DynamicSupervisor Specialization
|
||||
|
||||
**Source:** `lib/elixir/lib/task/supervisor.ex:151-165` (start_link implementation)
|
||||
|
||||
**What it does:** `Task.Supervisor` is implemented directly on top of `DynamicSupervisor`. It stores default restart/shutdown settings in the process dictionary and delegates `init` to `DynamicSupervisor.init`.
|
||||
|
||||
**Why:** Specialization without duplication. Task.Supervisor adds task-specific behavior (caller tracking, async/nolink patterns, stream support) on top of the generic dynamic supervision infrastructure. It's a compositional pattern — build specialized supervisors by wrapping the generic one.
|
||||
|
||||
**Anti-pattern:** Re-implementing task supervision from scratch with a plain DynamicSupervisor + custom start logic. Use Task.Supervisor — it handles caller tracking, owner propagation, and proper shutdown.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
# Task.Supervisor.start_link delegates to DynamicSupervisor
|
||||
def start_link(options \\ []) do
|
||||
{restart, options} = Keyword.pop(options, :restart)
|
||||
{shutdown, options} = Keyword.pop(options, :shutdown)
|
||||
keys = [:max_children, :max_seconds, :max_restarts]
|
||||
{sup_opts, start_opts} = Keyword.split(options, keys)
|
||||
restart_and_shutdown = {restart || :temporary, shutdown || 5000}
|
||||
DynamicSupervisor.start_link(__MODULE__, {restart_and_shutdown, sup_opts}, start_opts)
|
||||
end
|
||||
|
||||
def init({{_restart, _shutdown} = arg, options}) do
|
||||
Process.put(__MODULE__, arg)
|
||||
DynamicSupervisor.init([strategy: :one_for_one] ++ options)
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 10: Registry for Dynamic Process Naming and PubSub
|
||||
|
||||
**Source:** `lib/elixir/lib/registry.ex:1-70` (module docs), `lib/elixir/lib/registry.ex:250-270` (whereis_name via callbacks)
|
||||
|
||||
**What it does:** Registry provides two modes:
|
||||
- `:unique` keys — each key maps to exactly one process (name registry, process lookup)
|
||||
- `:duplicate` keys — each key maps to many processes (PubSub topics, event dispatch)
|
||||
|
||||
Processes are automatically unregistered on death. Registry integrates with GenServer naming via `{:via, Registry, {registry, key}}`.
|
||||
|
||||
**Why:** Solves the dynamic naming problem without atom leaks. Also provides local PubSub without external dependencies. The registry is partitioned for scalability and uses ETS for O(1) lookups.
|
||||
|
||||
**Anti-pattern:** Building custom ETS-based process registries with manual cleanup on process death. Registry handles monitor-based cleanup automatically.
|
||||
|
||||
**Code example from source (registry.ex :via callbacks):**
|
||||
```elixir
|
||||
# :via integration — GenServer uses these callbacks
|
||||
def whereis_name({registry, key}), do: whereis_name(registry, key)
|
||||
def whereis_name({registry, key, _value}), do: whereis_name(registry, key)
|
||||
|
||||
defp whereis_name(registry, key) do
|
||||
case key_info!(registry) do
|
||||
{:unique, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
case lookup_second(:unique, key_ets, key) do
|
||||
{pid, _} ->
|
||||
if Process.alive?(pid), do: pid, else: :undefined
|
||||
_ ->
|
||||
:undefined
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 11: Shutdown Semantics — Graceful Termination
|
||||
|
||||
**Source:** `lib/elixir/lib/supervisor.ex:156-192` (Shutdown values section)
|
||||
|
||||
**What it does:** Three shutdown modes:
|
||||
- `:brutal_kill` — immediate `Process.exit(child, :kill)`, no cleanup
|
||||
- integer (ms) — send `:shutdown` signal, wait N ms, then `:kill`
|
||||
- `:infinity` — wait forever (default for supervisor children)
|
||||
|
||||
Workers default to 5000ms. Supervisors default to `:infinity` (to give their children time).
|
||||
|
||||
**Why:** Graceful shutdown enables cleanup (closing connections, flushing buffers, deregistering from services). The timeout prevents hung processes from blocking system shutdown indefinitely. The hierarchy matters: supervisors need infinite time because they're waiting for their own children to shut down.
|
||||
|
||||
**Anti-pattern:** Setting `:brutal_kill` on processes that hold external resources (DB connections, file handles). They'll leak. Also: setting `:infinity` on worker processes — a bug in `terminate/2` will hang your entire shutdown.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
# From supervisor.ex docs:
|
||||
# :brutal_kill - unconditional and immediate termination
|
||||
# integer >= 0 - wait that many ms after :shutdown signal
|
||||
# :infinity - wait forever (recommended for supervisors)
|
||||
|
||||
# Worker default: 5000ms
|
||||
%{shutdown: 5_000, type: :worker}
|
||||
|
||||
# Supervisor default: :infinity
|
||||
%{shutdown: :infinity, type: :supervisor}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 12: DynamicSupervisor Internal State — Struct with Restart Tracking
|
||||
|
||||
**Source:** `lib/elixir/lib/dynamic_supervisor.ex:165-178` (defstruct)
|
||||
|
||||
**What it does:** The DynamicSupervisor uses a struct for its GenServer state with explicit fields: `children` (map of pid → child spec), `restarts` (list of timestamps for rate limiting), and configuration fields.
|
||||
|
||||
**Why:** This shows the Elixir team's state design philosophy: use a struct with named fields, not a bare map or tuple. The `children` field uses a `%{}` map keyed by PID for O(1) lookup/deletion on child exit. The `restarts` list uses a simple sliding-window approach for restart intensity.
|
||||
|
||||
**Anti-pattern:** Using a list for children lookup (O(n) on every EXIT message), or using a tuple-based state that requires positional knowledge.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
defstruct [
|
||||
:args,
|
||||
:extra_arguments,
|
||||
:mod,
|
||||
:name,
|
||||
:strategy,
|
||||
:max_children,
|
||||
:max_restarts,
|
||||
:max_seconds,
|
||||
children: %{},
|
||||
restarts: []
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 13: Restart Logic with Exponential Backoff via `:try_again`
|
||||
|
||||
**Source:** `lib/elixir/lib/dynamic_supervisor.ex:710-758` (restart_child and related functions)
|
||||
|
||||
**What it does:** When a child fails to restart (start function returns error), DynamicSupervisor doesn't give up. It stores the child as `{:restarting, child}`, sends itself a `:"$gen_restart"` message, and retries later. This prevents the supervisor from blocking on a transiently failing child.
|
||||
|
||||
**Why:** During network partitions or resource exhaustion, a child might fail to start immediately but succeed seconds later. Instead of counting this as a restart (which would hit intensity limits), the supervisor retries asynchronously. The `:try_again` path is separate from the restart counter.
|
||||
|
||||
**Anti-pattern:** Treating every start failure as a "restart" — this would exhaust `max_restarts` quickly during transient failures like port conflicts.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
defp restart_child(:one_for_one, current_pid, child, state) do
|
||||
{{m, f, args} = mfa, restart, shutdown, type, modules} = child
|
||||
%{extra_arguments: extra} = state
|
||||
|
||||
case start_child(m, f, extra ++ args) do
|
||||
{:ok, pid, _} ->
|
||||
state = delete_child(current_pid, state)
|
||||
{:ok, save_child(pid, mfa, restart, shutdown, type, modules, state)}
|
||||
|
||||
{:ok, pid} ->
|
||||
state = delete_child(current_pid, state)
|
||||
{:ok, save_child(pid, mfa, restart, shutdown, type, modules, state)}
|
||||
|
||||
:ignore ->
|
||||
{:ok, delete_child(current_pid, state)}
|
||||
|
||||
{:error, reason} ->
|
||||
report_error(:start_error, reason, {:restarting, current_pid}, child, state)
|
||||
state = put_in(state.children[current_pid], {:restarting, child})
|
||||
{:try_again, state}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 14: `$ancestors` and `$callers` — Process Lineage Tracking
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:227-268` (Ancestor and Caller Tracking section)
|
||||
|
||||
**What it does:** Elixir uses two process dictionary keys for lineage:
|
||||
- `$ancestors` — the supervision hierarchy (who spawned/supervises this process)
|
||||
- `$callers` — the logical call chain (who requested this work)
|
||||
|
||||
These are different! A task's ancestor is its supervisor, but its caller is the process that initiated the async operation.
|
||||
|
||||
**Why:** Debugging and tracing. When a task crashes, the log includes both its supervisor (for restart context) and its caller (for business logic context). This dual tracking is essential for understanding failures in systems where the spawner and supervisor are different processes.
|
||||
|
||||
**Anti-pattern:** Ignoring caller tracking when building custom process spawning. If you build something like `Task.Supervisor`, propagate `$callers` so crash logs are meaningful.
|
||||
|
||||
**Code example from source (task/supervisor.ex):**
|
||||
```elixir
|
||||
defp get_callers(owner) do
|
||||
case :erlang.get(:"$callers") do
|
||||
[_ | _] = list -> [owner | list]
|
||||
_ -> [owner]
|
||||
end
|
||||
end
|
||||
|
||||
# Task.start_link propagates both owner and callers
|
||||
def start_link(module, function, args)
|
||||
when is_atom(module) and is_atom(function) and is_list(args) do
|
||||
mfa = {module, function, args}
|
||||
Task.Supervised.start_link(get_owner(self()), get_callers(self()), mfa)
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 15: GenServer.reply/2 for Deferred Responses
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:620-640` (callback docs), `lib/elixir/lib/gen_server.ex:1328-1346` (reply/2 function)
|
||||
|
||||
**What it does:** A `handle_call` can return `{:noreply, state}` without replying, then later call `GenServer.reply(from, response)` from any process. This decouples request receipt from response delivery.
|
||||
|
||||
**Why:** Three use cases (from the source):
|
||||
1. Reply before returning (response known, but need to do cleanup after)
|
||||
2. Reply after returning (response not yet available, computed asynchronously)
|
||||
3. Reply from another process (delegate work to a task)
|
||||
|
||||
This enables non-blocking request handling in GenServers that would otherwise be bottlenecked.
|
||||
|
||||
**Anti-pattern:** Spawning a task to do work and then having the GenServer block on `Task.await` inside `handle_call`. This defeats the purpose — use `reply/2` from the task instead.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
def handle_call(:reply_in_one_second, from, state) do
|
||||
Process.send_after(self(), {:reply, from}, 1_000)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info({:reply, from}, state) do
|
||||
GenServer.reply(from, :one_second_has_passed)
|
||||
{:noreply, state}
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 16: Process.alias for Safe Request/Response
|
||||
|
||||
**Source:** `lib/elixir/lib/process.ex:32-95` (Aliases section)
|
||||
|
||||
**What it does:** Process aliases (Erlang/OTP 24+) provide a deactivatable reference for receiving replies. After sending a request with an alias as the reply address, you can deactivate the alias if you no longer want the response — any messages sent to a deactivated alias are silently dropped.
|
||||
|
||||
**Why:** Solves the "late reply" problem. In request/response patterns, if the requester times out and moves on, a late reply to its PID could confuse future `receive` blocks. With aliases, you deactivate after timeout and the late reply harmlessly vanishes.
|
||||
|
||||
**Anti-pattern:** Using bare PIDs for reply addresses in protocols where timeouts are possible. Late messages pollute the mailbox.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
server = spawn(&server/0)
|
||||
|
||||
source_alias = Process.alias()
|
||||
send(server, {:ping, source_alias})
|
||||
|
||||
receive do
|
||||
:pong -> :pong
|
||||
end
|
||||
|
||||
# Deactivate — late replies to this alias are silently dropped
|
||||
Process.unalias(source_alias)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 17: Registry Partitioning Strategies
|
||||
|
||||
**Source:** `lib/elixir/lib/registry.ex:310-350` (start_link partitioning docs)
|
||||
|
||||
**What it does:** Duplicate registries support two partitioning strategies:
|
||||
- `{:duplicate, :pid}` (default) — groups entries by the registering process's PID. Good for few keys with many entries (e.g., one PubSub topic with many subscribers).
|
||||
- `{:duplicate, :key}` — groups entries by key. Good for many keys with few entries each (e.g., many topics with few subscribers).
|
||||
|
||||
**Why:** The partitioning strategy determines which partition(s) need to be scanned during lookup. With `:key` partitioning, a key lookup hits exactly one partition (O(1) partitions). With `:pid` partitioning, key lookups must scan all partitions but process-based operations (unregister on death) are localized.
|
||||
|
||||
**Anti-pattern:** Using default `:pid` partitioning with millions of unique keys and frequent lookups. Each lookup scans all partitions. Switch to `{:duplicate, :key}`.
|
||||
|
||||
**Code example from source:**
|
||||
```elixir
|
||||
# Many topics, few subscribers each — use key partitioning
|
||||
Registry.start_link(
|
||||
keys: {:duplicate, :key},
|
||||
name: MyApp.TopicRegistry,
|
||||
partitions: System.schedulers_online()
|
||||
)
|
||||
|
||||
# Few topics, many subscribers — use pid partitioning (default)
|
||||
Registry.start_link(
|
||||
keys: :duplicate,
|
||||
name: MyApp.BroadcastRegistry,
|
||||
partitions: System.schedulers_online()
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern 18: `init/1` Return Values — The Full Spectrum
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:498-545` (init callback spec)
|
||||
|
||||
**What it does:** `init/1` supports five return values:
|
||||
- `{:ok, state}` — normal start
|
||||
- `{:ok, state, timeout}` — start with idle timeout
|
||||
- `{:ok, state, :hibernate}` — start and immediately hibernate (GC + compact heap)
|
||||
- `{:ok, state, {:continue, arg}}` — start then immediately invoke `handle_continue`
|
||||
- `:ignore` — don't start, supervisor treats as successful (child can be restarted later)
|
||||
- `{:stop, reason}` — initialization failed
|
||||
|
||||
**Why:** Each covers a real scenario:
|
||||
- `:ignore` — process is disabled by configuration but might be enabled later via `Supervisor.restart_child/2`
|
||||
- `{:stop, reason}` — unrecoverable initialization failure
|
||||
- `:hibernate` — process will be idle for a long time, minimize memory
|
||||
- `{:continue, _}` — split fast init from slow setup
|
||||
|
||||
**Anti-pattern:** Using `{:stop, reason}` when `:ignore` is appropriate. If a feature is disabled by config, `:ignore` keeps the child spec in the supervisor for later activation. `{:stop, reason}` signals a real failure.
|
||||
|
||||
@@ -0,0 +1,545 @@
|
||||
# Testing Patterns in Elixir
|
||||
|
||||
Patterns extracted from the Elixir standard library source code — how the core team writes and organizes tests.
|
||||
|
||||
---
|
||||
|
||||
## 1. Module-Level Async Declaration
|
||||
|
||||
**Source:** `lib/elixir/test/elixir/gen_server_test.exs:9`, `lib/elixir/test/elixir/enum_test.exs:8`, nearly all test files
|
||||
|
||||
**What it does:** Every test module declares `async: true` or `async: false` at the module level, making concurrency intent explicit.
|
||||
|
||||
**Why:** Tests that don't mutate global state run concurrently, dramatically speeding up the suite. The explicit opt-in forces developers to think about whether their test touches shared resources.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
defmodule GenServerTest do
|
||||
use ExUnit.Case, async: true
|
||||
# ...
|
||||
end
|
||||
|
||||
# When global state is modified (e.g. registered processes):
|
||||
defmodule TaskTest do
|
||||
use ExUnit.Case # async defaults to false
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
**Key insight:** The vast majority of Elixir's own tests use `async: true`. Only tests that register global names, modify Logger config, or interact with the filesystem use synchronous mode.
|
||||
|
||||
---
|
||||
|
||||
## 2. Parameterized Tests
|
||||
|
||||
**Source:** `lib/elixir/test/elixir/registry_test.exs:12-22`
|
||||
|
||||
**What it does:** Runs the same test suite against multiple configurations using the `:parameterize` option (since v1.18).
|
||||
|
||||
**Why:** Avoids duplicating test modules for combinatorial configurations. The Registry needs testing with `:unique`/`:duplicate` keys and varying partition counts.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
defmodule Registry.Test do
|
||||
use ExUnit.Case,
|
||||
async: true,
|
||||
parameterize:
|
||||
for(
|
||||
keys <- [:unique, :duplicate, {:duplicate, :pid}, {:duplicate, :key}],
|
||||
partitions <- [1, 8],
|
||||
do: %{keys: keys, partitions: partitions}
|
||||
)
|
||||
|
||||
setup config do
|
||||
name = :"#{config.test}_#{config.partitions}_#{inspect(config.keys)}"
|
||||
opts = [keys: config.keys, name: name, partitions: config.partitions]
|
||||
{:ok, _} = start_supervised({Registry, opts})
|
||||
%{registry: name}
|
||||
end
|
||||
|
||||
test "clean up registry on process crash", %{registry: registry, partitions: partitions} do
|
||||
# Test body uses parameters from context
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Warning from docs:** "If you use parameterized tests and then find yourself adding conditionals in your tests to deal with different parameters, then parameterized tests may be the wrong solution."
|
||||
|
||||
---
|
||||
|
||||
## 3. Setup with `start_supervised/2`
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/callbacks.ex:277-340`, `lib/elixir/test/elixir/registry_test.exs:31`
|
||||
|
||||
**What it does:** Starts processes under a test supervisor that guarantees cleanup before the next test.
|
||||
|
||||
**Why:** Eliminates manual cleanup. The test supervisor terminates children in reverse order before `on_exit` callbacks run. No leaked processes between tests.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
setup config do
|
||||
{:ok, _} = start_supervised({Registry, keys: :unique, name: config.test})
|
||||
%{registry: config.test}
|
||||
end
|
||||
```
|
||||
|
||||
**Contrast with anti-pattern:**
|
||||
```elixir
|
||||
# BAD — process may leak if test crashes before cleanup
|
||||
setup do
|
||||
{:ok, pid} = Registry.start_link(keys: :unique, name: :my_reg)
|
||||
on_exit(fn -> Process.exit(pid, :kill) end)
|
||||
%{registry: :my_reg}
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Named Setup Functions (Composable Pipelines)
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/callbacks.ex:100-120` (docs)
|
||||
|
||||
**What it does:** Defines setup as a list of named functions rather than anonymous blocks.
|
||||
|
||||
**Why:** Each step is independently testable, reusable, and the setup pipeline reads like a declaration of preconditions.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
setup [:clean_up_tmp_directory, :start_server, :seed_data]
|
||||
|
||||
defp clean_up_tmp_directory(_context), do: [tmp_dir: "/tmp/test"]
|
||||
defp start_server(context), do: {:ok, server: start_supervised!({MyServer, context.tmp_dir})}
|
||||
defp seed_data(context), do: :ok
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. `on_exit` for Reversing Global Side Effects
|
||||
|
||||
**Source:** `lib/elixir/test/elixir/task_test.exs:1128-1131`, `lib/logger/test/logger_test.exs:12-17`
|
||||
|
||||
**What it does:** Registers cleanup callbacks that always run, even if the test fails.
|
||||
|
||||
**Why:** Guarantees global state (Logger config, ETS tables, process registrations) is restored regardless of test outcome.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
setup do
|
||||
translator = :logger.get_primary_config().filters[:logger_translator]
|
||||
assert :ok = :logger.remove_primary_filter(:logger_translator)
|
||||
on_exit(fn -> :logger.add_primary_filter(:logger_translator, translator) end)
|
||||
end
|
||||
```
|
||||
|
||||
**Key design:** `on_exit` runs in a *separate process* from the test, so it cannot interfere with test assertions.
|
||||
|
||||
---
|
||||
|
||||
## 6. Pattern Match Assertions
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/assertions.ex:145-175`
|
||||
|
||||
**What it does:** Uses `assert` with `=` for structural pattern matching in assertions.
|
||||
|
||||
**Why:** Provides rich failure messages showing both sides. More expressive than `assert x == y` when you only care about shape.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
# Assert structure, ignore specifics
|
||||
assert {:ok, %{id: id}} = create_user("alice")
|
||||
assert is_integer(id)
|
||||
|
||||
# Pin variables in patterns
|
||||
x = 5
|
||||
assert {:count, ^x} = get_counter()
|
||||
|
||||
# match? for complex guards
|
||||
assert match?([%{id: id} | _] when is_integer(id), records)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. `assert_receive` / `refute_receive` for Process Communication
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/assertions.ex:466-526`, `lib/elixir/test/elixir/process_test.exs:90-100`
|
||||
|
||||
**What it does:** Waits for messages matching a pattern within a timeout (default 100ms).
|
||||
|
||||
**Why:** Tests asynchronous process communication without `Process.sleep`. The test either receives the expected message or fails with a helpful mailbox dump.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
# Basic message assertion
|
||||
test "send_after/3 sends messages once expired" do
|
||||
Process.send_after(self(), :hello, 10)
|
||||
assert_receive :hello
|
||||
end
|
||||
|
||||
# Pattern matching with pins
|
||||
test "monitor/2 with monitor options" do
|
||||
ref_and_alias = Process.monitor(pid, alias: :explicit_unalias)
|
||||
send(pid, {:ping, ref_and_alias})
|
||||
assert_receive :pong
|
||||
assert_receive {:DOWN, ^ref_and_alias, _, _, _}
|
||||
end
|
||||
|
||||
# Negative assertion
|
||||
test "exit(pid, :normal) does not cause the target process to exit" do
|
||||
Process.exit(pid, :normal)
|
||||
refute_receive {:EXIT, ^pid, :normal}, 100
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Testing GenServers via Public API (No Internal State Inspection)
|
||||
|
||||
**Source:** `lib/elixir/test/elixir/gen_server_test.exs:87-106`
|
||||
|
||||
**What it does:** Tests GenServer behavior exclusively through `GenServer.call/cast/stop` — never peeks at internal state.
|
||||
|
||||
**Why:** Tests the contract, not the implementation. Internal state changes don't break tests.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
test "start_link/2, call/2 and cast/2" do
|
||||
{:ok, pid} = GenServer.start_link(Stack, [:hello])
|
||||
|
||||
assert GenServer.call(pid, :pop) == :hello
|
||||
assert GenServer.cast(pid, {:push, :world}) == :ok
|
||||
assert GenServer.call(pid, :pop) == :world
|
||||
assert GenServer.stop(pid) == :ok
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. `catch_exit` for Testing Process Failures
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/assertions.ex:950-960`, `lib/elixir/test/elixir/gen_server_test.exs:118-137`
|
||||
|
||||
**What it does:** Catches exit signals from linked processes for assertion, or uses `Process.flag(:trap_exit, true)` + `assert_receive {:EXIT, ...}`.
|
||||
|
||||
**Why:** Testing error conditions in OTP requires intercepting exit signals. The two approaches serve different needs.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
# catch_exit for synchronous exit testing
|
||||
test "call/3 exit messages" do
|
||||
assert catch_exit(GenServer.call(pid, :noreply, 1)) ==
|
||||
{:timeout, {GenServer, :call, [pid, :noreply, 1]}}
|
||||
end
|
||||
|
||||
# trap_exit for linked process exits
|
||||
test "exits on task error" do
|
||||
Process.flag(:trap_exit, true)
|
||||
task = Task.async(fn -> raise "oops" end)
|
||||
assert {{%RuntimeError{}, _}, {Task, :await, [^task, 5000]}} = catch_exit(Task.await(task))
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. `@tag capture_log: true` for Suppressing Expected Log Output
|
||||
|
||||
**Source:** `lib/elixir/test/elixir/gen_server_test.exs:114`, `lib/elixir/test/elixir/task_test.exs:10`
|
||||
|
||||
**What it does:** Captures log output during the test, only printing it if the test fails.
|
||||
|
||||
**Why:** Tests that intentionally trigger error conditions produce noisy log output. Capturing keeps the test output clean while preserving diagnostics on failure.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
# Per-test tag
|
||||
@tag capture_log: true
|
||||
test "call/3 exit messages" do
|
||||
# This test triggers error logs — they're captured
|
||||
end
|
||||
|
||||
# Module-level for all tests
|
||||
@moduletag :capture_log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. `capture_log` / `capture_io` for Content Assertions
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/capture_log.ex:1-50`, `lib/elixir/test/elixir/task_test.exs:1138-1150`
|
||||
|
||||
**What it does:** Captures log/IO output and returns it as a string for assertion.
|
||||
|
||||
**Why:** Tests that the right messages are logged/printed without relying on side effects.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
# capture_log for asserting log content
|
||||
test "logs a terminated task" do
|
||||
assert ExUnit.CaptureLog.capture_log(fn ->
|
||||
ref = Process.monitor(pid)
|
||||
send(pid, :go)
|
||||
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
|
||||
end) =~ ~r/Task .* terminating/
|
||||
end
|
||||
|
||||
# with_io returns both result and output (since v1.13)
|
||||
{result, output} = with_io(fn ->
|
||||
IO.puts("a")
|
||||
2 + 2
|
||||
end)
|
||||
assert result == 4
|
||||
assert output == "a\n"
|
||||
```
|
||||
|
||||
**Important for async tests:** Use `=~` instead of `==` for `:stderr` captures because output from other tests may interleave.
|
||||
|
||||
---
|
||||
|
||||
## 12. `describe` Blocks for Logical Grouping
|
||||
|
||||
**Source:** `lib/elixir/test/elixir/task_test.exs:218,272,365`, `lib/elixir/test/elixir/process_test.exs:146`
|
||||
|
||||
**What it does:** Groups related tests under a named describe block. Setup inside describe only applies to that group.
|
||||
|
||||
**Why:** Organizes tests by function/feature. Makes test output readable. Allows scoped `@describetag` and scoped setup.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
describe "await/2" do
|
||||
test "exits on timeout" do
|
||||
task = %Task{ref: make_ref(), owner: self(), pid: nil, mfa: {__MODULE__, :test, 1}}
|
||||
assert catch_exit(Task.await(task, 0)) == {:timeout, {Task, :await, [task, 0]}}
|
||||
end
|
||||
|
||||
test "exits on normal exit" do
|
||||
task = Task.async(fn -> exit(:normal) end)
|
||||
assert catch_exit(Task.await(task)) == {:normal, {Task, :await, [task, 5000]}}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Constraint:** Describe blocks cannot be nested. `setup_all` cannot appear inside describe.
|
||||
|
||||
---
|
||||
|
||||
## 13. `ExUnit.CaseTemplate` for Shared Test Infrastructure
|
||||
|
||||
**Source:** `lib/mix/test/test_helper.exs:79-140`, `lib/logger/test/test_helper.exs:24-65`
|
||||
|
||||
**What it does:** Defines reusable test case templates with shared setup, helpers, and imports.
|
||||
|
||||
**Why:** Eliminates duplication across test modules. Provides domain-specific test DSLs.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
# In test_helper.exs
|
||||
defmodule Logger.Case do
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
using _ do
|
||||
quote do
|
||||
import Logger.Case
|
||||
end
|
||||
end
|
||||
|
||||
setup do
|
||||
on_exit(fn ->
|
||||
# Shared cleanup for all tests using this template
|
||||
end)
|
||||
:ok
|
||||
end
|
||||
|
||||
def capture_log(level \\ :debug, fun) do
|
||||
Logger.configure(level: level)
|
||||
capture_io(:user, fn ->
|
||||
fun.()
|
||||
Logger.flush()
|
||||
end)
|
||||
after
|
||||
Logger.configure(level: :debug)
|
||||
end
|
||||
end
|
||||
|
||||
# In test file:
|
||||
defmodule LoggerTest do
|
||||
use Logger.Case
|
||||
# Gets all imports and setup from the template
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. `doctest` Integration
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/doc_test.ex:1-80`, `lib/elixir/test/elixir/agent_test.exs:9`
|
||||
|
||||
**What it does:** Generates tests from `@doc` and `@moduledoc` code examples.
|
||||
|
||||
**Why:** Documentation examples are always verified. Prevents docs from rotting.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
defmodule AgentTest do
|
||||
use ExUnit.Case, async: true
|
||||
doctest Agent
|
||||
end
|
||||
|
||||
# Selective doctesting:
|
||||
doctest Kernel, except: [===: 2, !==: 2, and: 2, or: 2]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. `Process.sleep(:infinity)` as a Process Parking Pattern
|
||||
|
||||
**Source:** `lib/elixir/test/elixir/task_test.exs:417`, `lib/elixir/test/elixir/registry_test.exs:71`
|
||||
|
||||
**What it does:** Spawns processes that block forever, used as test subjects that need to exist until explicitly killed.
|
||||
|
||||
**Why:** Creates stable process references for testing supervision, monitoring, and registry behavior. The process stays alive until the test supervisor shuts it down.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
# Process exists solely to be registered/monitored
|
||||
{:ok, task} =
|
||||
Task.start(fn ->
|
||||
send(parent, Registry.register(registry, key, value))
|
||||
Process.sleep(:infinity)
|
||||
end)
|
||||
|
||||
# Then kill it to test cleanup:
|
||||
Process.exit(task, :kill)
|
||||
assert_receive {:DOWN, ^ref, _, _, _}
|
||||
```
|
||||
|
||||
**Important distinction:** This is NOT `Process.sleep(100)` for timing — it's an intentional "park this process" pattern where the process is always explicitly terminated by the test.
|
||||
|
||||
---
|
||||
|
||||
## 16. Helper Functions for Test-Specific Behavior
|
||||
|
||||
**Source:** `lib/elixir/test/elixir/task_test.exs:12-36`, `lib/elixir/test/elixir/supervisor_test.exs:278-285`
|
||||
|
||||
**What it does:** Defines private helper functions within test modules for common test operations.
|
||||
|
||||
**Why:** Keeps tests DRY without over-abstracting. Helpers like `wait_until_down`, `assert_kill`, `create_dummy_task` encapsulate recurring patterns.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
defmodule TaskTest do
|
||||
use ExUnit.Case
|
||||
|
||||
# Helper to create a known-state task for testing edge cases
|
||||
defp create_dummy_task(reason) do
|
||||
{pid, ref} = spawn_monitor(Kernel, :exit, [reason])
|
||||
receive do
|
||||
{:DOWN, ^ref, _, _, _} ->
|
||||
%Task{ref: ref, pid: pid, owner: self(), mfa: {__MODULE__, :create_dummy_task, 1}}
|
||||
end
|
||||
end
|
||||
|
||||
# Helper that properly waits for process termination
|
||||
def wait_until_down(task) do
|
||||
ref = Process.monitor(task.pid)
|
||||
assert_receive {:DOWN, ^ref, _, _, _}
|
||||
end
|
||||
|
||||
# Helper for asserting process kill
|
||||
defp assert_kill(pid, reason) do
|
||||
ref = Process.monitor(pid)
|
||||
Process.exit(pid, reason)
|
||||
assert_receive {:DOWN, ^ref, _, _, _}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17. `@tag :tmp_dir` for Filesystem Tests
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/case.ex:281-304`, `lib/elixir/test/elixir/path_test.exs:12`
|
||||
|
||||
**What it does:** ExUnit automatically creates a unique temporary directory and passes its path via the test context.
|
||||
|
||||
**Why:** Filesystem tests need isolation. Each test gets its own directory, removed before creation to ensure a clean slate.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
@tag :tmp_dir
|
||||
test "writes files", %{tmp_dir: tmp_dir} do
|
||||
path = Path.join(tmp_dir, "test.txt")
|
||||
File.write!(path, "hello")
|
||||
assert File.read!(path) == "hello"
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18. `assert_raise` with Message Matching
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/assertions.ex:815-885`
|
||||
|
||||
**What it does:** Asserts both the exception type AND the message content (string or regex).
|
||||
|
||||
**Why:** Verifying the exception type alone is insufficient — the message tells users what went wrong. Testing it ensures error UX.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
# Exact message match
|
||||
assert_raise ArgumentError, ~r"expected :name option to be one of the following:", fn ->
|
||||
GenServer.start_link(Stack, [:hello], name: "my_gen_server_name")
|
||||
end
|
||||
|
||||
# Regex for dynamic content
|
||||
assert_raise RuntimeError, ~r/^today's lucky number is 0\.\d+!$/, fn ->
|
||||
raise "today's lucky number is #{:rand.uniform()}!"
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19. `@moduletag` / `@describetag` for Cross-Cutting Configuration
|
||||
|
||||
**Source:** `lib/elixir/test/elixir/system_test.exs:104,163`, `lib/elixir/test/elixir/task_test.exs:10`
|
||||
|
||||
**What it does:** Sets tags that apply to all tests in a module or describe block, used for filtering and configuration.
|
||||
|
||||
**Why:** Enables running subsets of tests (`mix test --include unix`) and applying configuration (like `:capture_log`) without repeating it on every test.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
defmodule SystemTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
describe "Windows" do
|
||||
@describetag :windows
|
||||
# All tests here tagged :windows
|
||||
end
|
||||
|
||||
describe "Unix" do
|
||||
@describetag :unix
|
||||
# All tests here tagged :unix
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 20. Context Pattern Matching in Test Signatures
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/case.ex:57-80`, `lib/elixir/test/elixir/gen_server_test.exs:166`
|
||||
|
||||
**What it does:** Destructures the test context directly in the test function signature.
|
||||
|
||||
**Why:** Makes dependencies explicit. You see exactly what each test needs from setup.
|
||||
|
||||
**Pattern:**
|
||||
```elixir
|
||||
test "abcast/3", %{test: name} do
|
||||
{:ok, _} = GenServer.start_link(Stack, [], name: name)
|
||||
assert GenServer.abcast(name, {:push, :hello}) == :abcast
|
||||
end
|
||||
|
||||
# Using test name for unique naming — prevents collision in async tests
|
||||
```
|
||||
|
||||
The `%{test: name}` pattern is ubiquitous — the test name is unique per module, making it perfect for naming registered processes in async tests.
|
||||
@@ -0,0 +1,308 @@
|
||||
# Typespecs Patterns
|
||||
|
||||
Patterns extracted from the Elixir standard library source code.
|
||||
|
||||
---
|
||||
|
||||
## 1. Public Type with @typedoc
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex` lines 862–896
|
||||
|
||||
**What it does:** Every public `@type` is preceded by a `@typedoc` that explains what the type represents, often referencing which functions use it.
|
||||
|
||||
**Why:** Types are part of the public API. Without documentation, users must guess what a type means from its definition alone. The `@typedoc` bridges intent and implementation.
|
||||
|
||||
**Anti-pattern:** Defining `@type` without any `@typedoc`, leaving users to decipher complex union types.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@typedoc "Return values of `start*` functions"
|
||||
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
|
||||
|
||||
@typedoc "The GenServer name"
|
||||
@type name :: nil | atom | {:global, term} | {:via, module, term}
|
||||
|
||||
@typedoc """
|
||||
The server reference.
|
||||
|
||||
This is either a plain PID or a value representing a registered name.
|
||||
...
|
||||
"""
|
||||
@type server :: pid | name | {atom, node}
|
||||
|
||||
@typedoc """
|
||||
Tuple describing the client of a call request.
|
||||
|
||||
`pid` is the PID of the caller and `tag` is a unique term used to identify the
|
||||
call.
|
||||
"""
|
||||
@type from :: {pid, tag :: term}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Private Types with @typep
|
||||
|
||||
**Source:** `lib/elixir/lib/macro.ex` lines 84, 97
|
||||
|
||||
**What it does:** Uses `@typep` for internal recursive type definitions that are implementation details not meant for external consumers.
|
||||
|
||||
**Why:** Keeps the public type surface small while allowing internal type reuse. Private types can reference themselves recursively (e.g., for AST node structure) without polluting the documentation.
|
||||
|
||||
**Anti-pattern:** Making all types public "just in case." This bloats documentation and creates implicit public API guarantees.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@typedoc "The inputs of a macro"
|
||||
@type input ::
|
||||
input_expr
|
||||
| {input, input}
|
||||
| [input]
|
||||
| atom
|
||||
| number
|
||||
| binary
|
||||
|
||||
@typep input_expr :: {input_expr | atom, metadata, atom | [input]}
|
||||
|
||||
@typedoc "The output of a macro"
|
||||
@type output ::
|
||||
output_expr
|
||||
| {output, output}
|
||||
| [output]
|
||||
| atom
|
||||
| number
|
||||
| binary
|
||||
| captured_remote_function
|
||||
| pid
|
||||
|
||||
@typep output_expr :: {output_expr | atom, metadata, atom | [output]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. @opaque Types (Protocol t())
|
||||
|
||||
**Source:** `lib/elixir/lib/protocol.ex` lines 150–168 (documentation), runtime generation
|
||||
|
||||
**What it does:** Protocols auto-generate an opaque `@type t :: term()` type that represents "any value implementing this protocol."
|
||||
|
||||
**Why:** The type is opaque because the concrete implementations are open-ended — any module can implement the protocol. Making it opaque prevents users from pattern-matching on the internal representation (which could be anything) and signals that you should only use the protocol functions.
|
||||
|
||||
**Anti-pattern:** Defining `@opaque` for types where consumers *need* to destructure the value. Use `@opaque` only when the internal representation is truly hidden.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
# Generated automatically by defprotocol — documented behavior:
|
||||
defprotocol Size do
|
||||
@doc "Calculates the size (and not the length!) of a data structure"
|
||||
def size(data)
|
||||
end
|
||||
|
||||
# Usage in specs:
|
||||
@spec print_size(Size.t()) :: :ok
|
||||
def print_size(data) do
|
||||
# ...
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Union Types in @spec Return Values
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex` lines 577–583, 647–658
|
||||
|
||||
**What it does:** Uses union types with descriptive tagged tuples in callback specs, making all possible return shapes explicit.
|
||||
|
||||
**Why:** OTP callbacks accept multiple return shapes (e.g., `{:reply, ...}`, `{:noreply, ...}`, `{:stop, ...}`). Spelling out every variant in the spec enables Dialyzer and makes the contract self-documenting.
|
||||
|
||||
**Anti-pattern:** Using `term()` as a catch-all return type when specific shapes are known. This defeats the purpose of typespecs.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@callback init(init_arg :: term) ::
|
||||
{:ok, state}
|
||||
| {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}}
|
||||
| :ignore
|
||||
| {:stop, reason :: term}
|
||||
when state: term
|
||||
|
||||
@callback handle_call(request :: term, from, state :: term) ::
|
||||
{:reply, reply, new_state}
|
||||
| {:reply, reply, new_state,
|
||||
timeout | :hibernate | {:continue, continue_arg :: term}}
|
||||
| {:noreply, new_state}
|
||||
| {:noreply, new_state, timeout | :hibernate | {:continue, continue_arg :: term}}
|
||||
| {:stop, reason, reply, new_state}
|
||||
| {:stop, reason, new_state}
|
||||
when reply: term, new_state: term, reason: term
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. `when` Constraints in Specs
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel.ex` lines 635, 1072, 1455
|
||||
|
||||
**What it does:** Uses the `when` clause in `@spec` to bind type variables, expressing relationships between parameters and return values.
|
||||
|
||||
**Why:** When the same type variable appears in multiple positions, it communicates that those values are related (same type). This is critical for generic functions like `hd/1`, `max/2`.
|
||||
|
||||
**Anti-pattern:** Using unrelated type names in different argument positions when the same variable would express the contract more precisely.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@spec hd(nonempty_maybe_improper_list(elem, term)) :: elem when elem: term
|
||||
|
||||
@spec max(first, second) :: first | second when first: term, second: term
|
||||
|
||||
@spec tl(nonempty_maybe_improper_list(elem, last)) :: maybe_improper_list(elem, last) | last
|
||||
when elem: term, last: term
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Map Types with required/optional Keys
|
||||
|
||||
**Source:** `lib/elixir/lib/supervisor.ex` lines 602–607, 644–652
|
||||
|
||||
**What it does:** Uses map type syntax with `required()` and `optional()` keys to define struct-like specs where some fields have defaults.
|
||||
|
||||
**Why:** Supervisors have a child_spec map where `:id` and `:start` are mandatory but `:restart`, `:shutdown` etc. are optional with defaults. The type system reflects this precisely.
|
||||
|
||||
**Anti-pattern:** Using a plain `map()` type or making all keys required when some have sensible defaults.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@type sup_flags() :: %{
|
||||
strategy: strategy(),
|
||||
intensity: non_neg_integer(),
|
||||
period: pos_integer(),
|
||||
auto_shutdown: auto_shutdown()
|
||||
}
|
||||
|
||||
@type child_spec :: %{
|
||||
required(:id) => atom() | term(),
|
||||
required(:start) => {module(), function_name :: atom(), args :: [term()]},
|
||||
optional(:restart) => restart(),
|
||||
optional(:shutdown) => shutdown(),
|
||||
optional(:type) => type(),
|
||||
optional(:modules) => [module()] | :dynamic,
|
||||
optional(:significant) => boolean()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Keyword List Types for Options
|
||||
|
||||
**Source:** `lib/logger/lib/logger.ex` lines 509–531
|
||||
|
||||
**What it does:** Defines option types as keyword lists with specific key-value constraints, sometimes nested.
|
||||
|
||||
**Why:** Many Elixir APIs accept keyword options. Typing them precisely documents valid options and their expected value types without forcing users to read function documentation.
|
||||
|
||||
**Anti-pattern:** Typing options as `keyword()` (untyped keyword list), which provides zero guidance.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@type configure_opts :: [
|
||||
level: level(),
|
||||
translator_inspect_opts: Inspect.Opts.t(),
|
||||
sync_threshold: non_neg_integer(),
|
||||
discard_threshold: non_neg_integer(),
|
||||
truncate: non_neg_integer() | :infinity,
|
||||
utc_log: boolean()
|
||||
]
|
||||
|
||||
@type formatter_opts :: [
|
||||
colors: [
|
||||
enabled: boolean(),
|
||||
debug: atom(),
|
||||
info: atom(),
|
||||
warning: atom(),
|
||||
error: atom()
|
||||
],
|
||||
format: String.t() | {module(), atom()},
|
||||
metadata: :all | [atom()],
|
||||
truncate: pos_integer() | :infinity,
|
||||
utc_log: boolean()
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Parameterized Types (t/1)
|
||||
|
||||
**Source:** `lib/elixir/lib/enum.ex` lines 58–73 (Enumerable protocol)
|
||||
|
||||
**What it does:** Defines a parameterized type `t(_element)` that allows expressing the element type of an enumerable in function specs.
|
||||
|
||||
**Why:** Enables downstream functions to express type flow: "takes an enumerable of integers, returns an enumerable of strings." The parameter communicates element type even though the protocol dispatch erases it at runtime.
|
||||
|
||||
**Anti-pattern:** Always using `t()` without parameters when the element type is known and useful for documentation.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@typedoc """
|
||||
An enumerable of elements of type `element`.
|
||||
|
||||
This type is equivalent to `t:t/0` but is especially useful for documentation.
|
||||
|
||||
For example, imagine you define a function that expects an enumerable of
|
||||
integers and returns an enumerable of strings:
|
||||
|
||||
@spec integers_to_strings(Enumerable.t(integer())) :: Enumerable.t(String.t())
|
||||
def integers_to_strings(integers) do
|
||||
Stream.map(integers, &Integer.to_string/1)
|
||||
end
|
||||
|
||||
"""
|
||||
@typedoc since: "1.14.0"
|
||||
@type t(_element) :: t()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Named Parameters in Specs (:: annotation)
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex` line 577, `lib/elixir/lib/supervisor.ex` line 562
|
||||
|
||||
**What it does:** Uses `name :: type` syntax in callback/spec parameter positions to give meaningful names to parameters.
|
||||
|
||||
**Why:** `init(init_arg :: term)` is vastly more readable than `init(term)`. The name serves as inline documentation within the spec itself.
|
||||
|
||||
**Anti-pattern:** Writing specs with only types and no parameter names, especially for callbacks that users must implement.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@callback init(init_arg :: term) ::
|
||||
{:ok, state}
|
||||
| {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}}
|
||||
| :ignore
|
||||
| {:stop, reason :: term}
|
||||
when state: term
|
||||
|
||||
@callback terminate(reason, state :: term) :: term
|
||||
when reason: :normal | :shutdown | {:shutdown, term} | term
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. @typedoc since: Annotation
|
||||
|
||||
**Source:** `lib/elixir/lib/supervisor.ex` lines 669–670, `lib/elixir/lib/enum.ex` line 72
|
||||
|
||||
**What it does:** Attaches a `since:` metadata annotation to `@typedoc` indicating when a type was introduced.
|
||||
|
||||
**Why:** Helps library consumers know version requirements. The pattern mirrors `@doc since:` used on functions.
|
||||
|
||||
**Anti-pattern:** Omitting `since:` on types added after the initial release, leaving users guessing about compatibility.
|
||||
|
||||
**Code example:**
|
||||
```elixir
|
||||
@typedoc since: "1.16.0"
|
||||
@type module_spec :: {module(), args :: term()} | module()
|
||||
|
||||
@typedoc since: "1.14.0"
|
||||
@type t(_element) :: t()
|
||||
```
|
||||
@@ -0,0 +1,151 @@
|
||||
# Phoenix Deviations from Elixir Core
|
||||
|
||||
Where Phoenix deliberately differs from Elixir core patterns and why.
|
||||
|
||||
## 1. Heavy Macro Usage for Performance
|
||||
|
||||
**Elixir core philosophy:** Keep macro usage minimal. From the Router source:
|
||||
|
||||
> Phoenix does its best to keep the usage of macros low.
|
||||
|
||||
**Phoenix deviation:** The Router uses macros extensively.
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:109-123`
|
||||
|
||||
> We use `get`, `post`, `put`, and `delete` to define your routes. We use macros
|
||||
> for two purposes:
|
||||
>
|
||||
> * They define the routing engine... Phoenix compiles all of your routes to a
|
||||
> single case-statement with pattern matching rules
|
||||
>
|
||||
> * For each route you define, we also define metadata to implement
|
||||
> `Phoenix.VerifiedRoutes`
|
||||
|
||||
**Why the deviation:** Performance. Elixir core uses macros sparingly because they add cognitive complexity. Phoenix justifies them because routing is the hottest path in a web app — compile-time optimization yields measurable request/second gains.
|
||||
|
||||
---
|
||||
|
||||
## 2. `import` without Restriction in Router
|
||||
|
||||
**Elixir core pattern:** Always use `import Module, only: [...]` to be explicit.
|
||||
|
||||
**Phoenix deviation:** The Router imports entire modules:
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:274-276`
|
||||
|
||||
```elixir
|
||||
import Phoenix.Router
|
||||
import Plug.Conn
|
||||
import Phoenix.Controller
|
||||
```
|
||||
|
||||
**Why the deviation:** The Router is a DSL. Users need `get`, `post`, `pipe_through`, `scope`, `resources`, `plug`, `fetch_session`, etc. — all available without qualification. Restricting imports would make the DSL unusable.
|
||||
|
||||
---
|
||||
|
||||
## 3. Compile-Time State Accumulation
|
||||
|
||||
**Elixir core pattern:** Modules are generally stateless during compilation. Functions are defined and that's it.
|
||||
|
||||
**Phoenix deviation:** Aggressive use of module attribute accumulation.
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:271-280`
|
||||
|
||||
```elixir
|
||||
Module.register_attribute(__MODULE__, :phoenix_routes, accumulate: true)
|
||||
@phoenix_pipeline nil
|
||||
Phoenix.Router.Scope.init(__MODULE__)
|
||||
@before_compile unquote(__MODULE__)
|
||||
```
|
||||
|
||||
**Why the deviation:** The Router needs to collect ALL routes, then compile them into a single dispatch function. This requires building up state during module compilation, then consuming it all at `@before_compile`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Channel Restart Strategy: `:temporary`
|
||||
|
||||
**Elixir core GenServer default:** `:permanent` (always restart).
|
||||
|
||||
**Phoenix Channel default:** `:temporary` (never restart).
|
||||
|
||||
**Source:** `lib/phoenix/channel.ex:470-475`
|
||||
|
||||
```elixir
|
||||
def child_spec(init_arg) do
|
||||
%{
|
||||
id: __MODULE__,
|
||||
start: {__MODULE__, :start_link, [init_arg]},
|
||||
shutdown: @phoenix_shutdown,
|
||||
restart: :temporary
|
||||
}
|
||||
end
|
||||
```
|
||||
|
||||
**Why the deviation:** A crashed channel should NOT auto-restart — the client needs to explicitly reconnect and rejoin. Auto-restarting would create a channel without a connected client, which is meaningless.
|
||||
|
||||
---
|
||||
|
||||
## 5. Auto-Hibernation
|
||||
|
||||
**Elixir core GenServer:** No default hibernation — processes stay in memory.
|
||||
|
||||
**Phoenix Channel:** Defaults to hibernate after 15 seconds of inactivity.
|
||||
|
||||
**Source:** `lib/phoenix/channel.ex:460`
|
||||
|
||||
```elixir
|
||||
@phoenix_hibernate_after Keyword.get(opts, :hibernate_after, 15_000)
|
||||
```
|
||||
|
||||
```elixir
|
||||
def start_link(triplet) do
|
||||
GenServer.start_link(Phoenix.Channel.Server, triplet,
|
||||
hibernate_after: @phoenix_hibernate_after
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
**Why the deviation:** Web apps have many idle connections. Channels for users who are "connected but not active" are common. Hibernation reclaims memory for the heap without killing the process. A chat app with 10,000 connected users benefits enormously.
|
||||
|
||||
---
|
||||
|
||||
## 6. `Plug.Builder` vs Raw Behaviour
|
||||
|
||||
**Elixir core:** Behaviours define contracts. Implementations are manual.
|
||||
|
||||
**Phoenix Endpoint:** Uses `Plug.Builder` — a macro that generates the `call/2` pipeline by chaining plugs at compile time.
|
||||
|
||||
**Source:** `lib/phoenix/endpoint.ex:481-483`
|
||||
|
||||
```elixir
|
||||
defp plug() do
|
||||
quote location: :keep do
|
||||
use Plug.Builder, init_mode: Phoenix.plug_init_mode()
|
||||
...
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Why the deviation:** The Plug specification (`init/1` + `call/2`) is too low-level for composing dozens of middleware. `Plug.Builder` provides the `plug` macro that chains them automatically. It's a higher-level abstraction over the raw behaviour pattern.
|
||||
|
||||
---
|
||||
|
||||
## 7. Exception Structs with HTTP Status Codes
|
||||
|
||||
**Elixir core exceptions:** Pure data — message, maybe some context fields.
|
||||
|
||||
**Phoenix exceptions:** Include `plug_status` for HTTP response mapping.
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:7-8`
|
||||
|
||||
```elixir
|
||||
defmodule NoRouteError do
|
||||
defexception plug_status: 404, message: "no route found", conn: nil, router: nil
|
||||
end
|
||||
|
||||
defmodule MalformedURIError do
|
||||
defexception [:message, plug_status: 400]
|
||||
end
|
||||
```
|
||||
|
||||
**Why the deviation:** In a web context, exceptions need to map to HTTP status codes. Plug's error handling middleware reads `plug_status` to determine the response code. This bridges the gap between Elixir's exception system and HTTP semantics.
|
||||
@@ -0,0 +1,235 @@
|
||||
# Phoenix Patterns
|
||||
|
||||
Patterns specific to Phoenix extracted from the framework source code.
|
||||
|
||||
## 1. Endpoint as Supervision Tree Root + Plug Pipeline
|
||||
|
||||
**Source:** `lib/phoenix/endpoint.ex:1-40` (moduledoc)
|
||||
|
||||
> The endpoint is the boundary where all requests to your web application start.
|
||||
> It is also the interface your application provides to the underlying web servers.
|
||||
>
|
||||
> Overall, an endpoint has three responsibilities:
|
||||
> - to provide a wrapper for starting and stopping the endpoint as part of a supervision tree
|
||||
> - to define an initial plug pipeline for requests to pass through
|
||||
> - to host web specific configuration for your application
|
||||
|
||||
**Source:** `lib/phoenix/endpoint.ex:408-418` (`__using__` macro)
|
||||
|
||||
```elixir
|
||||
defmacro __using__(opts) do
|
||||
quote do
|
||||
@behaviour Phoenix.Endpoint
|
||||
unquote(config(opts))
|
||||
unquote(pubsub())
|
||||
unquote(plug())
|
||||
unquote(server())
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The endpoint is four things composed together:
|
||||
1. **Config** — compile-time and runtime configuration
|
||||
2. **PubSub** — subscribe/broadcast interface
|
||||
3. **Plug** — request pipeline (via `Plug.Builder`)
|
||||
4. **Server** — supervision and HTTP server management
|
||||
|
||||
**Why:** The Endpoint is a supervisor, a plug pipeline, AND a configuration host — all in one module. This unification means one place to configure and start the entire web layer.
|
||||
|
||||
**Anti-pattern:** Splitting endpoint responsibilities across multiple unrelated modules — Phoenix deliberately consolidates the "boundary" concept.
|
||||
|
||||
---
|
||||
|
||||
## 2. Router: Compile-Time Route Optimization
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:109-123` (Why the macros? info block)
|
||||
|
||||
> We use macros for two purposes:
|
||||
>
|
||||
> * They define the routing engine, used on every request, to choose which
|
||||
> controller to dispatch the request to. Thanks to macros, Phoenix compiles
|
||||
> all of your routes to a single case-statement with pattern matching rules,
|
||||
> which is heavily optimized by the Erlang VM
|
||||
>
|
||||
> * For each route you define, we also define metadata to implement `Phoenix.VerifiedRoutes`.
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:280` (route accumulation)
|
||||
|
||||
```elixir
|
||||
Module.register_attribute(__MODULE__, :phoenix_routes, accumulate: true)
|
||||
```
|
||||
|
||||
**Why:** Routes are defined with macros that accumulate route data at compile time. At `@before_compile`, all routes are compiled into a single pattern-match dispatch function. This is O(1) routing, not O(n) list scanning.
|
||||
|
||||
**Anti-pattern:** Runtime route tables (like maps or lists that are scanned per-request) — compile-time pattern matching is orders of magnitude faster.
|
||||
|
||||
---
|
||||
|
||||
## 3. Pipeline and `pipe_through` for Request Processing
|
||||
|
||||
**Source:** `lib/phoenix/router.ex:230-260` (Pipelines and plugs section)
|
||||
|
||||
```elixir
|
||||
pipeline :browser do
|
||||
plug :fetch_session
|
||||
plug :accepts, ["html"]
|
||||
end
|
||||
|
||||
scope "/" do
|
||||
pipe_through :browser
|
||||
# routes
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Pipelines are named, composable groups of plugs. Routes declare which pipelines they pass through. This separates concerns:
|
||||
- Pipeline definition (what transformations exist)
|
||||
- Route definition (which routes use which pipelines)
|
||||
|
||||
**Anti-pattern:** Putting plug logic directly in controllers or duplicating plug chains across routes.
|
||||
|
||||
---
|
||||
|
||||
## 4. Controller as Thin Dispatch Layer
|
||||
|
||||
**Source:** `lib/phoenix/controller.ex:28-45` (moduledoc examples)
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.UserController do
|
||||
use MyAppWeb, :controller
|
||||
|
||||
def show(conn, %{"id" => id}) do
|
||||
user = Repo.get(User, id)
|
||||
render(conn, :show, user: user)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Controllers:
|
||||
- Pattern match on params (destructure what you need)
|
||||
- Call domain logic (the Repo/context layer)
|
||||
- Render the result
|
||||
|
||||
**Source:** `lib/phoenix/controller.ex:1-3` (imports)
|
||||
|
||||
```elixir
|
||||
defmodule Phoenix.Controller do
|
||||
import Plug.Conn
|
||||
alias Plug.Conn.AlreadySentError
|
||||
require Logger
|
||||
```
|
||||
|
||||
**Why:** Controllers import `Plug.Conn` for connection manipulation. They're pluggable themselves — a controller IS a plug. The action is just the last step in the plug pipeline.
|
||||
|
||||
**Anti-pattern:** Fat controllers with business logic — controllers should delegate to context modules.
|
||||
|
||||
---
|
||||
|
||||
## 5. Channel as GenServer with Topic-Based Routing
|
||||
|
||||
**Source:** `lib/phoenix/channel.ex:1-20` (topic pattern)
|
||||
|
||||
```elixir
|
||||
channel "room:*", MyAppWeb.RoomChannel
|
||||
```
|
||||
|
||||
Then in the channel:
|
||||
```elixir
|
||||
def join("room:lobby", _payload, socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def join("room:" <> room_id, _payload, socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
```
|
||||
|
||||
**Source:** `lib/phoenix/channel.ex:476-479` (channels are GenServers)
|
||||
|
||||
```elixir
|
||||
def start_link(triplet) do
|
||||
GenServer.start_link(Phoenix.Channel.Server, triplet,
|
||||
hibernate_after: @phoenix_hibernate_after
|
||||
)
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Each channel join creates a process. Pattern matching on the topic string provides natural routing. The GenServer backing means channels get supervision, hibernation, and all OTP semantics.
|
||||
|
||||
**Anti-pattern:** Managing channel state in shared ETS or external state — each channel IS its own process with its own state.
|
||||
|
||||
---
|
||||
|
||||
## 6. PubSub Integration via Endpoint
|
||||
|
||||
**Source:** `lib/phoenix/endpoint.ex:440-475` (pubsub macro)
|
||||
|
||||
```elixir
|
||||
def subscribe(topic, opts \\ []) when is_binary(topic) do
|
||||
Phoenix.PubSub.subscribe(pubsub_server!(), topic, opts)
|
||||
end
|
||||
|
||||
def broadcast(topic, event, msg) do
|
||||
Phoenix.Channel.Server.broadcast(pubsub_server!(), topic, event, msg)
|
||||
end
|
||||
|
||||
defp pubsub_server! do
|
||||
config(:pubsub_server) ||
|
||||
raise ArgumentError, "no :pubsub_server configured for #{inspect(__MODULE__)}"
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** PubSub is wired through the endpoint — `MyAppWeb.Endpoint.broadcast!("topic", "event", payload)`. The endpoint knows its pubsub server from config; channels broadcast through it transparently.
|
||||
|
||||
**Anti-pattern:** Passing PubSub server names around manually — the endpoint already knows and exposes the interface.
|
||||
|
||||
---
|
||||
|
||||
## 7. Socket as Authentication Boundary
|
||||
|
||||
**Source:** `lib/phoenix/socket.ex` (connect callback pattern)
|
||||
|
||||
```elixir
|
||||
defmodule MyAppWeb.UserSocket do
|
||||
use Phoenix.Socket
|
||||
|
||||
channel "room:*", MyAppWeb.RoomChannel
|
||||
|
||||
def connect(params, socket, _connect_info) do
|
||||
{:ok, assign(socket, :user_id, params["user_id"])}
|
||||
end
|
||||
|
||||
def id(socket), do: "users_socket:#{socket.assigns.user_id}"
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** Authentication happens ONCE at socket connection. All channels on that socket inherit the authenticated identity. `id/1` enables targeted disconnection — `Endpoint.broadcast("users_socket:123", "disconnect", %{})`.
|
||||
|
||||
**Anti-pattern:** Authenticating in every `join/3` callback instead of at the socket level.
|
||||
|
||||
---
|
||||
|
||||
## 8. Plug Pattern: `init/1` + `call/2`
|
||||
|
||||
**Source:** `lib/phoenix/router/route.ex:51-58`
|
||||
|
||||
```elixir
|
||||
@doc "Used as a plug on forwarding"
|
||||
def init(opts), do: opts
|
||||
|
||||
@doc "Used as a plug on forwarding"
|
||||
def call(%{path_info: path, script_name: script} = conn, {fwd_segments, plug, opts}) do
|
||||
new_path = path -- fwd_segments
|
||||
{base, ^new_path} = Enum.split(path, length(path) - length(new_path))
|
||||
conn = %{conn | path_info: new_path, script_name: script ++ base}
|
||||
conn = plug.call(conn, plug.init(opts))
|
||||
%{conn | path_info: path, script_name: script}
|
||||
end
|
||||
```
|
||||
|
||||
**Why:** The Plug specification splits work into:
|
||||
- `init/1` — compile-time setup (called once, result cached)
|
||||
- `call/2` — runtime execution (called per-request, must be fast)
|
||||
|
||||
This is Phoenix's fundamental composition pattern. Everything is a plug.
|
||||
|
||||
**Anti-pattern:** Doing expensive setup work in `call/2` instead of `init/1` — it runs on every request.
|
||||
@@ -0,0 +1,181 @@
|
||||
# Anti-Patterns
|
||||
|
||||
Things the Elixir and Phoenix source deliberately avoids — and why you should too.
|
||||
|
||||
## 1. GenServer for Pure Functions
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:533-575` ("When (not) to use a GenServer")
|
||||
|
||||
The GenServer docs explicitly say:
|
||||
|
||||
> If you don't need a process, then you don't need a process.
|
||||
|
||||
**What they avoid:** Creating a GenServer to wrap stateless computation.
|
||||
|
||||
**Why:** A process adds message passing overhead, serialization (one request at a time), and supervision complexity — all unnecessary for pure functions.
|
||||
|
||||
**Do this instead:** A plain module with functions:
|
||||
```elixir
|
||||
# Good: pure function, no process needed
|
||||
defmodule MyApp.Calculator do
|
||||
def add(a, b), do: a + b
|
||||
end
|
||||
|
||||
# Bad: unnecessary process
|
||||
defmodule MyApp.Calculator do
|
||||
use GenServer
|
||||
def add(a, b), do: GenServer.call(__MODULE__, {:add, a, b})
|
||||
def handle_call({:add, a, b}, _from, state), do: {:reply, a + b, state}
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Dynamic Atoms for Process Names
|
||||
|
||||
**Source:** `lib/elixir/lib/registry.ex:28-60` (Registry as alternative to atoms)
|
||||
|
||||
The Registry module exists specifically because dynamic atom creation is dangerous:
|
||||
|
||||
> atoms are never garbage collected
|
||||
|
||||
**What they avoid:** `String.to_atom("worker_#{id}")`
|
||||
|
||||
**What they do instead:**
|
||||
```elixir
|
||||
{:via, Registry, {MyApp.Registry, "worker-#{id}"}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Broad `import` Without `:only`
|
||||
|
||||
**Source:** `lib/elixir/lib/enum.ex:250`
|
||||
|
||||
```elixir
|
||||
import Kernel, except: [max: 2, min: 2]
|
||||
```
|
||||
|
||||
Even within the standard library, imports are scoped. Enum explicitly excludes the specific Kernel functions it replaces.
|
||||
|
||||
**What they avoid:** `import Kernel` without qualification when they define conflicting names.
|
||||
|
||||
**Exception:** Phoenix Router (`lib/phoenix/router.ex:274-276`) imports broadly — but it's a DSL where usability trumps explicitness.
|
||||
|
||||
---
|
||||
|
||||
## 4. Exceptions for Control Flow
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:455-460` (async documentation on error handling)
|
||||
|
||||
> an asynchronous task should be thought of as an extension of the
|
||||
> caller process rather than a mechanism to isolate it from all errors.
|
||||
|
||||
The Task documentation advises returning `{:ok, val} | :error` for normal flow, NOT using try/rescue:
|
||||
|
||||
> For example, to either return `{:ok, val} | :error` results or,
|
||||
> in more extreme cases, by using `try/rescue`
|
||||
|
||||
**What they avoid:** Using `try/rescue` around expected failure cases.
|
||||
|
||||
**Why:** Pattern matching on tagged tuples is more explicit, composable (works with `with`), and doesn't hide the error path.
|
||||
|
||||
---
|
||||
|
||||
## 5. Trapping Exits in Normal Code
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:469-477` (explicit warning)
|
||||
|
||||
> Setting `:trap_exit` to `true` - trapping exits should be used only in special
|
||||
> circumstances as it would make your process immune to not only exits from the
|
||||
> task but from any other processes.
|
||||
>
|
||||
> Moreover, even when trapping exits, calling `await` will still exit if the
|
||||
> task has terminated without sending its result back.
|
||||
|
||||
**What they avoid:** `Process.flag(:trap_exit, true)` in normal application code.
|
||||
|
||||
**Why:** Trapping exits breaks the supervision contract. A supervisor expects to be able to kill its children — if they trap exits, shutdown semantics change unpredictably.
|
||||
|
||||
---
|
||||
|
||||
## 6. Expensive Work in `init/1`
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:127-145` (handle_continue pattern)
|
||||
|
||||
```elixir
|
||||
# What NOT to do — blocks the supervisor
|
||||
def init(url) do
|
||||
data = HTTP.get!(url) # BAD: blocks here
|
||||
{:ok, data}
|
||||
end
|
||||
|
||||
# What TO do — return immediately, do work later
|
||||
def init(url) do
|
||||
{:ok, :unset, {:continue, {:fetch, url}}}
|
||||
end
|
||||
|
||||
def handle_continue({:fetch, url}, _state) do
|
||||
{:noreply, HTTP.get!(url)}
|
||||
end
|
||||
```
|
||||
|
||||
**What they avoid:** Network calls, disk I/O, or any slow operation in `init/1`.
|
||||
|
||||
**Why:** `init/1` blocks `start_link`, which blocks the supervisor. If your init takes 5 seconds, the entire supervision tree startup stalls.
|
||||
|
||||
---
|
||||
|
||||
## 7. Unlinking Task Processes
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:478-482`
|
||||
|
||||
> Unlinking the task process started with `async`/`await`. If you unlink the
|
||||
> processes and the task does not belong to any supervisor, you may leave
|
||||
> dangling tasks in case the caller process dies.
|
||||
|
||||
**What they avoid:** `Process.unlink/1` on task processes.
|
||||
|
||||
**Why:** The link is a safety mechanism. If the caller dies, the task should die too (since nobody will read the result). Unlinking creates orphan processes.
|
||||
|
||||
---
|
||||
|
||||
## 8. Blocking the Agent with Expensive Computation
|
||||
|
||||
**Source:** `lib/elixir/lib/agent.ex:62-82` (client vs server computation)
|
||||
|
||||
```elixir
|
||||
# BAD: blocks the agent, other callers queue up
|
||||
def get_something(agent) do
|
||||
Agent.get(agent, fn state -> do_something_expensive(state) end)
|
||||
end
|
||||
|
||||
# GOOD: copies state to caller, work happens in caller's process
|
||||
def get_something(agent) do
|
||||
Agent.get(agent, & &1) |> do_something_expensive()
|
||||
end
|
||||
```
|
||||
|
||||
**What they avoid:** Running expensive operations inside the Agent's process.
|
||||
|
||||
**Why:** The Agent is a single process. While it's computing, ALL other get/update/cast operations queue up. Move computation to the caller unless atomicity is required.
|
||||
|
||||
---
|
||||
|
||||
## 9. Raw `spawn` Instead of Supervised Processes
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:24-26` (why Task over spawn)
|
||||
|
||||
> Compared to plain processes, started with `spawn/1`, tasks include monitoring
|
||||
> metadata and logging in case of errors.
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:100-115`
|
||||
|
||||
> We encourage developers to rely on supervised tasks as much as possible.
|
||||
> Supervised tasks improve the visibility of how many tasks are running
|
||||
> at a given moment and enable a variety of patterns that give you
|
||||
> explicit control on how to handle the results, errors, and timeouts.
|
||||
|
||||
**What they avoid:** `spawn/1` and `spawn_link/1` in application code.
|
||||
|
||||
**Why:** Unsupervised processes are invisible to the system. They don't appear in observer, don't get logged on crash, and can't be gracefully shut down.
|
||||
@@ -0,0 +1,265 @@
|
||||
# Common Mistakes
|
||||
|
||||
What "bad Elixir" looks like, based on what the source code explicitly warns against or demonstrates the correct way to avoid.
|
||||
|
||||
## 1. Using `++` in a Loop (O(n²) List Building)
|
||||
|
||||
**Source:** `lib/elixir/lib/enum.ex:306-320` (internal macros all use prepend)
|
||||
|
||||
```elixir
|
||||
# What the source does: prepend then reverse
|
||||
defmacrop next(_, entry, acc) do
|
||||
quote(do: [unquote(entry) | unquote(acc)])
|
||||
end
|
||||
```
|
||||
|
||||
**The mistake:**
|
||||
```elixir
|
||||
# O(n²) — copies the entire left list for every element
|
||||
Enum.reduce(items, [], fn item, acc -> acc ++ [transform(item)] end)
|
||||
```
|
||||
|
||||
**The fix:**
|
||||
```elixir
|
||||
# O(n) — prepend is O(1), reverse once at the end
|
||||
items |> Enum.map(&transform/1)
|
||||
# or
|
||||
Enum.reduce(items, [], fn item, acc -> [transform(item) | acc] end) |> Enum.reverse()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Forgetting `@impl true`
|
||||
|
||||
**Source:** `lib/elixir/lib/gen_server.ex:44-56` (every callback uses @impl)
|
||||
|
||||
**The mistake:**
|
||||
```elixir
|
||||
defmodule MyServer do
|
||||
use GenServer
|
||||
|
||||
# Typo! This will never be called — no warning without @impl
|
||||
def handle_cll(msg, _from, state) do
|
||||
{:reply, msg, state}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**The fix:**
|
||||
```elixir
|
||||
@impl true
|
||||
def handle_call(msg, _from, state) do
|
||||
{:reply, msg, state}
|
||||
end
|
||||
```
|
||||
|
||||
With `@impl true`, the compiler catches the typo at compile time.
|
||||
|
||||
---
|
||||
|
||||
## 3. Not Handling All `with` Failure Cases
|
||||
|
||||
**Source:** `lib/elixir/lib/kernel/special_forms.ex:1680-1715` (with Beware! section)
|
||||
|
||||
**The mistake:**
|
||||
```elixir
|
||||
with {:ok, width} <- Map.fetch(opts, "width"),
|
||||
{:ok, height} <- Map.fetch(opts, "height") do
|
||||
{:ok, width * height}
|
||||
else
|
||||
# Only handles one case — what if Map.fetch returns something else?
|
||||
:error -> {:error, :missing_field}
|
||||
end
|
||||
```
|
||||
|
||||
If an `else` block is used and no clause matches, a `WithClauseError` is raised.
|
||||
|
||||
**The fix:** Either handle all possible non-match values in `else`, or better yet, normalize return values in helper functions so you don't need `else` at all.
|
||||
|
||||
---
|
||||
|
||||
## 4. async Without await
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:38-40`
|
||||
|
||||
> If you are using async tasks, you **must await** a reply as they are *always* sent.
|
||||
|
||||
**The mistake:**
|
||||
```elixir
|
||||
# Leaked reference — message sits in mailbox forever
|
||||
Task.async(fn -> send_email(user) end)
|
||||
# Never awaited!
|
||||
```
|
||||
|
||||
**The fix:**
|
||||
```elixir
|
||||
# Fire-and-forget: use start_child
|
||||
Task.Supervisor.start_child(MyApp.TaskSupervisor, fn -> send_email(user) end)
|
||||
|
||||
# OR if you need the result:
|
||||
task = Task.async(fn -> send_email(user) end)
|
||||
Task.await(task)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Anonymous Functions in Distributed Agents
|
||||
|
||||
**Source:** `lib/elixir/lib/agent.ex:141-160` ("A word on distributed agents")
|
||||
|
||||
> In a distributed setup with multiple nodes, the API that accepts anonymous
|
||||
> functions only works if the caller (client) and the agent have the same
|
||||
> version of the caller module.
|
||||
|
||||
**The mistake:**
|
||||
```elixir
|
||||
# Fails if nodes have different code versions
|
||||
Agent.get({MyAgent, :remote@node}, fn state -> state.count end)
|
||||
```
|
||||
|
||||
**The fix:**
|
||||
```elixir
|
||||
# Use MFA (module, function, args) for distributed calls
|
||||
Agent.get({MyAgent, :remote@node}, MyModule, :get_count, [])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Starting Processes Outside Supervision Trees
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:100-115`
|
||||
|
||||
> We encourage developers to rely on supervised tasks as much as possible.
|
||||
|
||||
**The mistake:**
|
||||
```elixir
|
||||
# No supervision, no monitoring, no logging
|
||||
spawn(fn -> do_important_work() end)
|
||||
|
||||
# Or:
|
||||
Task.async(fn -> do_important_work() end) |> Task.await()
|
||||
# Linked to caller but not supervised
|
||||
```
|
||||
|
||||
**The fix:**
|
||||
```elixir
|
||||
# Add to your supervision tree:
|
||||
{Task.Supervisor, name: MyApp.TaskSupervisor}
|
||||
|
||||
# Then use it:
|
||||
Task.Supervisor.start_child(MyApp.TaskSupervisor, fn -> do_important_work() end)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Putting State Logic in Controllers
|
||||
|
||||
**Source:** `lib/phoenix/controller.ex:28-45`
|
||||
|
||||
Controllers are shown as thin dispatch layers:
|
||||
```elixir
|
||||
def show(conn, %{"id" => id}) do
|
||||
user = Repo.get(User, id)
|
||||
render(conn, :show, user: user)
|
||||
end
|
||||
```
|
||||
|
||||
**The mistake:**
|
||||
```elixir
|
||||
def create(conn, params) do
|
||||
# Business logic in the controller
|
||||
changeset = User.changeset(%User{}, params)
|
||||
if changeset.valid? do
|
||||
user = Repo.insert!(changeset)
|
||||
send_welcome_email(user)
|
||||
update_analytics(user)
|
||||
notify_admin(user)
|
||||
render(conn, :show, user: user)
|
||||
else
|
||||
render(conn, :error, errors: changeset.errors)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**The fix:** Move business logic to a context module. The controller just dispatches:
|
||||
```elixir
|
||||
def create(conn, params) do
|
||||
case Accounts.register_user(params) do
|
||||
{:ok, user} -> render(conn, :show, user: user)
|
||||
{:error, changeset} -> render(conn, :error, errors: changeset.errors)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Using `:permanent` Restart for One-Shot Tasks
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:178-186`
|
||||
|
||||
> a Task has a default `:restart` of `:temporary`. This means the task will
|
||||
> not be restarted even if it crashes.
|
||||
|
||||
**The mistake:**
|
||||
```elixir
|
||||
# Will restart infinitely if the HTTP call keeps failing
|
||||
use Task, restart: :permanent
|
||||
|
||||
def start_link(url) do
|
||||
Task.start_link(fn -> HTTP.get!(url) end)
|
||||
end
|
||||
```
|
||||
|
||||
**The fix:** Use `:temporary` (default) for one-shot work. Use `:transient` if you want restart only on abnormal exit:
|
||||
```elixir
|
||||
use Task, restart: :transient
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Pattern Matching the Internals of Opaque Types
|
||||
|
||||
**Source:** `lib/elixir/lib/task.ex:298-300`
|
||||
|
||||
```elixir
|
||||
@opaque ref :: reference()
|
||||
```
|
||||
|
||||
**The mistake:**
|
||||
```elixir
|
||||
# Accessing internal structure of an opaque type
|
||||
%Task{ref: ref} = task
|
||||
send(ref, :custom_message) # This breaks if internals change
|
||||
```
|
||||
|
||||
**The fix:** Use the public API. If a type is `@opaque`, its structure is not guaranteed between versions. Use functions like `Task.await/2` that work with the type properly.
|
||||
|
||||
---
|
||||
|
||||
## 10. Not Using `on_exit` for Test Cleanup
|
||||
|
||||
**Source:** `lib/ex_unit/lib/ex_unit/case.ex:86-94`
|
||||
|
||||
**The mistake:**
|
||||
```elixir
|
||||
test "writes to file" do
|
||||
File.write!("/tmp/test_file", "data")
|
||||
assert File.read!("/tmp/test_file") == "data"
|
||||
File.rm!("/tmp/test_file") # Never runs if assert above fails!
|
||||
end
|
||||
```
|
||||
|
||||
**The fix:**
|
||||
```elixir
|
||||
setup do
|
||||
path = "/tmp/test_file_#{System.unique_integer()}"
|
||||
on_exit(fn -> File.rm(path) end)
|
||||
{:ok, path: path}
|
||||
end
|
||||
|
||||
test "writes to file", %{path: path} do
|
||||
File.write!(path, "data")
|
||||
assert File.read!(path) == "data"
|
||||
# Cleanup happens automatically, even on failure
|
||||
end
|
||||
```
|
||||
Reference in New Issue
Block a user