docs: Elixir patterns vs official guidelines with code examples + hypotheses
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
# Elixir Patterns vs Official Guidelines — Divergence Analysis
|
||||
|
||||
## Summary
|
||||
- **Areas of agreement:** 12
|
||||
- **Divergences found:** 5
|
||||
- **Our patterns go beyond official:** 14
|
||||
|
||||
The extracted patterns are overwhelmingly *compatible* with the official guides but operate at a fundamentally different level of specificity. Official guides describe *what* to do; our patterns describe *how the core team actually does it* — with concrete source line references, anti-patterns, and trigger conditions.
|
||||
|
||||
---
|
||||
|
||||
## Agreements (brief)
|
||||
|
||||
| Area | Our Pattern | Official Source | Notes |
|
||||
|------|-------------|-----------------|-------|
|
||||
| Naming conventions | snake_case functions, CamelCase modules | Hexdocs naming-conventions, Style Guide | Perfect alignment |
|
||||
| `@moduledoc` / `@doc` usage | Always document public modules/functions | Hexdocs writing-documentation | Both emphasize first-class documentation |
|
||||
| Typespec placement | `@spec` above `def` | Style Guide §Typespecs | Agreed |
|
||||
| `@impl true` annotations | Mark all behaviour callbacks | Style Guide §Modules | Both recommend explicit `@impl` |
|
||||
| Module attribute ordering | `@moduledoc`, `@behaviour`, `@derive`, etc. | Style Guide §Modules (ordering) | Our modules.md follows same canonical order |
|
||||
| `with` clause formatting | Align successive clauses, multiline for `else` | Style Guide §Indentation (with-clauses) | Our error-handling.md matches formatting rules |
|
||||
| Testing: `async: true` | Explicitly declare async intent per module | CONTRIBUTING.md, Style Guide §Testing | Both emphasize explicit concurrency declaration |
|
||||
| Testing: `assert` pattern matching | Use `assert {:ok, val} = expr` for structural checks | Style Guide §Testing | Aligned |
|
||||
| Parentheses in pipes | Always use parens for arity-1 in pipes | Style Guide §Parentheses | Aligned |
|
||||
| Exceptions: meaningful names | Custom exceptions with clear naming | Style Guide §Exceptions | Both agree |
|
||||
| One module per file | Standard rule | Style Guide §Modules | Aligned |
|
||||
| Behaviour callbacks with full types | Document all return variants | Style Guide §Typespecs | Agreed on thoroughness |
|
||||
|
||||
---
|
||||
|
||||
## Divergences
|
||||
|
||||
### 1. `with` — When to Use `else` Blocks
|
||||
|
||||
**Our pattern (error-handling.md):**
|
||||
> The `else` block is an anti-pattern in most cases. Prefer letting non-matching clauses fall through or use pattern-matched function heads. Only use `else` when you must normalize heterogeneous error tuples into a single return shape.
|
||||
|
||||
Provides specific trigger conditions: "Use `else` ONLY when upstream functions return differently-shaped errors that must be normalized to a single interface."
|
||||
|
||||
**Official guide (Style Guide §Indentation):**
|
||||
> Simply shows formatting rules for `with`/`else` — when to use multiline syntax vs inline `do:`. No opinion on whether `else` blocks are advisable.
|
||||
|
||||
**Hexdocs (Kernel.SpecialForms):**
|
||||
> Documents `else` as a normal feature. States that without `else`, non-matching values are returned as-is.
|
||||
|
||||
**Example — what the official guide formats but doesn't judge:**
|
||||
```elixir
|
||||
with {:ok, user} <- fetch_user(id),
|
||||
{:ok, avatar} <- fetch_avatar(user) do
|
||||
{:ok, %{user | avatar: avatar}}
|
||||
else
|
||||
{:error, :not_found} -> {:error, :user_not_found}
|
||||
{:error, :timeout} -> {:error, :avatar_unavailable}
|
||||
end
|
||||
```
|
||||
|
||||
**Example — what our patterns recommend instead:**
|
||||
```elixir
|
||||
# Let non-matching clauses pass through (implicit return):
|
||||
with {:ok, user} <- fetch_user(id),
|
||||
{:ok, avatar} <- fetch_avatar(user) do
|
||||
{:ok, %{user | avatar: avatar}}
|
||||
end
|
||||
# Returns {:error, :not_found} or {:error, :timeout} unchanged.
|
||||
# Only use `else` when you MUST normalize heterogeneous shapes.
|
||||
```
|
||||
|
||||
**Hypothesis:** The core team writes `with` without `else` ~90% of the time in Elixir source. They designed `with` so that non-matching values pass through cleanly. The `else` clause exists as an escape hatch, not the expected path. The style guide doesn't say this because it's *formatting* documentation, not *design* documentation. Our patterns fill the gap between "how to indent it" and "should you write it at all."
|
||||
|
||||
---
|
||||
|
||||
### 2. Error Return Shape Normalization
|
||||
|
||||
**Our pattern (error-handling.md):**
|
||||
> Errors should follow a normalized shape: `{:error, atom}` or `{:error, {atom, detail}}`. The first element identifies the error class; optional second element provides context. Functions should coerce upstream errors into this shape at module boundaries.
|
||||
|
||||
Explicit anti-pattern: `{:error, "string message"}` as a return value (strings are for humans, atoms are for machines).
|
||||
|
||||
**Official guide:**
|
||||
> The Style Guide and Hexdocs say nothing about error return shapes beyond the basic `{:ok, value} | {:error, reason}` convention visible in standard library typespecs.
|
||||
|
||||
**Hexdocs naming-conventions:**
|
||||
> Mentions trailing `!` for functions that raise instead of returning error tuples, but doesn't prescribe error tuple internals.
|
||||
|
||||
**Example — what code commonly looks like (no official guidance exists):**
|
||||
```elixir
|
||||
# Anti-pattern: string errors (no machine-readable classification)
|
||||
def create_user(params) do
|
||||
case validate(params) do
|
||||
:ok -> {:error, "validation failed"} # String — can't pattern match
|
||||
{:error, reason} -> {:error, reason} # Might be atom, might be string
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Example — what our patterns prescribe (extracted from OTP source):**
|
||||
```elixir
|
||||
# Normalized shape: {:error, atom} or {:error, {atom, detail}}
|
||||
def create_user(params) do
|
||||
case validate(params) do
|
||||
{:error, changeset} -> {:error, {:validation, changeset}}
|
||||
:ok -> do_create(params)
|
||||
end
|
||||
end
|
||||
# Consumer can match on the atom: {:error, {:validation, _}}
|
||||
```
|
||||
|
||||
**Hypothesis:** This is tribal knowledge that the core team practices but never codified. In GenServer, errors are always atoms (`:timeout`, `:noproc`). In File, they're always POSIX atoms (`:enoent`, `:eacces`). In Ecto, they're always `{:error, changeset}`. Each subsystem converged independently on "atoms for machines, strings for humans." No official doc connects these dots because each library documents itself — nobody wrote the cross-cutting principle. Our patterns make the implicit explicit.
|
||||
|
||||
---
|
||||
|
||||
### 3. Testing: Parameterized Tests (`:parameterize` option)
|
||||
|
||||
**Our pattern (testing.md):**
|
||||
> Detailed guidance on ExUnit's `:parameterize` option (since v1.18), including when to use it, when NOT to use it, and the critical warning: "If you find yourself adding conditionals in your tests to deal with different parameters, parameterized tests are the wrong solution."
|
||||
|
||||
**Official guide (Style Guide §Testing):**
|
||||
> No mention of parameterized tests at all. The style guide's testing section covers: `assert` over `assert_receive`, pattern matching in assertions, and using `setup` blocks. No `:parameterize`.
|
||||
|
||||
**CONTRIBUTING.md:**
|
||||
> Doesn't mention parameterization as a testing strategy.
|
||||
|
||||
**Example — parameterized test (our patterns document, guides don't mention):**
|
||||
```elixir
|
||||
defmodule MyApp.StoreTest do
|
||||
use ExUnit.Case,
|
||||
async: true,
|
||||
parameterize: [
|
||||
%{store: MyApp.Store.ETS},
|
||||
%{store: MyApp.Store.Redis},
|
||||
%{store: MyApp.Store.Postgres}
|
||||
]
|
||||
|
||||
test "get/put round-trips", %{store: store} do
|
||||
{:ok, pid} = store.start_link([])
|
||||
:ok = store.put(pid, "key", "value")
|
||||
assert {:ok, "value"} = store.get(pid, "key")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
**Example — what our patterns warn against:**
|
||||
```elixir
|
||||
# ANTI-PATTERN: conditionals inside parameterized tests
|
||||
test "handles limits", %{store: store} do
|
||||
if store == MyApp.Store.Redis do
|
||||
# Redis-specific logic here...
|
||||
else
|
||||
# Everything else...
|
||||
end
|
||||
# If you need conditionals, these aren't the same test.
|
||||
end
|
||||
```
|
||||
|
||||
**Hypothesis:** Style guides are maintained by community volunteers who update them periodically. The Elixir source moves faster — José added `:parameterize` in v1.18 and immediately used it in Registry/ETS tests. The hexdocs auto-generate from source so they're current, but human-written guides lag. This is a pure temporal gap — the guide will eventually catch up.
|
||||
|
||||
---
|
||||
|
||||
### 4. Documentation: Trigger/Context Sections in @doc
|
||||
|
||||
**Our pattern (documentation.md):**
|
||||
> Beyond standard documentation, includes patterns for "When to Use" / "When NOT to Use" sections with explicit trigger conditions and anti-patterns. This structured approach to documenting preconditions isn't just `@doc` — it's a pedagogical documentation style.
|
||||
|
||||
**Official guide (Hexdocs writing-documentation):**
|
||||
> Prescribes: brief first-line description, code examples, doctests. Recommends documenting "what" the function does and showing examples. Does NOT prescribe structured "when to use" / "when not to use" sections.
|
||||
|
||||
**Style Guide §Documentation:**
|
||||
> "Prefer @moduledoc over @doc for module-level docs." Various formatting rules. Nothing about documenting preconditions or trigger conditions.
|
||||
|
||||
**Why they differ:**
|
||||
Our patterns document a *pedagogical style* found in Elixir's source — the core team uses extensive "## Examples" and contextual explanations in their docs. But we've formalized it further into a structured template (triggers, anti-patterns, before/after). This goes beyond what any official guide prescribes because it's a *pattern documentation* format, not a *code documentation* format. The official guides address developers documenting their code; our patterns address someone extracting reusable knowledge from code.
|
||||
|
||||
---
|
||||
|
||||
### 5. Typespecs: Union Types with Named Parameters
|
||||
|
||||
**Our pattern (typespecs.md):**
|
||||
> Emphasizes using named parameters in callback specs (`init_arg :: term`, `request :: term`) and `when` clauses for type variable scoping across return type unions. Treats specs as primary documentation for behaviour implementors.
|
||||
|
||||
**Official guide (Style Guide §Typespecs):**
|
||||
> "Define @typedoc and @type in the first part of the module." Basic formatting. Place `@spec` above `def`. Use `@type` for complex types. No guidance on naming parameters or `when` clause style.
|
||||
|
||||
**Hexdocs:**
|
||||
> Documents the syntax but doesn't prescribe stylistic choices about named parameters.
|
||||
|
||||
**Why they differ:**
|
||||
The style guide treats typespecs as a formatting concern. Our patterns treat them as *interface design* — derived from GenServer's callback definitions where named parameters like `init_arg` and `request` make the typespec self-documenting. The style guide answers "where do I put the spec?" while our patterns answer "how do I write a spec that teaches the reader?"
|
||||
|
||||
---
|
||||
|
||||
## Beyond Official (patterns we extracted that guides don't cover)
|
||||
|
||||
These represent knowledge extracted from source code that no official guide addresses:
|
||||
|
||||
### 1. List-Specialized Clause Before Protocol Dispatch (data-transforms.md)
|
||||
The pattern of `when is_list(enumerable)` guard clauses before generic protocol dispatch. Pure performance optimization visible in Enum/Stream source. No official guide mentions this — it's an internal implementation strategy.
|
||||
|
||||
### 2. Client/Server API Separation in GenServers (genserver.md)
|
||||
Formal separation of client functions from server callbacks within the same module. The official GenServer docs show this in examples, but no style guide *prescribes* it as a pattern or calls out calling `GenServer.call` directly from external modules as an anti-pattern.
|
||||
|
||||
### 3. Context-Aware Macros via `__CALLER__.context` (macros.md)
|
||||
Generating different code for guards vs match vs normal context. The style guide says "don't write macros unless you have to" but provides zero guidance on *how* to write them properly when you do.
|
||||
|
||||
### 4. Static vs Dynamic Supervision Selection Criteria (process-design.md)
|
||||
When to choose `Supervisor` vs `DynamicSupervisor`. Official docs describe both but don't provide selection heuristics or anti-patterns (e.g., using DynamicSupervisor for fixed infrastructure).
|
||||
|
||||
### 5. `start_supervised!` vs Manual Process Start in Tests (testing.md)
|
||||
Detailed guidance on when to use `start_supervised` vs raw `start_link`. ExUnit docs mention the function but don't provide the decision framework.
|
||||
|
||||
### 6. Named Setup Functions as Composable Pipelines (testing.md)
|
||||
Using `setup [:step1, :step2, :step3]` for composable test preconditions. Documented in ExUnit but not promoted in any style guide.
|
||||
|
||||
### 7. `on_exit` Scoping Rules (testing.md)
|
||||
When to use `on_exit` vs `start_supervised` for cleanup. Nuanced guidance about `on_exit` running in a separate process.
|
||||
|
||||
### 8. `assert_receive` / `refute_receive` Patterns (testing.md)
|
||||
Detailed async process testing without `Process.sleep`. The anti-pattern of sleeping before assertions.
|
||||
|
||||
### 9. Behaviour Callbacks with Full Union Returns (behaviours.md)
|
||||
Pattern of documenting every valid return shape in `@callback` type unions. Shown in GenServer source but never prescribed in guides.
|
||||
|
||||
### 10. Pattern Match Assertions vs Equality Assertions (testing.md)
|
||||
When to use `assert {:ok, val} = expr` vs `assert expr == expected`. The style guide uses both without differentiating.
|
||||
|
||||
### 11. Process Registration Anti-Patterns
|
||||
Our patterns identify that registering global names in async tests causes race conditions. No style guide covers this interaction.
|
||||
|
||||
### 12. Macro Hygiene Strategies (macros.md)
|
||||
Specific patterns for variable hygiene, `unquote` placement, and avoiding name collisions. Style guide says "be careful with macros" — our patterns show how.
|
||||
|
||||
### 13. Registry Usage Patterns (process-design.md)
|
||||
When Registry replaces manual process tracking. Not in any style guide.
|
||||
|
||||
### 14. Supervision Tree Architecture Decisions (process-design.md)
|
||||
How to structure supervision trees, when to use `:one_for_one` vs `:one_for_all` vs `:rest_for_one`. Docs describe the options; our patterns provide selection criteria.
|
||||
|
||||
---
|
||||
|
||||
## Assessment
|
||||
|
||||
### Which is more trustworthy when they conflict?
|
||||
|
||||
**Our patterns are more trustworthy for "how to write Elixir well."**
|
||||
|
||||
Here's why:
|
||||
|
||||
1. **Source authority:** Our patterns are extracted from the Elixir standard library source code itself — the code written and maintained by José Valim and the core team. When the core team's actual practice diverges from a community style guide, the source code represents ground truth.
|
||||
|
||||
2. **Temporal freshness:** The community style guide hasn't incorporated patterns from v1.18+ (parameterized tests, for example). Our patterns reflect the current state of the source.
|
||||
|
||||
3. **Specificity vs generality:** The official guides are intentionally general — they serve beginners through experts across all contexts. Our patterns are specific, opinionated, and contextual. For someone building production systems, specificity wins.
|
||||
|
||||
4. **The "why" factor:** Official guides mostly state rules. Our patterns explain *why* by showing the anti-pattern and its consequences. This makes them more useful for decision-making.
|
||||
|
||||
**However, the official guides are more trustworthy for:**
|
||||
- Formatting rules (use `mix format`, the formatter is canonical)
|
||||
- Naming conventions (stable, universally agreed upon)
|
||||
- Basic file/module organization (one module per file, snake_case files)
|
||||
|
||||
**Bottom line:** The guides are complementary, not competing. Official guides set the baseline; our extracted patterns provide the advanced, practice-derived knowledge that only comes from reading thousands of lines of production Elixir written by the language creators themselves.
|
||||
Reference in New Issue
Block a user