From edef02ed0f017647b15e15ce24f042680e289c36 Mon Sep 17 00:00:00 2001 From: Aaron Weiker Date: Sat, 2 May 2026 10:03:56 -0700 Subject: [PATCH] docs: add rule for when @impl functions earn their own @doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- patterns/documentation.md | 66 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/patterns/documentation.md b/patterns/documentation.md index c7555d1..c1ba307 100644 --- a/patterns/documentation.md +++ b/patterns/documentation.md @@ -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)