docs: backfill TOC + decision trees, fix review findings

- Add ## Contents and ## Decision Tree to all 10 existing pattern files
- Fix embed_as/1 semantics inversion in types.md (:self → :dump)
- Fix fabricated __meta__.changes reference in changesets.md
- Fix default primary key type (:integer → :id) in schemas.md
- Combine @impl subsections into single "Minimal Callback Annotation"
This commit is contained in:
2026-05-01 22:13:35 -07:00
parent b33accf37c
commit 10218813d3
13 changed files with 356 additions and 87 deletions
+23
View File
@@ -2,6 +2,18 @@
How behaviours are designed, implemented, and used in Elixir core and Phoenix.
## Contents
1. [Behaviour Definition with `@callback`](#1-behaviour-definition-with-callback)
2. [`@optional_callbacks` for Extensibility](#2-optional_callbacks-for-extensibility)
3. [`@behaviour` Declaration in `__using__`](#3-behaviour-declaration-in-__using__)
4. [Default Implementations via `defoverridable`](#4-default-implementations-via-defoverridable)
5. [Phoenix Channel: Behaviour + Process + Protocol](#5-phoenix-channel-behaviour--process--protocol)
6. [Callback Documentation Pattern](#6-callback-documentation-pattern)
7. [Phoenix.Endpoint: Behaviour as Interface Contract](#7-phoenixendpoint-behaviour-as-interface-contract)
---
## 1. Behaviour Definition with `@callback`
**Source:** [lib/elixir/lib/gen_server.ex#L577](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L577) (all callback definitions)
@@ -678,4 +690,15 @@ end
**Why:** The more code a `use` macro generates, the harder it is to debug. If users regularly need to read the generated code to understand failures, the abstraction is leaking. Reserve heavy `use` macros for well-established patterns (GenServer, Endpoint, Channel) where the community has internalized the mental model.
## Decision Tree
- If you need a contract that multiple modules will implement differently → define a behaviour with `@callback` (Pattern 1)
- If most implementors will use a default for some callbacks → mark those `@optional_callbacks` (Pattern 2)
- If your behaviour requires boilerplate setup (module attributes, compile hooks) → inject `@behaviour` inside `__using__` (Pattern 3)
- If 90% of implementors want the same default for a callback → provide a `defoverridable` implementation (Pattern 4)
- If the behaviour involves a running process with lifecycle configuration → combine behaviour + process + module attributes (Pattern 5)
- If callback semantics are non-obvious (multiple return shapes, triggering conditions) → write comprehensive `@doc` with examples on each `@callback` (Pattern 6)
- If the behaviour requires significant generated boilerplate (plugs, routing, supervision wiring) → use the `use` macro as the full interface contract (Pattern 7)
- If there is only one implementation and no plans for more → skip the behaviour, use a plain module
<!-- PATTERN_COMPLETE -->
+9 -10
View File
@@ -328,17 +328,16 @@ end
**Anti-pattern:** Reimplementing the "has change?" check inside the validator:
```elixir
# BAD — redundant check; validate_change already handles this
# BAD — redundant check; validate_change already skips if :sku is unchanged
def changeset(item, params) do
item
|> cast(params, [:sku])
|> validate_change(:sku, fn :sku, value ->
if Map.has_key?(item.__meta__.changes, :sku) do
if valid_sku?(value), do: [], else: [{:sku, "invalid format"}]
else
[]
end
end)
changeset = cast(item, params, [:sku])
if Map.has_key?(changeset.changes, :sku) do
value = get_change(changeset, :sku)
if valid_sku?(value), do: changeset, else: add_error(changeset, :sku, "invalid format")
else
changeset
end
end
```
+28
View File
@@ -2,6 +2,21 @@
Patterns extracted from Elixir's standard library source code.
## Contents
1. [List-Specialized Clause Before Protocol Dispatch](#1-list-specialized-clause-before-protocol-dispatch)
2. [Build-Then-Reverse (Cons-Cell Accumulation)](#2-build-then-reverse-cons-cell-accumulation)
3. [Pipeline for Linear Transformations, Bare Calls for Control Flow](#3-pipeline-for-linear-transformations-bare-calls-for-control-flow)
4. [Pipeline Ending with `|> elem(1)` (Protocol Reduce Unwrap)](#4-pipeline-ending-with--elem1-protocol-reduce-unwrap)
5. [Private Helper Decomposition: Recursive Workers with Guards](#5-private-helper-decomposition-recursive-workers-with-guards)
6. [Enum vs Stream Decision Pattern](#6-enum-vs-stream-decision-pattern)
7. [Map.update vs Map.put Decision Pattern](#7-mapupdate-vs-mapput-decision-pattern)
8. [Pattern Matching on Map Structure for Dispatch](#8-pattern-matching-on-map-structure-for-dispatch)
9. [Delegating to Erlang BIFs with `defdelegate`](#9-delegating-to-erlang-bifs-with-defdelegate)
10. [Reduce as the Universal Primitive](#10-reduce-as-the-universal-primitive)
11. [Keyword Multi-Clause Guard Dispatch (String.split pattern)](#11-keyword-multi-clause-guard-dispatch-stringsplit-pattern)
12. [Lazy Private Helpers with `defp parts_to_index`](#12-lazy-private-helpers-with-defp-parts_to_index)
---
## 1. List-Specialized Clause Before Protocol Dispatch
@@ -1010,4 +1025,17 @@ def log(msg) when is_atom(msg), do: IO.puts(Atom.to_string(msg))
**Why:** When a conversion is used exactly once and the calling function already dispatches on clauses, folding the conversion into the caller's clauses reduces indirection. Named helpers shine when reused or when they name a non-obvious transformation.
## Decision Tree
- If you accept "any enumerable" but lists are the common case → add a `when is_list` clause before protocol dispatch (Pattern 1)
- If you are building a result list element-by-element and order matters → prepend with `[x | acc]` then reverse at the end (Pattern 2)
- If data flows through 2+ sequential transformations → use the pipe operator (Pattern 3)
- If you call `Enumerable.reduce/3` directly and always want the accumulated value → unwrap with `|> elem(1)` (Pattern 4)
- If you need a recursive function with multiple termination conditions → decompose into public entry + private multi-clause worker (Pattern 5)
- If the collection is large/infinite or you chain 3+ transforms → use Stream; otherwise use Enum (Pattern 6)
- If the new value depends on the old value (increment, append) → use `Map.update/4`; if replacing unconditionally → use `Map.put/3` (Pattern 7)
- If you need to branch on whether a key exists and extract the value → pattern-match with `%{^key => value}` in a `case` (Pattern 8)
- If an Erlang function has identical semantics and argument order → use `defdelegate` (Pattern 9)
- If you are implementing a custom iterable data structure → implement `Enumerable.reduce/3` to get the full Enum API (Pattern 10)
<!-- PATTERN_COMPLETE -->
+27
View File
@@ -2,6 +2,20 @@
Patterns extracted from the Elixir standard library source code.
## Contents
1. [@moduledoc with Structured Sections](#1-moduledoc-with-structured-sections)
2. [@doc with Sections and Examples](#2-doc-with-sections-and-examples)
3. [@doc since: Version Annotation](#3-doc-since-version-annotation)
4. [@doc guard: true Metadata](#4-doc-guard-true-metadata)
5. [@doc false — Hiding from Documentation](#5-doc-false--hiding-from-documentation)
6. [@moduledoc false — Hiding Modules](#6-moduledoc-false--hiding-modules)
7. [Mermaid Diagrams in Documentation](#7-mermaid-diagrams-in-documentation)
8. [Admonition Blocks in Documentation](#8-admonition-blocks-in-documentation)
9. [@doc deprecated: Soft Deprecation](#9-doc-deprecated-soft-deprecation)
10. [Callback Documentation Convention](#10-callback-documentation-convention)
11. [Documentation with Link References (c: and t: prefixes)](#11-documentation-with-link-references-c-and-t-prefixes)
---
## 1. @moduledoc with Structured Sections
@@ -1000,4 +1014,17 @@ Returns `true` if the calling process is the owner of this resource.
**Why:** Link references should aid navigation, not turn documentation into hypertext soup. Link types and callbacks that users might need to look up; don't link primitive types or universally known functions.
## Decision Tree
- If the module is a primary entry point with 4+ public functions → use structured `@moduledoc` with sections (Pattern 1)
- If a function has non-obvious behavior or edge cases → add `@doc` with sections and `## Examples` doctests (Pattern 2)
- If adding a new public function to a versioned library → annotate with `@doc since: "X.Y.Z"` (Pattern 3)
- If the function/macro is valid in guard clauses → add `@doc guard: true` metadata (Pattern 4)
- If a function must be public for technical reasons but is not user-facing → use `@doc false` (Pattern 5)
- If an entire module is purely internal implementation → use `@moduledoc false` (Pattern 6)
- If documenting multi-component architecture (client-server, pipelines) → embed a Mermaid diagram (Pattern 7)
- If critical information must stand out (security, breaking changes, `use` behavior) → use an admonition block (Pattern 8)
- If a function still works but a better alternative exists → use `@doc deprecated:` for soft deprecation (Pattern 9)
- If defining a behaviour callback with multiple return shapes → write comprehensive callback docs with trigger, params, returns, and example (Pattern 10)
<!-- PATTERN_COMPLETE -->
+32
View File
@@ -2,6 +2,23 @@
Patterns extracted from Elixir's standard library source code.
## Contents
1. [The `with` Macro — Normalized Error Clauses](#1-the-with-macro--normalized-error-clauses)
2. [Real-World `with` — Multi-Step Fallible Operations](#2-real-world-with--multi-step-fallible-operations)
3. [Another `with` — Error Info Extraction](#3-another-with--error-info-extraction)
4. [`{:ok, value}` / `:error` Convention (Map.fetch)](#4-ok-value--error-convention-mapfetch)
5. [Bang Functions: Raise on Error (`fetch!` vs `fetch`)](#5-bang-functions-raise-on-error-fetch-vs-fetch)
6. [Exception Structure: `defexception` Fields](#6-exception-structure-defexception-fields)
7. [Custom `exception/1` Callback for Ergonomic Raising](#7-custom-exception1-callback-for-ergonomic-raising)
8. [`raise` Macro Internals: Compile-Time Type Resolution](#8-raise-macro-internals-compile-time-type-resolution)
9. [Error Normalization: Erlang → Elixir Exception Translation](#9-error-normalization-erlang--elixir-exception-translation)
10. [`blame/2` Callback: Enriching Exceptions After the Fact](#10-blame2-callback-enriching-exceptions-after-the-fact)
11. [Guards for Type Dispatch in Error Handling](#11-guards-for-type-dispatch-in-error-handling)
12. [The `:error` / `{:error, reason}` Convention Split](#12-the-error--error-reason-convention-split)
13. [`reduce_while` — Early Exit Without Exceptions](#13-reduce_while--early-exit-without-exceptions)
14. [Three-Tier Error Strategy in Map Operations](#14-three-tier-error-strategy-in-map-operations)
---
## 1. The `with` Macro — Normalized Error Clauses
@@ -1400,4 +1417,19 @@ end
**Why:** The three-tier pattern only makes sense when failure is a real possibility and different callers genuinely need different responses to that failure. Don't cargo-cult it onto functions that always succeed or have a single calling context.
## Decision Tree
- If you have 2+ sequential steps that each return a value to pattern-match → use `with` with normalized error shapes (Pattern 1)
- If the caller only cares success vs failure (not which step failed) → use `with` + `else _ -> :error` catch-all (Pattern 2)
- If extracting nested data from loosely-structured inputs (stacktraces, metadata) → chain pattern matching in `with` (Pattern 3)
- If a function has exactly one failure mode obvious from context → return bare `:error` (Pattern 4)
- If failure means a bug (preconditions guarantee success) → provide a bang variant that raises (Pattern 5)
- If callers need to programmatically inspect error context → use `defexception` with structured fields (Pattern 6)
- If the exception message is computed from multiple fields or requires validation → override `exception/1` (Pattern 7)
- If wrapping an Erlang library that returns raw error atoms/tuples → normalize to Elixir exceptions at the boundary (Pattern 9)
- If you can provide expensive but helpful context (did-you-mean suggestions) → implement `blame/2` (Pattern 10)
- If multiple distinct failure modes exist → use `{:error, reason}` tuples; if only one → use bare `:error` (Pattern 12)
- If you need early exit from iteration without exceptions → use `reduce_while` with `{:cont, acc}` / `{:halt, acc}` (Pattern 13)
- If designing a module with lookup operations for different caller needs → provide three tiers: get/fetch/fetch! (Pattern 14)
<!-- PATTERN_COMPLETE -->
+61 -66
View File
@@ -2,6 +2,21 @@
Analysis of `lib/elixir/lib/gen_server.ex`, `lib/elixir/lib/agent.ex`, and related modules.
## Contents
1. [Pattern 1: Client/Server API Separation](#pattern-1-clientserver-api-separation)
2. [Pattern 2: `@impl true` Annotations on All Callbacks](#pattern-2-impl-true-annotations-on-all-callbacks)
3. [Pattern 3: Guard-Protected `start_link`](#pattern-3-guard-protected-start_link)
4. [Pattern 4: `handle_continue` for Post-Init Work](#pattern-4-handle_continue-for-post-init-work)
5. [Pattern 5: Timeout-Based Idle Shutdown](#pattern-5-timeout-based-idle-shutdown)
6. [Pattern 6: Periodic Work via `Process.send_after`](#pattern-6-periodic-work-via-processsend_after)
7. [Pattern 7: Call vs Cast Decision (Synchronous vs Asynchronous)](#pattern-7-call-vs-cast-decision-synchronous-vs-asynchronous)
8. [Pattern 8: Default Callback Implementations with Clear Error Messages](#pattern-8-default-callback-implementations-with-clear-error-messages)
9. [Pattern 9: `child_spec/1` Generation and Customization via `use` Options](#pattern-9-child_spec1-generation-and-customization-via-use-options)
10. [Pattern 10: Agent as Minimal State Wrapper (GenServer Under the Hood)](#pattern-10-agent-as-minimal-state-wrapper-genserver-under-the-hood)
11. [Pattern 11: Name Registration via `:via` Tuple](#pattern-11-name-registration-via-via-tuple)
12. [Pattern 12: GenServer as Anti-Pattern — Don't Use Processes for Code Organization](#pattern-12-genserver-as-anti-pattern--dont-use-processes-for-code-organization)
---
## Pattern 1: Client/Server API Separation
@@ -189,75 +204,35 @@ end
**Why:** `@impl true` only makes sense in the context of a declared behaviour. Using it without one causes a compiler warning, not a benefit.
### Multi-Clause Callbacks — `@impl` on First Clause Only
### Minimal Callback Annotation
**Source:** [lib/elixir/lib/module.ex#L72](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/module.ex#L72) (`@impl` documentation)
**What it does:** `@impl true` is a module attribute that applies to the function as a whole — all clauses, not just the one it precedes. Place it once before the first clause; subsequent clauses of the same function inherit the annotation.
**What it does:** Once `@impl true` is on a callback, two things become redundant:
**Why:** Repeating `@impl true` on every clause is noise. Worse, it can mislead readers into thinking each clause is a separate function. The compiler already associates all clauses with the behaviour callback after seeing `@impl` on the first one. Additionally, if you mark one callback with `@impl`, the compiler requires all callbacks in that module to be marked — this enforcement operates at the function level, not the clause level.
1. **Repeating `@impl true` on subsequent clauses** — the annotation applies to the function as a whole, not individual clauses. All clauses inherit it from the first.
2. **Adding `@spec`** — the behaviour's `@callback` already defines the type contract. Dialyzer uses the callback spec to check implementations, so a redundant `@spec` creates a second source of truth that can drift.
**Anti-pattern:** Annotating every clause:
**Why:** The behaviour owns the contract. Adding `@spec init(term()) :: {:ok, map()}` on a GenServer callback just restates `@callback init(init_arg :: term) :: {:ok, state} | ...` with less information. Repeating `@impl true` on every clause is noise that misleads readers into thinking each clause is a separate function. The minimal annotation communicates "this is a callback, the behaviour defines the contract."
**Anti-pattern:**
```elixir
# BAD — @impl repeated on each clause of the same function
@impl true
def handle_call({:get, key}, _from, state) do
{:reply, Map.get(state, key), state}
end
@impl true
def handle_call({:keys}, _from, state) do
{:reply, Map.keys(state), state}
end
@impl true
def handle_call({:size}, _from, state) do
{:reply, map_size(state), state}
end
```
**Example — after:**
```elixir
@impl true
def handle_call({:get, key}, _from, state) do
{:reply, Map.get(state, key), state}
end
def handle_call({:keys}, _from, state) do
{:reply, Map.keys(state), state}
end
def handle_call({:size}, _from, state) do
{:reply, map_size(state), state}
end
```
**When NOT to apply this:** If clauses of the same callback are separated by other functions (non-consecutive definitions), the compiler may not associate them. Keep multi-clause callbacks grouped together — this is independently good practice for readability.
### Omit `@spec` on `@impl true` Callbacks
**Source:** [lib/elixir/lib/module.ex#L121](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/module.ex#L121) (`@impl` marks function as `@doc false`)
**What it does:** When `@impl true` is present, omit the `@spec`. The behaviour's `@callback` already defines the type contract for the function. Dialyzer uses the callback spec to check implementations — a redundant `@spec` on the implementation adds no safety and creates a second source of truth that can drift.
**Why:** The behaviour owns the contract. Adding `@spec init(term()) :: {:ok, map()}` on a GenServer callback just restates `@callback init(init_arg :: term) :: {:ok, state} | ...` with less information (the callback documents the full union of valid return types). When the behaviour updates its callback spec, implementations with stale `@spec` annotations silently disagree.
**Anti-pattern:** Verbatim-copying the callback spec onto the implementation:
```elixir
# BAD — spec restates what the @callback already defines
@spec init(term()) :: {:ok, map()}
@impl true
def init(opts) do
{:ok, %{started_at: DateTime.utc_now(), config: opts}}
end
# BAD — redundant @spec and @impl on every clause
@spec handle_call(term(), GenServer.from(), map()) :: {:reply, term(), map()}
@impl true
def handle_call(:status, _from, state) do
{:reply, state.started_at, state}
def handle_call({:get, key}, _from, state) do
{:reply, Map.get(state, key), state}
end
@impl true
def handle_call({:keys}, _from, state) do
{:reply, Map.keys(state), state}
end
@impl true
def handle_call({:size}, _from, state) do
{:reply, map_size(state), state}
end
```
@@ -265,17 +240,23 @@ end
```elixir
@impl true
def init(opts) do
{:ok, %{started_at: DateTime.utc_now(), config: opts}}
def handle_call({:get, key}, _from, state) do
{:reply, Map.get(state, key), state}
end
@impl true
def handle_call(:status, _from, state) do
{:reply, state.started_at, state}
def handle_call({:keys}, _from, state) do
{:reply, Map.keys(state), state}
end
def handle_call({:size}, _from, state) do
{:reply, map_size(state), state}
end
```
**When NOT to apply this:** When the implementation intentionally accepts a narrower type than the callback declares, a more specific `@spec` documents that constraint for readers and Dialyzer:
**When NOT to apply this:**
- **Non-consecutive clauses:** If clauses of the same callback are separated by other functions, the compiler may not associate them. Keep multi-clause callbacks grouped together.
- **Intentionally narrower types:** When the implementation accepts a narrower type than the callback declares, a more specific `@spec` documents that constraint:
```elixir
# Justified — documents that this init/1 only accepts keyword lists,
@@ -1209,4 +1190,18 @@ end
**Why:** The pattern cuts both ways. Over-using GenServer creates bottlenecks. Under-using it means reinventing state management poorly. The litmus test: does the state need to survive between function calls? Does access need serialization? If yes, you need a process.
## Decision Tree
- If other modules will interact with your GenServer → define a client API wrapping call/cast (Pattern 1)
- If implementing any behaviour callback → annotate with `@impl true` (Pattern 2)
- If `start_link` accepts arguments with a specific expected shape → add guards for fail-fast validation (Pattern 3)
- If `init/1` does expensive work (DB, network, cache warming) → split into fast init + `handle_continue` (Pattern 4)
- If the process is ephemeral (per-user, per-session) and should clean up when idle → use timeout-based idle shutdown (Pattern 5)
- If you need work at regular intervals regardless of message traffic → use `Process.send_after` self-scheduling loop (Pattern 6)
- If the caller needs confirmation or backpressure → use `call`; only use `cast` for genuine fire-and-forget (Pattern 7)
- If the process needs non-default restart/shutdown behavior → customize via `use GenServer` options (Pattern 9)
- If the process is purely about state (no custom messages, no timers) → use Agent instead of GenServer (Pattern 10)
- If spawning processes dynamically with unbounded names → use `{:via, Registry, ...}` to avoid atom leaks (Pattern 11)
- If the operation is stateless pure computation → don't use a GenServer at all, use a plain function (Pattern 12)
<!-- PATTERN_COMPLETE -->
+29
View File
@@ -2,6 +2,21 @@
Patterns extracted from the Elixir standard library source code.
## Contents
1. [Context-Aware Macros (__CALLER__.context)](#1-context-aware-macros-__caller__context)
2. [defguard — Macro for Guard-Safe Expressions](#2-defguard--macro-for-guard-safe-expressions)
3. [quote + unquote for Code Generation](#3-quote--unquote-for-code-generation)
4. [var! for Breaking Hygiene](#4-var-for-breaking-hygiene)
5. [Macro Expanding with Macro.expand](#5-macro-expanding-with-macroexpand)
6. [assert_no_match_or_guard_scope Pattern](#6-assert_no_match_or_guard_scope-pattern)
7. [Protocol Definition as a Macro (defprotocol)](#7-protocol-definition-as-a-macro-defprotocol)
8. [@fallback_to_any in Protocols](#8-fallback_to_any-in-protocols)
9. [use/2 as Macro Injection Point](#9-use2-as-macro-injection-point)
10. [Sigil Macros (Pattern for DSL Literals)](#10-sigil-macros-pattern-for-dsl-literals)
11. [Pipe Operator as a Macro](#11-pipe-operator-as-a-macro)
12. [Macro.generate_unique_arguments for Hygiene](#12-macrogenerate_unique_arguments-for-hygiene)
---
## 1. Context-Aware Macros (__CALLER__.context)
@@ -1105,4 +1120,18 @@ end
**Why:** Variables created in `quote` are already hygienic by default — they can't clash with caller variables. `generate_unique_arguments` is needed when you're generating *multiple* variables dynamically (e.g., function parameters for a generated clause) where you need distinct names that also interoperate correctly.
## Decision Tree
- If a macro must behave differently in guards vs normal code → check `__CALLER__.context` (Pattern 1)
- If you need a reusable, compile-time-validated guard expression → use `defguard` (Pattern 2)
- If a macro argument might have side effects or be expensive → use `quote bind_quoted:` to evaluate once (Pattern 3)
- If a macro must reference a variable in the caller's scope → use `var!` sparingly (Pattern 4)
- If the macro receives input that could be an alias or module attribute → expand with `Macro.expand` before branching (Pattern 5)
- If your macro defines module-level constructs and should never appear in guards → assert context at the top (Pattern 6)
- If you need open-ended type dispatch that external code can extend → use `defprotocol` (Pattern 7)
- If a protocol should handle any value rather than raising on unknown types → use `@fallback_to_any true` (Pattern 8)
- If a module needs injected behaviours, attributes, or compile hooks → use the `use/2` + `__using__/1` pattern (Pattern 9)
- If you have compile-time-known literals that benefit from validation → define a sigil macro (Pattern 10)
- If you need a zero-cost syntactic transformation (argument rewriting) → implement as a macro like `|>` (Pattern 11)
<!-- PATTERN_COMPLETE -->
+22
View File
@@ -2,6 +2,18 @@
How modules are structured, named, and organized in Elixir core and Phoenix.
## Contents
1. [One Module per Concept, Nested for Sub-Concepts](#1-one-module-per-concept-nested-for-sub-concepts)
2. [Public API at the Top, Private Functions at the Bottom](#2-public-api-at-the-top-private-functions-at-the-bottom)
3. [`@moduledoc false` for Internal Modules](#3-moduledoc-false-for-internal-modules)
4. [Struct Definition Conventions](#4-struct-definition-conventions)
5. [Selective Imports in `__using__`](#5-selective-imports-in-__using__)
6. [Alias at Module Scope for Readability](#6-alias-at-module-scope-for-readability)
7. [Boolean-Suffixed Fields in Structs](#7-boolean-suffixed-fields-in-structs)
---
## 1. One Module per Concept, Nested for Sub-Concepts
**Source:** `lib/elixir/lib/` directory structure
@@ -590,4 +602,14 @@ defstruct [:user, :admin?, :count]
**Why:** The `?` suffix should only mark genuine booleans. Using it on non-boolean fields creates confusion about the field's type and breaks the convention's usefulness as a type signal.
## Decision Tree
- If your module has grown beyond 300 lines with distinct sub-responsibilities → [One Module per Concept, Nested for Sub-Concepts](#1-one-module-per-concept-nested-for-sub-concepts)
- If you need to decide function ordering within a module → [Public API at the Top, Private Functions at the Bottom](#2-public-api-at-the-top-private-functions-at-the-bottom)
- If a module exists purely for internal code organization and should not appear in docs → [`@moduledoc false` for Internal Modules](#3-moduledoc-false-for-internal-modules)
- If you need to define a struct and decide which fields are mandatory → [Struct Definition Conventions](#4-struct-definition-conventions)
- If your `use` macro needs to set up the caller's namespace with specific functions → [Selective Imports in `__using__`](#5-selective-imports-in-__using__)
- If multiple modules from the same parent namespace are used repeatedly → [Alias at Module Scope for Readability](#6-alias-at-module-scope-for-readability)
- If a struct field stores a boolean value and you want self-documenting naming → [Boolean-Suffixed Fields in Structs](#7-boolean-suffixed-fields-in-structs)
<!-- PATTERN_COMPLETE -->
+42
View File
@@ -2,6 +2,27 @@
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`.
## Contents
1. [Pattern 1: Static vs Dynamic Supervision — Choose the Right Tool](#pattern-1-static-vs-dynamic-supervision--choose-the-right-tool)
2. [Pattern 2: PartitionSupervisor for Scalability](#pattern-2-partitionsupervisor-for-scalability)
3. [Pattern 3: Supervision Strategies — Choosing the Right Restart Behavior](#pattern-3-supervision-strategies--choosing-the-right-restart-behavior)
4. [Pattern 4: Restart Intensity (`max_restarts` / `max_seconds`)](#pattern-4-restart-intensity-max_restarts--max_seconds)
5. [Pattern 5: Restart Values — `:permanent` vs `:transient` vs `:temporary`](#pattern-5-restart-values--permanent-vs-transient-vs-temporary)
6. [Pattern 6: Automatic Shutdown for Pipeline Supervisors](#pattern-6-automatic-shutdown-for-pipeline-supervisors)
7. [Pattern 7: Task.async/await for Concurrent Value Computation](#pattern-7-taskasyncawait-for-concurrent-value-computation)
8. [Pattern 8: Task.Supervisor.async_nolink for Fault-Tolerant Task Execution](#pattern-8-tasksupervisorasync_nolink-for-fault-tolerant-task-execution)
9. [Pattern 9: Task Supervisor as DynamicSupervisor Specialization](#pattern-9-task-supervisor-as-dynamicsupervisor-specialization)
10. [Pattern 10: Registry for Dynamic Process Naming and PubSub](#pattern-10-registry-for-dynamic-process-naming-and-pubsub)
11. [Pattern 11: Shutdown Semantics — Graceful Termination](#pattern-11-shutdown-semantics--graceful-termination)
12. [Pattern 12: DynamicSupervisor Internal State — Struct with Restart Tracking](#pattern-12-dynamicsupervisor-internal-state--struct-with-restart-tracking)
13. [Pattern 13: Restart Logic with Exponential Backoff via `:try_again`](#pattern-13-restart-logic-with-exponential-backoff-via-try_again)
14. [Pattern 14: `$ancestors` and `$callers` — Process Lineage Tracking](#pattern-14-ancestors-and-callers--process-lineage-tracking)
15. [Pattern 15: GenServer.reply/2 for Deferred Responses](#pattern-15-genserverreply2-for-deferred-responses)
16. [Pattern 16: Process.alias for Safe Request/Response](#pattern-16-processalias-for-safe-requestresponse)
17. [Pattern 17: Registry Partitioning Strategies](#pattern-17-registry-partitioning-strategies)
18. [Pattern 18: `init/1` Return Values — The Full Spectrum](#pattern-18-init1-return-values--the-full-spectrum)
---
## Pattern 1: Static vs Dynamic Supervision — Choose the Right Tool
@@ -1911,4 +1932,25 @@ end
**Why:** `:ignore` means "this child intentionally should not run right now." `{:stop, reason}` means "this child tried to start and failed." Conflating the two hides real failures from your supervision tree.
## Decision Tree
- If you have children known at compile time with ordering dependencies → [Pattern 1: Static vs Dynamic Supervision](#pattern-1-static-vs-dynamic-supervision--choose-the-right-tool)
- If a single DynamicSupervisor or Task.Supervisor is a bottleneck under high spawn load → [Pattern 2: PartitionSupervisor for Scalability](#pattern-2-partitionsupervisor-for-scalability)
- If you need to decide how a supervisor reacts when children share state or have dependencies → [Pattern 3: Supervision Strategies](#pattern-3-supervision-strategies--choosing-the-right-restart-behavior)
- If you want to tune how many restarts are tolerated before escalation → [Pattern 4: Restart Intensity](#pattern-4-restart-intensity-max_restarts--max_seconds)
- If different processes have different lifecycle expectations (one-shot vs permanent) → [Pattern 5: Restart Values](#pattern-5-restart-values--permanent-vs-transient-vs-temporary)
- If a supervisor should self-terminate when its children finish their work → [Pattern 6: Automatic Shutdown](#pattern-6-automatic-shutdown-for-pipeline-supervisors)
- If you need to compute values concurrently and the caller should crash on failure → [Pattern 7: Task.async/await](#pattern-7-taskasyncawait-for-concurrent-value-computation)
- If a GenServer needs to spawn work that might fail without taking down the server → [Pattern 8: Task.Supervisor.async_nolink](#pattern-8-tasksupervisorasync_nolink-for-fault-tolerant-task-execution)
- If you need supervised tasks with caller tracking, async_nolink, and streaming → [Pattern 9: Task Supervisor](#pattern-9-task-supervisor-as-dynamicsupervisor-specialization)
- If you need to look up processes by a dynamic key without atom leaks → [Pattern 10: Registry](#pattern-10-registry-for-dynamic-process-naming-and-pubsub)
- If processes hold external resources that need cleanup on shutdown → [Pattern 11: Shutdown Semantics](#pattern-11-shutdown-semantics--graceful-termination)
- If you are building a custom supervisor-like process and need efficient child tracking → [Pattern 12: DynamicSupervisor Internal State](#pattern-12-dynamicsupervisor-internal-state--struct-with-restart-tracking)
- If a child fails to start due to transient conditions and you want non-blocking retry → [Pattern 13: Restart Logic with Backoff](#pattern-13-restart-logic-with-exponential-backoff-via-try_again)
- If you need to trace which process initiated spawned work for debugging → [Pattern 14: Process Lineage Tracking](#pattern-14-ancestors-and-callers--process-lineage-tracking)
- If a GenServer needs to do async work before replying to a caller → [Pattern 15: GenServer.reply/2](#pattern-15-genserverreply2-for-deferred-responses)
- If you build a custom request/response protocol with timeouts and need to prevent late replies → [Pattern 16: Process.alias](#pattern-16-processalias-for-safe-requestresponse)
- If your Registry dispatch is slow because of wrong partitioning strategy → [Pattern 17: Registry Partitioning](#pattern-17-registry-partitioning-strategies)
- If you need to communicate "don't start this child" or split init into fast/slow phases → [Pattern 18: init/1 Return Values](#pattern-18-init1-return-values--the-full-spectrum)
<!-- PATTERN_COMPLETE -->
+2 -2
View File
@@ -132,8 +132,8 @@ defmodule MyApp.Schema do
defmacro __using__(_) do
quote do
use Ecto.Schema
@primary_key {:id, :integer, autogenerate: true} # Same as the default
@foreign_key_type :integer
@primary_key {:id, :id, autogenerate: true} # Same as the default (:id resolves to integer)
@foreign_key_type :id
end
end
end
+46
View File
@@ -2,6 +2,29 @@
Patterns extracted from the Elixir standard library source code — how the core team writes and organizes tests.
## Contents
1. [Module-Level Async Declaration](#1-module-level-async-declaration)
2. [Parameterized Tests](#2-parameterized-tests)
3. [Setup with `start_supervised/2`](#3-setup-with-start_supervised2)
4. [Named Setup Functions (Composable Pipelines)](#4-named-setup-functions-composable-pipelines)
5. [`on_exit` for Reversing Global Side Effects](#5-on_exit-for-reversing-global-side-effects)
6. [Pattern Match Assertions](#6-pattern-match-assertions)
7. [`assert_receive` / `refute_receive` for Process Communication](#7-assert_receive--refute_receive-for-process-communication)
8. [Testing GenServers via Public API (No Internal State Inspection)](#8-testing-genservers-via-public-api-no-internal-state-inspection)
9. [`catch_exit` for Testing Process Failures](#9-catch_exit-for-testing-process-failures)
10. [`@tag capture_log: true` for Suppressing Expected Log Output](#10-tag-capture_log-true-for-suppressing-expected-log-output)
11. [`capture_log` / `capture_io` for Content Assertions](#11-capture_log--capture_io-for-content-assertions)
12. [`describe` Blocks for Logical Grouping](#12-describe-blocks-for-logical-grouping)
13. [`ExUnit.CaseTemplate` for Shared Test Infrastructure](#13-exunitcasetemplate-for-shared-test-infrastructure)
14. [`doctest` Integration](#14-doctest-integration)
15. [`Process.sleep(:infinity)` as a Process Parking Pattern](#15-processsleepinfinity-as-a-process-parking-pattern)
16. [Helper Functions for Test-Specific Behavior](#16-helper-functions-for-test-specific-behavior)
17. [`@tag :tmp_dir` for Filesystem Tests](#17-tag-tmp_dir-for-filesystem-tests)
18. [`assert_raise` with Message Matching](#18-assert_raise-with-message-matching)
19. [`@moduletag` / `@describetag` for Cross-Cutting Configuration](#19-moduletag--describetag-for-cross-cutting-configuration)
20. [Context Pattern Matching in Test Signatures](#20-context-pattern-matching-in-test-signatures)
---
## 1. Module-Level Async Declaration
@@ -1725,4 +1748,27 @@ end
**Why:** Context destructuring signals "this test depends on external setup." If the test is self-contained, the pattern match is misleading — readers will look for setup that doesn't exist or isn't needed.
## Decision Tree
- If you are creating a new test module and need to decide on concurrency → [Module-Level Async Declaration](#1-module-level-async-declaration)
- If the same logic must work across multiple configurations or backends → [Parameterized Tests](#2-parameterized-tests)
- If your test needs a running process with guaranteed cleanup → [Setup with `start_supervised/2`](#3-setup-with-start_supervised2)
- If setup has multiple independent steps that different describe blocks reuse → [Named Setup Functions](#4-named-setup-functions-composable-pipelines)
- If your test modifies global state that must be restored regardless of outcome → [`on_exit` for Reversing Global Side Effects](#5-on_exit-for-reversing-global-side-effects)
- If you care about the shape/structure of a result but not every field → [Pattern Match Assertions](#6-pattern-match-assertions)
- If you need to test asynchronous message delivery between processes → [`assert_receive` / `refute_receive`](#7-assert_receive--refute_receive-for-process-communication)
- If you are testing a GenServer and want tests that survive refactoring → [Testing GenServers via Public API](#8-testing-genservers-via-public-api-no-internal-state-inspection)
- If you need to assert on OTP exit signals (timeouts, noproc, shutdown) → [`catch_exit`](#9-catch_exit-for-testing-process-failures)
- If tests intentionally trigger error paths that produce noisy log output → [`@tag capture_log: true`](#10-tag-capture_log-true-for-suppressing-expected-log-output)
- If you need to verify specific log or IO content was emitted → [`capture_log` / `capture_io`](#11-capture_log--capture_io-for-content-assertions)
- If a module tests multiple public functions and needs logical organization → [`describe` Blocks](#12-describe-blocks-for-logical-grouping)
- If multiple test modules share the same setup/teardown infrastructure → [`ExUnit.CaseTemplate`](#13-exunitcasetemplate-for-shared-test-infrastructure)
- If your module has `iex>` examples that should be verified automatically → [`doctest` Integration](#14-doctest-integration)
- If you need an inert process that exists only to be observed or killed → [`Process.sleep(:infinity)`](#15-processsleepinfinity-as-a-process-parking-pattern)
- If the same 3-5 line test pattern repeats across multiple tests → [Helper Functions](#16-helper-functions-for-test-specific-behavior)
- If tests create or modify files and need filesystem isolation → [`@tag :tmp_dir`](#17-tag-tmp_dir-for-filesystem-tests)
- If you need to verify both the exception type and the user-facing message → [`assert_raise` with Message Matching](#18-assert_raise-with-message-matching)
- If tests only run on certain platforms or you want to filter subsets → [`@moduletag` / `@describetag`](#19-moduletag--describetag-for-cross-cutting-configuration)
- If you want to make test dependencies on setup context explicit → [Context Pattern Matching](#20-context-pattern-matching-in-test-signatures)
<!-- PATTERN_COMPLETE -->
+9 -9
View File
@@ -135,21 +135,21 @@ end
When a custom type is used inside an `embeds_one` or `embeds_many` field, Ecto calls `embed_as/1` to decide whether to pass the value through `dump/1` or treat it as its own serialized form. The callback receives the embed format (`:json` by default) and returns either `:self` or `:dump`.
- `:self` — the value is used as-is when exporting embedded data (dump is still called for DB storage)
- `:dump``dump/1` is always called, even in embedded contexts
- `:self` — the in-memory value is used as-is without calling `dump/1`; appropriate when the runtime representation is already JSON-compatible (scalars, plain maps)
- `:dump``dump/1` is called to serialize the value before encoding; needed when the runtime representation (e.g., a struct) is not directly JSON-serializable
`use Ecto.Type` provides a default implementation that returns `:self`. Override it when your type must always run `dump/1` to produce its storable form, even when nested inside an embedded schema.
`use Ecto.Type` provides a default implementation that returns `:self`. Override it to return `:dump` when your type holds an Elixir struct or other value that cannot be directly encoded to JSON.
```elixir
defmodule EctoURI do
use Ecto.Type
# Override to ensure dump/1 is called in embedded contexts
def embed_as(_format), do: :self
# Override: %URI{} is not JSON-serializable, so run dump/1 in embedded contexts
def embed_as(_format), do: :dump
end
```
**Why:** When Ecto builds embedded documents for export (e.g., storing a JSON blob), it needs to know whether to trust the in-memory value or to re-serialize it. If your type holds state in an Elixir struct that cannot be stored directly (like `%URI{}`), returning `:self` without a proper `dump/1` would persist the raw struct map rather than your intended shape. Overriding `embed_as/1` makes the contract explicit.
**Why:** When Ecto builds embedded documents for export (e.g., storing a JSON blob), it needs to know whether to trust the in-memory value or to re-serialize it. If your type holds state in an Elixir struct that cannot be stored directly (like `%URI{}`), the default `:self` passes the raw struct to the JSON encoder — which either raises or produces garbage like `%{__struct__: "Elixir.URI", host: ..., ...}`. Returning `:dump` ensures `dump/1` converts it to a clean map first.
**Anti-pattern:** Assuming the default `:self` is correct for a type whose in-memory and storable representations differ, then debugging mysterious embedded schema corruption:
```elixir
@@ -199,8 +199,8 @@ defmodule EctoURI do
def dump(%URI{} = uri), do: {:ok, Map.from_struct(uri)}
def dump(_), do: :error
# Explicit: always pass through dump/1 when exporting embedded values
def embed_as(_format), do: :self
# Explicit: ensure dump/1 runs in embedded contexts to produce a clean map
def embed_as(_format), do: :dump
end
```
@@ -209,7 +209,7 @@ end
**Don't use this when:**
- Your type is never used in embedded schemas (the callback has no effect)
- The in-memory and storable representations are the same (plain maps, scalars)
- You intentionally want to skip `dump/1` in embedded contexts (then return `:dump` and document why)
- You intentionally want to skip `dump/1` in embedded contexts (the default `:self` already does this)
**Over-application example:**
```elixir
+26
View File
@@ -2,6 +2,19 @@
Patterns extracted from the Elixir standard library source code.
## Contents
1. [Public Type with @typedoc](#1-public-type-with-typedoc)
2. [Private Types with @typep](#2-private-types-with-typep)
3. [@opaque Types (Protocol t())](#3-opaque-types-protocol-t)
4. [Union Types in @spec Return Values](#4-union-types-in-spec-return-values)
5. [`when` Constraints in Specs](#5-when-constraints-in-specs)
6. [Map Types with required/optional Keys](#6-map-types-with-requiredoptional-keys)
7. [Keyword List Types for Options](#7-keyword-list-types-for-options)
8. [Parameterized Types (t/1)](#8-parameterized-types-t1)
9. [Named Parameters in Specs (:: annotation)](#9-named-parameters-in-specs--annotation)
10. [@typedoc since: Annotation](#10-typedoc-since-annotation)
---
## 1. Public Type with @typedoc
@@ -798,4 +811,17 @@ end
**Why:** `since:` annotations are for library consumers checking compatibility across versions. Application code doesn't have "consumers" checking which version introduced a type — it's all deployed together.
## Decision Tree
- If you are defining a public `@type` that appears in any `@spec` or callback → [Public Type with @typedoc](#1-public-type-with-typedoc)
- If a type is used only internally for recursion or DRYing up repeated expressions → [Private Types with @typep](#2-private-types-with-typep)
- If you want to hide internal representation and force consumers to use accessor functions → [@opaque Types](#3-opaque-types-protocol-t)
- If a function can return multiple distinct shapes (tagged tuples, atoms) → [Union Types in @spec Return Values](#4-union-types-in-spec-return-values)
- If the return type depends on the input type (generic/polymorphic function) → [`when` Constraints in Specs](#5-when-constraints-in-specs)
- If you accept a map with a mix of mandatory and optional keys → [Map Types with required/optional Keys](#6-map-types-with-requiredoptional-keys)
- If a function accepts a keyword list of options and you want to document valid keys → [Keyword List Types for Options](#7-keyword-list-types-for-options)
- If you define a container type and want specs to express what element type is inside → [Parameterized Types (t/1)](#8-parameterized-types-t1)
- If a parameter's type alone does not convey its purpose → [Named Parameters in Specs](#9-named-parameters-in-specs--annotation)
- If you are adding a new public type to an existing library post-1.0 → [@typedoc since: Annotation](#10-typedoc-since-annotation)
<!-- PATTERN_COMPLETE -->