# 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.