docs: add patterns extracted from elixir-lang/elixir source
Using codebase-analysis skill (patterns mode) on the language source. Real examples from the repo, not invented. Each pattern has: - Rule, Example, Why, When NOT to use, Source file. Topics: module org, protocol design, error handling, testing, documentation, naming, process design, smells.
This commit is contained in:
@@ -0,0 +1,326 @@
|
|||||||
|
# Elixir Patterns (from Source)
|
||||||
|
|
||||||
|
Prescriptive patterns extracted from elixir-lang/elixir source.
|
||||||
|
"If writing new Elixir, follow these rules."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module Organization
|
||||||
|
|
||||||
|
### One Module Per Concept
|
||||||
|
|
||||||
|
**Rule:** Each module owns exactly one concept. If you can't name it in
|
||||||
|
2-3 words, it's too broad.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Source: lib/elixir/lib/string.ex — String is strings. Period.
|
||||||
|
defmodule String do
|
||||||
|
@moduledoc """
|
||||||
|
Strings in Elixir are UTF-8 encoded binaries.
|
||||||
|
"""
|
||||||
|
@type t :: binary
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** The Elixir source has zero "util" or "helper" modules. Every
|
||||||
|
module has a noun name that IS the thing.
|
||||||
|
|
||||||
|
**When NOT to use:** Kernel is the exception — it's the implicit
|
||||||
|
surface area. You don't get to make your own Kernel.
|
||||||
|
|
||||||
|
**Source:** Every file in `lib/elixir/lib/` follows this.
|
||||||
|
|
||||||
|
### @moduledoc false for Internal Modules
|
||||||
|
|
||||||
|
**Rule:** Internal modules that users shouldn't call get `@moduledoc false`.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Source: lib/elixir/lib/code/formatter.ex
|
||||||
|
defmodule Code.Formatter do
|
||||||
|
@moduledoc false
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Hides from docs. Signals "this is implementation, not API."
|
||||||
|
The Elixir team uses this for 30+ internal modules.
|
||||||
|
|
||||||
|
**When NOT to use:** If ANYONE outside your team might call it. Public
|
||||||
|
API must have docs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Protocol Design
|
||||||
|
|
||||||
|
### Protocols for External Extension
|
||||||
|
|
||||||
|
**Rule:** Define a protocol when users need to extend behavior for
|
||||||
|
their own types.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Source: lib/elixir/lib/collectable.ex
|
||||||
|
defprotocol Collectable do
|
||||||
|
@doc """
|
||||||
|
Returns an initial accumulation value and a "collector" function.
|
||||||
|
"""
|
||||||
|
@spec into(t) :: {initial_acc :: term, collector(term)}
|
||||||
|
def into(collectable)
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Protocols dispatch on the first argument's type. They're the
|
||||||
|
extension point for "I made a new data structure and want it to work
|
||||||
|
with Enum."
|
||||||
|
|
||||||
|
**When NOT to use:** When you control all implementations. Use
|
||||||
|
behaviours instead. Protocols are for open extension; behaviours are
|
||||||
|
for closed contracts.
|
||||||
|
|
||||||
|
### Only 6 Stdlib Protocols
|
||||||
|
|
||||||
|
**Rule:** Be conservative defining protocols. The Elixir stdlib has
|
||||||
|
only 6 in 15 years.
|
||||||
|
|
||||||
|
- `Enumerable` — iterate over things
|
||||||
|
- `Collectable` — put things into containers
|
||||||
|
- `Inspect` — debug representation
|
||||||
|
- `String.Chars` — convert to string
|
||||||
|
- `List.Chars` — convert to charlist
|
||||||
|
- `JSON.Encoder` — JSON serialization (added 2024)
|
||||||
|
|
||||||
|
**Why:** Each protocol is a permanent API commitment. Once defined,
|
||||||
|
every type in the ecosystem may implement it.
|
||||||
|
|
||||||
|
**When NOT to use:** Don't define a protocol for something only your
|
||||||
|
app needs. A behaviour or function argument is cheaper.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Tagged Tuples for Expected Failures
|
||||||
|
|
||||||
|
**Rule:** Return `{:ok, value}` or `{:error, reason}` for operations
|
||||||
|
that can fail in expected ways.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Source: lib/elixir/lib/file.ex
|
||||||
|
@spec read(Path.t()) :: {:ok, binary} | {:error, posix}
|
||||||
|
def read(path) do
|
||||||
|
case :file.read_file(path) do
|
||||||
|
{:ok, binary} -> {:ok, binary}
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Pattern matching makes handling explicit. The caller MUST
|
||||||
|
decide what to do with errors — they can't accidentally ignore them.
|
||||||
|
|
||||||
|
**When NOT to use:** For programmer errors (bugs). Those should raise.
|
||||||
|
`File.read!` raises; `File.read` returns tuples.
|
||||||
|
|
||||||
|
### Bang Functions for "Should Never Fail"
|
||||||
|
|
||||||
|
**Rule:** Provide `function!` variant that raises on error. Use when
|
||||||
|
failure means a bug in the caller.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Convention: function returns {:ok, _} | {:error, _}
|
||||||
|
# function! raises on error
|
||||||
|
File.read("path") # => {:ok, "..."} | {:error, :enoent}
|
||||||
|
File.read!("path") # => "..." | raises File.Error
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** The `!` signals to the reader: "I expect this to succeed. If
|
||||||
|
it doesn't, crash." This is intentional — crashing is the correct
|
||||||
|
response to unexpected errors in OTP.
|
||||||
|
|
||||||
|
**When NOT to use:** When failure is expected and the caller should
|
||||||
|
handle it (use tagged tuples).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### CaseTemplate for Shared Setup
|
||||||
|
|
||||||
|
**Rule:** Use `ExUnit.CaseTemplate` when multiple test files share
|
||||||
|
setup logic.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Source: lib/ex_unit/lib/ex_unit/case_template.ex
|
||||||
|
defmodule MyApp.DataCase do
|
||||||
|
use ExUnit.CaseTemplate
|
||||||
|
|
||||||
|
setup tags do
|
||||||
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
|
||||||
|
unless tags[:async] do
|
||||||
|
Ecto.Adapters.SQL.Sandbox.mode(MyApp.Repo, {:shared, self()})
|
||||||
|
end
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Inheritance via `use` is how Phoenix's ConnCase/DataCase work.
|
||||||
|
This pattern comes directly from ExUnit itself.
|
||||||
|
|
||||||
|
**When NOT to use:** For helpers that don't need lifecycle callbacks.
|
||||||
|
Simple `import` is cleaner for utility functions.
|
||||||
|
|
||||||
|
### async: true by Default
|
||||||
|
|
||||||
|
**Rule:** Mark tests `async: true` unless they touch shared state.
|
||||||
|
|
||||||
|
**Why:** Async tests run in parallel. The Elixir stdlib tests show that
|
||||||
|
most tests CAN be async — only database/file/process tests need
|
||||||
|
serialization.
|
||||||
|
|
||||||
|
**When NOT to use:** Tests that modify global state, shared files, or
|
||||||
|
named processes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
### Every Public Function Gets @doc + @spec
|
||||||
|
|
||||||
|
**Rule:** All public functions have both `@doc` and `@spec`.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Source: lib/elixir/lib/enum.ex
|
||||||
|
@doc """
|
||||||
|
Returns `true` if all elements in `enumerable` are truthy.
|
||||||
|
"""
|
||||||
|
@spec all?(t) :: boolean
|
||||||
|
def all?(enumerable) when is_list(enumerable) do
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Specs enable Dialyzer checking. Docs generate ExDoc pages. The
|
||||||
|
Elixir stdlib has zero undocumented public functions.
|
||||||
|
|
||||||
|
**When NOT to use:** `@moduledoc false` modules skip docs (they're
|
||||||
|
internal).
|
||||||
|
|
||||||
|
### @type t for Structs
|
||||||
|
|
||||||
|
**Rule:** Every struct defines `@type t` with field types.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Source: lib/elixir/lib/kernel.ex (defstruct docs)
|
||||||
|
defmodule User do
|
||||||
|
defstruct name: "John", age: 25
|
||||||
|
@type t :: %__MODULE__{name: String.t(), age: non_neg_integer}
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Enables Dialyzer to catch type mismatches at struct boundaries.
|
||||||
|
Without `@type t`, struct fields are effectively untyped.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
### Modules Are Nouns
|
||||||
|
|
||||||
|
**Rule:** Module names are nouns. Never verbs, never adjectives.
|
||||||
|
|
||||||
|
`String`, `Enum`, `Map`, `File`, `Logger`, `GenServer`
|
||||||
|
|
||||||
|
**Why:** A module IS a thing. Functions are what you DO with that thing.
|
||||||
|
`String.split/2` reads as "take a String, split it."
|
||||||
|
|
||||||
|
**When NOT to use:** Mix tasks (they're commands: `Mix.Tasks.Deps.Get`).
|
||||||
|
|
||||||
|
### Functions Are Verbs
|
||||||
|
|
||||||
|
**Rule:** Function names start with a verb (or are a question with `?`).
|
||||||
|
|
||||||
|
`Enum.map/2`, `String.split/2`, `File.read/1`, `Enum.empty?/1`
|
||||||
|
|
||||||
|
**Why:** `module.verb(subject)` reads as a sentence.
|
||||||
|
|
||||||
|
### Underscore Prefix for Unused
|
||||||
|
|
||||||
|
**Rule:** Prefix unused variables with `_`.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
def handle_info(_message, state), do: {:noreply, state}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Compiler warning suppression AND documentation that the value
|
||||||
|
is intentionally ignored.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Process Design
|
||||||
|
|
||||||
|
### GenServer for Stateful Services
|
||||||
|
|
||||||
|
**Rule:** Use GenServer when you need mutable state that outlives a
|
||||||
|
request.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Source: lib/iex/lib/iex/broker.ex
|
||||||
|
defmodule IEx.Broker do
|
||||||
|
use GenServer
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** GenServer gives you: message serialization, supervision tree
|
||||||
|
integration, hot code upgrade, `:sys` debugging.
|
||||||
|
|
||||||
|
**When NOT to use:** Stateless transformations (just use functions).
|
||||||
|
One-off concurrent work (use Task). Accumulating state within a request
|
||||||
|
(use recursion or reduce).
|
||||||
|
|
||||||
|
### Agent for Simple State
|
||||||
|
|
||||||
|
**Rule:** Use Agent when you only need get/update on a value — no
|
||||||
|
complex message handling.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Source: lib/mix/lib/mix/tasks_server.ex
|
||||||
|
defmodule Mix.TasksServer do
|
||||||
|
use Agent
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why:** Agent is GenServer with the common case optimized. Less
|
||||||
|
boilerplate for "I just need a mutable box."
|
||||||
|
|
||||||
|
**When NOT to use:** When you need `handle_info`, timeouts, or multiple
|
||||||
|
operations that must be atomic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Smells
|
||||||
|
|
||||||
|
### GenEvent (Deprecated Pattern)
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# Source: lib/elixir/lib/gen_event.ex
|
||||||
|
@moduledoc deprecated: "Use one of the alternatives described below"
|
||||||
|
```
|
||||||
|
|
||||||
|
The Elixir team deprecated their own GenEvent. Alternatives: Registry +
|
||||||
|
GenServer, or Phoenix.PubSub. Lesson: event buses that try to do
|
||||||
|
everything are worse than composed primitives.
|
||||||
|
|
||||||
|
### Version-Gated TODOs (Deferred Cleanup)
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# TODO: Remove me on v2.0
|
||||||
|
# TODO: Deprecate me on Elixir v1.23
|
||||||
|
```
|
||||||
|
|
||||||
|
127 of these exist. They're not smells in the "bad code" sense —
|
||||||
|
they're discipline. But if YOUR code has TODOs without version targets,
|
||||||
|
that IS a smell.
|
||||||
|
|
||||||
|
### @moduledoc false Proliferation
|
||||||
|
|
||||||
|
30+ internal modules in the stdlib. If your app has this many, you may
|
||||||
|
be over-splitting. Internal modules should be rare in application code
|
||||||
|
— they're appropriate for libraries and frameworks.
|
||||||
|
|
||||||
|
<!-- PATTERN_COMPLETE -->
|
||||||
Reference in New Issue
Block a user