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,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.
|
||||
...
|
||||
"""
|
||||
```
|
||||
Reference in New Issue
Block a user