1002 lines
28 KiB
Markdown
1002 lines
28 KiB
Markdown
# 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
|
||
```
|
||
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- Your module is the primary entry point for a concept (GenServer, Logger, Supervisor)
|
||
- The module has more than 3-4 public functions with non-obvious relationships
|
||
- Users need to understand configuration, lifecycle, or architecture before calling individual functions
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
defmodule MyApp.Cache do
|
||
@moduledoc "A cache module."
|
||
|
||
def get(key), do: ...
|
||
def put(key, value, opts), do: ...
|
||
def invalidate(key), do: ...
|
||
end
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
defmodule MyApp.Cache do
|
||
@moduledoc """
|
||
A write-through cache backed by ETS with configurable TTL.
|
||
|
||
## Usage
|
||
|
||
MyApp.Cache.put("user:1", user, ttl: :timer.minutes(5))
|
||
MyApp.Cache.get("user:1") #=> {:ok, %User{}}
|
||
|
||
## Configuration
|
||
|
||
Configure in your application config:
|
||
|
||
config :my_app, MyApp.Cache,
|
||
max_size: 10_000,
|
||
default_ttl: :timer.hours(1)
|
||
|
||
## Eviction
|
||
|
||
When `max_size` is reached, entries are evicted LRU-first...
|
||
"""
|
||
end
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- The module has 1-2 functions and the purpose is obvious from the module name
|
||
- It's an internal/private module (`@moduledoc false` is better)
|
||
- The module is a thin wrapper where function-level docs suffice
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
defmodule MyApp.StringUtils do
|
||
@moduledoc """
|
||
# String Utilities
|
||
|
||
## Overview
|
||
|
||
This module provides string utility functions for the application.
|
||
|
||
## Architecture
|
||
|
||
Functions in this module operate on binaries and return binaries...
|
||
|
||
## Configuration
|
||
|
||
No configuration required.
|
||
|
||
## Examples
|
||
|
||
See individual function documentation.
|
||
"""
|
||
|
||
def capitalize_words(str), do: ...
|
||
end
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
defmodule MyApp.StringUtils do
|
||
@moduledoc "String transformation helpers for display formatting."
|
||
|
||
@doc "Capitalizes the first letter of each word."
|
||
def capitalize_words(str), do: ...
|
||
end
|
||
```
|
||
|
||
**Why:** A utility module with a few pure functions doesn't need architecture docs, configuration sections, or a table of contents. Match the documentation depth to the module's complexity.
|
||
|
||
---
|
||
|
||
## 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
|
||
```
|
||
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- The function has non-obvious behavior, edge cases, or preconditions
|
||
- Users need to understand what happens with boundary inputs (nil, empty list, negative numbers)
|
||
- The function is part of a public API that others will call without reading the source
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
def chunk(list, size), do: ...
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
@doc """
|
||
Splits `list` into chunks of the given `size`.
|
||
|
||
The last chunk may have fewer than `size` elements if the list
|
||
length is not evenly divisible.
|
||
|
||
## Examples
|
||
|
||
iex> chunk([1, 2, 3, 4, 5], 2)
|
||
[[1, 2], [3, 4], [5]]
|
||
|
||
iex> chunk([], 3)
|
||
[]
|
||
|
||
"""
|
||
def chunk(list, size), do: ...
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- The function is private (use `# comments` for private function notes)
|
||
- The function name + typespec are completely self-explanatory (e.g., `@spec pid() :: pid()`)
|
||
- You're implementing a behaviour callback and want it hidden (`@impl true` sets `@doc false` automatically)
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
@doc """
|
||
Returns the name.
|
||
|
||
## Examples
|
||
|
||
iex> get_name(%User{name: "Alice"})
|
||
"Alice"
|
||
|
||
"""
|
||
@spec get_name(User.t()) :: String.t()
|
||
def get_name(%User{name: name}), do: name
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
@spec get_name(User.t()) :: String.t()
|
||
def get_name(%User{name: name}), do: name
|
||
```
|
||
|
||
**Why:** When the function name, spec, and implementation are all trivially obvious, a `@doc` that restates them adds maintenance burden without helping anyone. Save detailed docs for functions where the behavior isn't immediately obvious.
|
||
|
||
---
|
||
|
||
## 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
|
||
```
|
||
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- You're adding a new public function to an existing library (anything post-1.0)
|
||
- Your library publishes versioned documentation (via ExDoc/HexDocs)
|
||
- Users may be on older versions and need to know when a function became available
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
@doc "Compacts the list by removing nil values."
|
||
def compact(list), do: Enum.reject(list, &is_nil/1)
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
@doc since: "1.4.0"
|
||
@doc "Compacts the list by removing nil values."
|
||
def compact(list), do: Enum.reject(list, &is_nil/1)
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- You're writing application code (not a published library)
|
||
- The function has existed since the library's first release
|
||
- The library doesn't publish versioned docs or follow semver
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# In a Phoenix controller — no one checks "which version of my app added this"
|
||
defmodule MyAppWeb.UserController do
|
||
@doc since: "0.1.0"
|
||
def index(conn, _params), do: ...
|
||
|
||
@doc since: "0.2.0"
|
||
def show(conn, %{"id" => id}), do: ...
|
||
end
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
defmodule MyAppWeb.UserController do
|
||
def index(conn, _params), do: ...
|
||
def show(conn, %{"id" => id}), do: ...
|
||
end
|
||
```
|
||
|
||
**Why:** `since:` annotations help library consumers check compatibility. Application code is deployed atomically — there's no concept of "which version of the app introduced this endpoint." Use git blame instead.
|
||
|
||
---
|
||
|
||
## 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: ...
|
||
```
|
||
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- The function/macro is valid in guard clauses
|
||
- You want tools (ExDoc, IEx) to programmatically identify guard-eligible functions
|
||
- The function is a Kernel macro that users might try to use in guards
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
@doc """
|
||
Returns true if `term` is a non-empty binary.
|
||
|
||
Allowed in guard tests.
|
||
"""
|
||
defmacro is_non_empty_binary(term) do
|
||
...
|
||
end
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
@doc """
|
||
Returns true if `term` is a non-empty binary.
|
||
"""
|
||
@doc guard: true
|
||
defmacro is_non_empty_binary(term) do
|
||
...
|
||
end
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- The function is NOT valid in guard clauses (adding it would be misleading)
|
||
- You're writing application code where no one filters functions by guard eligibility
|
||
- The function calls other functions or has side effects (it can't be a guard)
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
@doc guard: true
|
||
def valid_email?(email) do
|
||
String.contains?(email, "@") # NOT guard-safe — calls String.contains?
|
||
end
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
@doc "Checks if the string looks like an email address."
|
||
def valid_email?(email) do
|
||
String.contains?(email, "@")
|
||
end
|
||
```
|
||
|
||
**Why:** `@doc guard: true` is a semantic contract that the function works in guard position. Adding it to non-guard functions breaks tooling expectations and confuses users who try to use it in `when` clauses.
|
||
|
||
---
|
||
|
||
## 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
|
||
```
|
||
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- A function must be public for technical reasons (protocol dispatch, macro expansion) but isn't part of the user API
|
||
- You're implementing callbacks with `@impl true` and don't want generated docs
|
||
- The function is an internal hook that users should never call directly
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
# Users see this in docs and try to call it
|
||
def __struct__(fields), do: ...
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
@doc false
|
||
def __struct__(fields), do: ...
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- The function IS part of the public API (even if you think it's "obvious")
|
||
- You want to discourage use but still document it (use `@doc deprecated:` instead)
|
||
- You're hiding functions because you're too lazy to document them
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
defmodule MyApp.Repo do
|
||
@doc false
|
||
def get(schema, id), do: ...
|
||
|
||
@doc false
|
||
def all(query), do: ...
|
||
end
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
defmodule MyApp.Repo do
|
||
@doc "Fetches a single record by primary key. Returns nil if not found."
|
||
def get(schema, id), do: ...
|
||
|
||
@doc "Fetches all records matching the query."
|
||
def all(query), do: ...
|
||
end
|
||
```
|
||
|
||
**Why:** `@doc false` means "this function is not part of the public API." If users are expected to call it, it needs documentation. Hiding public API behind `@doc false` is a maintenance hazard — users will call undocumented functions and break on upgrades.
|
||
|
||
---
|
||
|
||
## 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
|
||
```
|
||
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- The module exists purely for internal code organization
|
||
- It's a protocol implementation module that users never reference directly
|
||
- It's a migration, task, or generated module that shouldn't appear in docs
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
defmodule MyApp.Repo.Supervisor do
|
||
# Internal supervisor — users never interact with this directly
|
||
use Supervisor
|
||
def start_link(opts), do: ...
|
||
def init(opts), do: ...
|
||
end
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
defmodule MyApp.Repo.Supervisor do
|
||
@moduledoc false
|
||
use Supervisor
|
||
def start_link(opts), do: ...
|
||
def init(opts), do: ...
|
||
end
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- The module has any function that users are expected to call
|
||
- You're hiding it because documentation feels like too much work
|
||
- The module is a behaviour that others will implement
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
defmodule MyApp.Telemetry do
|
||
@moduledoc false # "It's just telemetry setup, nobody cares"
|
||
|
||
def setup do
|
||
# Attaches telemetry handlers that affect app behavior
|
||
:telemetry.attach_many(...)
|
||
end
|
||
end
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
defmodule MyApp.Telemetry do
|
||
@moduledoc """
|
||
Telemetry event handlers for request metrics and error tracking.
|
||
|
||
Called from `Application.start/2`. Attaches handlers for:
|
||
- `[:my_app, :request, :stop]` — request duration histograms
|
||
- `[:my_app, :error]` — error counters
|
||
"""
|
||
def setup, do: ...
|
||
end
|
||
```
|
||
|
||
**Why:** If a module affects application behavior and other developers need to understand it during debugging or extension, it needs documentation. `@moduledoc false` should be reserved for truly mechanical/generated code.
|
||
|
||
---
|
||
|
||
## 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
|
||
...
|
||
"""
|
||
```
|
||
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- You're documenting a multi-component architecture (client-server, pipeline, state machine)
|
||
- The relationship between components is spatial or sequential and hard to express in prose
|
||
- ExDoc is your documentation renderer (it supports Mermaid natively)
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
@moduledoc """
|
||
The pipeline processes events through three stages:
|
||
first the parser, then the transformer, then the loader.
|
||
The parser sends to transformer, transformer sends to loader.
|
||
Errors at any stage go to the error handler.
|
||
"""
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
@moduledoc """
|
||
Event processing pipeline with three stages.
|
||
|
||
```mermaid
|
||
graph LR
|
||
Parser -->|events| Transformer
|
||
Transformer -->|records| Loader
|
||
Parser & Transformer & Loader -.->|errors| ErrorHandler
|
||
```
|
||
|
||
## Stages
|
||
...
|
||
"""
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- The module's architecture is simple enough that a one-sentence description suffices
|
||
- Your docs aren't rendered by a tool that supports Mermaid (raw markdown viewers won't render it)
|
||
- The diagram would be trivial (one box, one arrow) — a sentence is clearer
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
@moduledoc """
|
||
Wraps an integer counter.
|
||
|
||
```mermaid
|
||
graph LR
|
||
User -->|increment| Counter
|
||
Counter -->|value| User
|
||
```
|
||
"""
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
@moduledoc """
|
||
A simple integer counter. Call `increment/1` to add, `value/1` to read.
|
||
"""
|
||
```
|
||
|
||
**Why:** Diagrams add cognitive overhead. If the reader can understand the architecture faster from a sentence than from parsing a diagram, the diagram is noise. Reserve diagrams for genuinely complex interactions.
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
...
|
||
"""
|
||
```
|
||
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- There's critical information that users MUST notice (breaking changes, security implications, required setup)
|
||
- You need to document what `use MyModule` injects (the OTP convention)
|
||
- A common mistake or foot-gun deserves visual prominence
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
@moduledoc """
|
||
...
|
||
Note: calling process/1 with untrusted input can execute arbitrary code.
|
||
Make sure to validate inputs first.
|
||
...
|
||
"""
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
@moduledoc """
|
||
...
|
||
|
||
> #### Security Warning {: .warning}
|
||
>
|
||
> `process/1` evaluates its input. Never pass untrusted user input
|
||
> directly — always validate and sanitize first.
|
||
|
||
...
|
||
"""
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- The information isn't actually critical (don't cry wolf)
|
||
- You're using admonitions for every paragraph (they lose impact)
|
||
- Your rendering target doesn't support admonition syntax (it'll render as ugly blockquotes)
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
@moduledoc """
|
||
> #### Overview {: .info}
|
||
>
|
||
> This module provides helper functions.
|
||
|
||
> #### Usage {: .info}
|
||
>
|
||
> Call the functions with the appropriate arguments.
|
||
|
||
> #### Note {: .neutral}
|
||
>
|
||
> All functions return their results.
|
||
"""
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
@moduledoc """
|
||
Helper functions for string formatting.
|
||
|
||
## Usage
|
||
|
||
MyHelpers.format_name("alice") #=> "Alice"
|
||
"""
|
||
```
|
||
|
||
**Why:** Admonitions are visual interrupts — they break reading flow to demand attention. When everything is an admonition, nothing is. Reserve them for information where missing it would cause real problems.
|
||
|
||
---
|
||
|
||
## 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
|
||
```
|
||
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- You want to signal "prefer the alternative" without emitting compiler warnings
|
||
- The function still works fine but a better API exists
|
||
- You're planning a future hard deprecation and want to give users a migration window
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
@doc "Fetches a user by ID. Deprecated: use Accounts.get_user/1."
|
||
def fetch_user(id), do: ...
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
@doc deprecated: "Use Accounts.get_user/1 instead"
|
||
@doc "Fetches a user by ID."
|
||
def fetch_user(id), do: ...
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- You actually want compiler warnings (use `@deprecated "reason"` instead)
|
||
- The function is being removed in the next release (hard deprecation is appropriate)
|
||
- The function is still the recommended approach (don't pre-deprecate)
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
# Deprecating before the replacement exists
|
||
@doc deprecated: "Will be replaced by new_process/2 in v3.0"
|
||
def process(input) do
|
||
# new_process/2 doesn't exist yet!
|
||
...
|
||
end
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
# Wait until the replacement is available, then deprecate
|
||
def process(input), do: ...
|
||
|
||
# In v3.0, after new_process/2 ships:
|
||
@doc deprecated: "Use new_process/2 instead"
|
||
def process(input), do: new_process(input, [])
|
||
```
|
||
|
||
**Why:** Never deprecate a function before the recommended alternative exists and is documented. Users seeing "use X instead" need X to actually exist — otherwise you're creating confusion without offering a path forward.
|
||
|
||
---
|
||
|
||
## 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
|
||
```
|
||
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- You're defining a behaviour that other modules will implement
|
||
- Implementers need to know: when the callback fires, what all return values mean, and which callbacks are optional
|
||
- The callback has multiple valid return shapes with different effects
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
@callback handle_event(event :: term(), state :: term()) :: {:ok, term()} | {:error, term()}
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
@doc """
|
||
Invoked when an event is received from the event bus.
|
||
|
||
`event` is the decoded event payload. `state` is the handler's
|
||
current state, initialized by `c:init/1`.
|
||
|
||
Returning `{:ok, new_state}` acknowledges the event and continues.
|
||
Returning `{:skip, new_state}` skips without acknowledgment (redelivery possible).
|
||
Returning `{:error, reason}` triggers the error handler configured in `start_link/1`.
|
||
|
||
This callback is required.
|
||
"""
|
||
@callback handle_event(event :: term(), state :: term()) ::
|
||
{:ok, new_state :: term()}
|
||
| {:skip, new_state :: term()}
|
||
| {:error, reason :: term()}
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- The callback has a single obvious return type and the name says it all
|
||
- You're documenting internal callbacks that aren't part of a public behaviour
|
||
- The behaviour is private to your application and only you implement it
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
@doc """
|
||
Called to format the value.
|
||
|
||
## Parameters
|
||
|
||
- `value` - The value to format (term)
|
||
|
||
## Returns
|
||
|
||
- `String.t()` - The formatted string
|
||
|
||
## Examples
|
||
|
||
iex> format(:hello)
|
||
"hello"
|
||
"""
|
||
@callback format(value :: term()) :: String.t()
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
@doc "Formats `value` into a human-readable string for display."
|
||
@callback format(value :: term()) :: String.t()
|
||
```
|
||
|
||
**Why:** A callback with one parameter and one return type doesn't need a full reference manual. Match documentation depth to complexity — a one-liner with good naming is better than padded sections that add no information.
|
||
|
||
---
|
||
|
||
## 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.
|
||
...
|
||
"""
|
||
```
|
||
|
||
### When to Use
|
||
|
||
**Triggers:**
|
||
- Your moduledoc or function doc references other functions, callbacks, or types in the same or other modules
|
||
- Users would benefit from clicking through to related documentation
|
||
- You have callbacks and functions with the same name that need disambiguation
|
||
|
||
**Example — before:**
|
||
```elixir
|
||
@doc """
|
||
Starts the server. Calls init/1 internally.
|
||
See the server type for accepted values.
|
||
"""
|
||
```
|
||
|
||
**Example — after:**
|
||
```elixir
|
||
@doc """
|
||
Starts the server. Calls `c:init/1` internally.
|
||
See `t:server/0` for accepted name values.
|
||
"""
|
||
```
|
||
|
||
### When NOT to Use
|
||
|
||
**Don't use this when:**
|
||
- You're writing documentation that won't be rendered by ExDoc (plain README, comments)
|
||
- The reference is to a well-known Erlang/Elixir function that readers will recognize without a link
|
||
- Over-linking makes the prose unreadable (every other word is a link)
|
||
|
||
**Over-application example:**
|
||
```elixir
|
||
@doc """
|
||
Returns `t:boolean/0` indicating if the `t:pid/0` from `Kernel.self/0`
|
||
matches the `t:pid/0` stored in `t:state/0` field `:owner`.
|
||
"""
|
||
```
|
||
|
||
**Better alternative:**
|
||
```elixir
|
||
@doc """
|
||
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.
|