Files
elixir-patterns/patterns/documentation.md
T
Aaron Weiker 4ea9a884aa 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.
2026-04-29 22:50:12 -07:00

329 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Documentation Patterns
Patterns extracted from the Elixir standard library source code.
---
## 1. @moduledoc with Structured Sections
**Source:** `lib/elixir/lib/gen_server.ex` lines 6100+, `lib/logger/lib/logger.ex` lines 6200+
**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 315335 (abs/1), `lib/logger/lib/logger.ex` lines 536540
**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 1420
**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 8895, `lib/elixir/lib/supervisor.ex` lines 3438
**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 163180
**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 584646 (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.
...
"""
```