From e6fbfced9601c47a9af0428ea311f9ddb7a8b5d6 Mon Sep 17 00:00:00 2001 From: Rodin Date: Thu, 30 Apr 2026 13:26:23 -0700 Subject: [PATCH] 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. --- patterns/from-source.md | 326 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 patterns/from-source.md diff --git a/patterns/from-source.md b/patterns/from-source.md new file mode 100644 index 0000000..793a0e2 --- /dev/null +++ b/patterns/from-source.md @@ -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. + +