docs: add rule for when @impl functions earn their own @doc

Pattern 10 (Callback Documentation Convention) now owns the full rule
for callback documentation — both the behaviour side (@callback docs)
and the implementation side (when to override @doc false on @impl
functions). Patterns 2 and 5 cross-reference Pattern 10 instead of
making their own partial statements.

The test: "Would this statement be true of any implementation?" If yes,
it belongs on the @callback. If no, the implementation earns its own
@doc.
This commit is contained in:
2026-05-02 10:03:56 -07:00
parent 8e77a5e321
commit edef02ed0f
+65 -1
View File
@@ -215,7 +215,7 @@ def chunk(list, size), do: ...
**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)
- You're implementing a behaviour callback `@impl true` sets `@doc false` automatically. Override only when the implementation has semantics the behaviour can't speak to (see [Pattern 10](#when-to-override-doc-false-on-impl-functions))
**Over-application example:**
```elixir
@@ -456,6 +456,7 @@ def __struct__(fields), do: ...
- 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
- The `@impl` function has implementation-specific semantics that callers need to know (see [Pattern 10 — implementation-side docs](#when-to-override-doc-false-on-impl-functions))
**Over-application example:**
```elixir
@@ -481,6 +482,8 @@ 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.
> **Note:** `@impl true` auto-sets `@doc false`, but some implementations earn their own `@doc`. See [Pattern 10 — implementation-side docs](#when-to-override-doc-false-on-impl-functions) for the rule.
---
## 6. @moduledoc false — Hiding Modules
@@ -941,6 +944,67 @@ Called to format the value.
**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.
### When to override `@doc false` on `@impl` functions
`@impl true` sets `@doc false` by default — the assumption is that the behaviour's `@callback` doc covers it. This is correct when the implementation is unremarkable. Override it with an explicit `@doc` when the implementation has semantics that would not be true of every conforming implementation.
**The test:** "Would this statement be true of *any* implementation of this callback?" If yes → the doc belongs on the `@callback` in the behaviour, not here. If no → the implementation earns its own `@doc`.
**Override when:**
- The implementation makes a guarantee the behaviour doesn't promise (e.g., "always returns immediately", "never buffers", "fires at most once")
- There's a race condition, ordering constraint, or subtle failure mode specific to this implementation
- The implementation's behavior under edge cases differs from what the behaviour's generic contract implies
**Don't override when:**
- The doc would just restate the `@callback` doc in different words
- The doc describes what the function does rather than what's surprising about this implementation
- The function name + behaviour doc are sufficient for an implementor or caller to understand it
**Example — unnecessary override (just restates the contract):**
```elixir
defmodule JsonSerializer do
@behaviour Serializer
@doc """
Encodes the given term to a binary.
"""
@impl true
def encode(term), do: Jason.encode!(term)
end
```
**Example — justified override (implementation-specific guarantee):**
```elixir
defmodule Immediate do
@behaviour Aggregation
@doc """
Always returns `{:ready, [signal]}` — immediate mode fires on first signal
without buffering or waiting for a timer.
"""
@impl true
def check(%Signal{} = signal), do: {:ready, [signal]}
end
```
**Example — justified override (race condition warning):**
```elixir
defmodule AlpacaAdapter do
@behaviour BrokerAdapter
@doc """
Cancels an open order by broker ID. Returns `:ok` on success.
The order may still receive a final fill between the cancel request
and confirmation — callers must handle the `partially_filled` → `cancelled` race.
"""
@impl true
def cancel(credential, broker_order_id), do: ...
end
```
**Why:** The behaviour's `@callback` doc is the generic contract — "what any implementation must do." An implementation's `@doc` is the specific contract — "what callers of *this* implementation can rely on." When those differ meaningfully, silence (`@doc false`) hides information that would prevent bugs.
---
## 11. Documentation with Link References (c: and t: prefixes)