commit 21a5ea0d581c090e9ceec3f7721781b878cf790b Author: Rodin Date: Wed Apr 29 22:57:22 2026 -0700 feat: comprehensive Elixir patterns guide with source citations 25 patterns extracted from the Elixir standard library source code, each with exact file:line citations for authoritative reference. Covers: multi-clause dispatch, type specialization, tagged tuples, protocols, use/__using__, behaviours, binary parsing, supervision, GenServer patterns, Agent wrappers, options validation, pipe operator, Enumerable/Collectable protocols, Task async/await, documentation conventions, parameterized testing, error messages, defoverridable, infinity sentinels, defguard, Erlang delegation, flexible APIs, and naming conventions. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3cfa6cf --- /dev/null +++ b/README.md @@ -0,0 +1,1036 @@ +# Idiomatic Elixir: Patterns from the Source + +A reference guide to writing Elixir the way the core team writes it, derived from studying the Elixir standard library source code (v1.18+). Every pattern cites the specific file and line number where you can see it in practice. + +> All paths are relative to the Elixir source root: `lib/elixir/lib/` unless noted otherwise. + +--- + +## 1. Multi-Clause Functions as Control Flow + +The most pervasive pattern in idiomatic Elixir: instead of `cond`, `case`, or `if/else` chains inside a function body, express different cases as separate function clauses. The BEAM's pattern matching engine handles dispatch. + +### Exhaustive dispatch via clauses + +```elixir +# lib/elixir/lib/enum.ex:1766-1775 +def map_every(enumerable, 1, fun), do: map(enumerable, fun) +def map_every(enumerable, 0, _fun), do: to_list(enumerable) +def map_every([], nth, _fun) when is_integer(nth) and nth > 1, do: [] + +def map_every(enumerable, nth, fun) when is_integer(nth) and nth > 1 do + {res, _} = reduce(enumerable, {[], :first}, R.map_every(nth, fun)) + :lists.reverse(res) +end +``` + +Each clause handles one logical case. No nested conditionals. The guard clauses (`when`) serve as assertions that would otherwise be runtime checks. + +### Recursive traversal with base cases + +```elixir +# lib/elixir/lib/enum.ex:4541-4549 +defp filter_list([head | tail], fun) do + if fun.(head) do + [head | filter_list(tail, fun)] + else + filter_list(tail, fun) + end +end + +defp filter_list([], _fun) do + [] +end +``` + +The empty list clause is the termination condition. The `[head | tail]` clause processes one element and recurses. This replaces loops entirely. + +### Mathematical recursion by squaring + +```elixir +# lib/elixir/lib/kernel.ex:4617-4626 +defp integer_pow(_, _, 0), + do: 1 + +defp integer_pow(b, a, 1), + do: b * a + +defp integer_pow(b, a, e) when :erlang.band(e, 1) == 0, + do: integer_pow(b * b, a, :erlang.bsr(e, 1)) + +defp integer_pow(b, a, e), + do: integer_pow(b * b, a * b, :erlang.bsr(e, 1)) +``` + +Four clauses implement exponentiation by squaring. No mutable state, no loop counters. Each clause is a case in the mathematical definition. + +--- + +## 2. Type-Specialized Clauses (Optimistic Dispatch) + +When a function works on multiple types, provide specialized clauses for the common/fast case, with a general fallback. + +### List-optimized `map/2` + +```elixir +# lib/elixir/lib/enum.ex:1724-1732 +def map(enumerable, fun) when is_list(enumerable) do + :lists.map(fun, enumerable) +end + +def map(first..last//step, fun) do + map_range(first, last, step, fun) +end + +def map(enumerable, fun) do + reduce(enumerable, [], R.map(fun)) |> :lists.reverse() +end +``` + +Lists get the fast `:lists.map` path. Ranges get their own optimized path. Everything else goes through the generic reduce. The caller never picks — the VM dispatches to the right clause. + +### `Access.fetch/2` — struct, map, keyword, nil + +```elixir +# lib/elixir/lib/access.ex:249-275 +def fetch(%module{} = container, key) do + module.fetch(container, key) +end + +def fetch(map, key) when is_map(map) do + case map do + %{^key => value} -> {:ok, value} + _ -> :error + end +end + +def fetch(list, key) when is_list(list) and is_atom(key) do + case :lists.keyfind(key, 1, list) do + {_, value} -> {:ok, value} + false -> :error + end +end + +def fetch(nil, _key) do + :error +end +``` + +Each data type gets its own clause. Structs delegate to their module's implementation. This is how polymorphism works without inheritance. + +--- + +## 3. The `{:ok, value} | {:error, reason}` Convention + +Elixir uses tagged tuples to signal success/failure without exceptions. The `!`-suffix variant raises on failure. + +### Pattern: Provide both variants + +```elixir +# lib/elixir/lib/keyword.ex:272-296 +def validate(keyword, values) when is_list(keyword) and is_list(values) do + validate(keyword, values, [], keyword, []) +end + +# ...returns {:ok, keyword()} | {:error, [atom]} +``` + +```elixir +# lib/elixir/lib/keyword.ex:355 +def validate!(keyword, values) do + case validate(keyword, values) do + {:ok, keyword} -> keyword + {:error, keys} -> raise ArgumentError, "unknown keys #{inspect(keys)}..." + end +end +``` + +The non-bang version returns tagged tuples for programmatic handling. The bang version raises and is for situations where failure is unexpected. This duality appears throughout: `File.read/1` vs `File.read!/1`, `Map.fetch/2` vs `Map.fetch!/2`. + +### `with` for happy-path chaining + +```elixir +# lib/elixir/lib/uri.ex:486-492 +defp unpercent(<>, acc, spaces) do + with <> <- tail, + dec1 when is_integer(dec1) <- hex_to_dec(hex1), + dec2 when is_integer(dec2) <- hex_to_dec(hex2) do + unpercent(tail, <>, spaces) + else + _ -> unpercent(tail, <>, spaces) + end +end +``` + +`with` chains operations where each step can fail. If any `<-` doesn't match, execution falls to `else`. This replaces nested case statements. + +### The `with` best practice (from the docs) + +```elixir +# lib/elixir/lib/kernel/special_forms.ex:1679-1710 (from `with` documentation) +# WRONG: reconstructing error types in else (line 1679) +with ".ex" <- Path.extname(path), + true <- File.exists?(path) do + ... +else + binary when is_binary(binary) -> {:error, :invalid_extension} + false -> {:error, :missing_file} +end + +# RIGHT: normalize in helper functions so each <- returns clear errors (line 1697) +with :ok <- validate_extension(path), + :ok <- validate_exists(path) do + ... +end +``` + +The Elixir docs (`lib/elixir/lib/kernel/special_forms.ex:1692-1710`) explicitly recommend that each `<-` clause return a normalized format. Extract validation into named helper functions rather than decoding raw return values in `else`. + +--- + +## 4. Protocols for Polymorphism + +Protocols are Elixir's answer to polymorphism. They define a contract that any type can implement. + +### Defining a protocol + +```elixir +# lib/elixir/lib/json.ex:1-117 +defprotocol JSON.Encoder do + @moduledoc "A protocol for custom JSON encoding..." + def encode(term, encoder) +end +``` + +### Implementing for specific types + +```elixir +# lib/elixir/lib/json.ex:120-128 +defimpl JSON.Encoder, for: Atom do + def encode(value, encoder) do + case value do + nil -> "null" + true -> "true" + false -> "false" + _ -> encoder.(Atom.to_string(value), encoder) + end + end +end + +# lib/elixir/lib/json.ex:130-134 +defimpl JSON.Encoder, for: BitString do + def encode(value, _encoder) do + :json.encode_binary(value) + end +end +``` + +### Deriving protocol implementations + +```elixir +# lib/elixir/lib/inspect.ex:78-80 (from moduledoc example) +defmodule User do + @derive {Inspect, only: [:id, :name]} + defstruct [:id, :name, :address] +end +``` + +```elixir +# lib/elixir/lib/json.ex:12-14 (from JSON.Encoder @moduledoc) +@derive {JSON.Encoder, only: [...]} +defstruct ... +``` + +`@derive` generates a protocol implementation at compile time. The `only:` option prevents accidentally leaking private fields. + +--- + +## 5. The `use` Macro and `__using__/1` Pattern + +`use` is not inheritance. It's compile-time code injection. The convention is to use it for setting up behaviours and generating boilerplate. + +### What `use GenServer` actually does + +```elixir +# lib/elixir/lib/gen_server.ex:899-1002 +defmacro __using__(opts) do + quote location: :keep, bind_quoted: [opts: opts] do + @behaviour GenServer # line 901 + + def child_spec(init_arg) do # line 911 + default = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, [init_arg]} + } + Supervisor.child_spec(default, unquote(Macro.escape(opts))) + end + + defoverridable child_spec: 1 # line 921 + + @before_compile GenServer # line 924 + + def handle_call(msg, _from, state) do # line 926 — raises with helpful error + ... + end + + def handle_info(msg, state) do # line 943 — logs warning about unhandled messages + ... + end + + def handle_cast(msg, state) do # line 973 — raises with helpful error + ... + end + + def terminate(_reason, _state), do: :ok # line 992 + def code_change(_old, state, _extra), do: {:ok, state} # line 997 + + defoverridable code_change: 3, terminate: 2, # line 1002 + handle_info: 2, handle_cast: 2, handle_call: 3 + end +end +``` + +It sets the behaviour, defines `child_spec/1`, injects sensible default callbacks (that raise helpful errors), and marks them as overridable. The `@before_compile` hook warns if `init/1` is missing. + +### The anti-pattern (from Kernel docs) + +```elixir +# lib/elixir/lib/kernel.ex:6087-6096 (documentation for `use/2`) +# DON'T do this — just use `import` directly +defmodule MyModule do + defmacro __using__(_opts) do + quote do + import MyModule + end + end +end +``` + +The `use/2` docs (`lib/elixir/lib/kernel.ex:6060-6096`) explicitly say: don't use `__using__` if all it does is import the module. Let callers import/alias directly. + +--- + +## 6. Behaviour Callbacks with `@impl` + +Behaviours define callbacks. `@impl true` marks which functions fulfill which contract. + +### Defining callbacks + +```elixir +# lib/elixir/lib/gen_server.ex:577 (@callback init) +@callback init(init_arg :: term) :: + {:ok, state} + | {:ok, state, timeout | :hibernate | {:continue, continue_arg}} + | :ignore + | {:stop, reason :: any} + +# lib/elixir/lib/gen_server.ex:647 (@callback handle_call) +@callback handle_call(request :: term, from, state :: term) :: + {:reply, reply, new_state} + | {:reply, reply, new_state, timeout | :hibernate | {:continue, continue_arg}} + | {:noreply, new_state} + | ... + +# lib/elixir/lib/gen_server.ex:853 +@optional_callbacks code_change: 3, terminate: 2, format_status: 1, format_status: 2 +``` + +### Implementing with `@impl` + +```elixir +# lib/elixir/lib/exception.ex:1102-1103 +@impl true +def blame(%{message: message} = exception, [{:erlang, fun, args, _} | _] = stacktrace) do + ... +end +``` + +`@impl true` tells both the compiler and readers: "this function is fulfilling a behaviour contract." The compiler will warn if you annotate a function that doesn't match any callback. + +--- + +## 7. Binary Pattern Matching for Parsing + +Elixir inherits Erlang's powerful binary pattern matching. The standard library uses it extensively for parsing. + +### Recursive binary parser + +```elixir +# lib/elixir/lib/option_parser.ex:600-630 +# If we have an escaped quote, simply remove the escape +defp do_split(<>, buffer, acc, quote), + do: do_split(t, <>, acc, quote) + +# If we have a quote and we were not in a quote, start one +defp do_split(<>, buffer, acc, nil) when quote in [?", ?'], + do: do_split(t, buffer, acc, quote) + +# If we have a quote and we were inside it, close it +defp do_split(<>, buffer, acc, quote), + do: do_split(t, buffer, acc, nil) + +# If we have space and we are outside of a quote, start new segment +defp do_split(<>, buffer, acc, nil), + do: do_split(String.trim_leading(t, " "), "", [buffer | acc], nil) + +# All other characters are moved to buffer +defp do_split(<>, buffer, acc, quote) do + do_split(t, <>, acc, quote) +end + +# Finish the string expecting a nil marker +defp do_split(<<>>, "", acc, nil), do: Enum.reverse(acc) +defp do_split(<<>>, buffer, acc, nil), do: Enum.reverse([buffer | acc]) +``` + +This is a state machine encoded as function clauses. The `quote` parameter tracks parser state (inside quotes or not). Each clause handles one character class. No regex, no mutable state. + +### Compile-time generated clauses for character matching + +```elixir +# lib/elixir/lib/string.ex:345-356 +for char <- 0x20..0x7E do + defp recur_printable?(<>, character_limit) do + recur_printable?(rest, decrement(character_limit)) + end +end + +for char <- [?\n, ?\r, ?\t, ?\v, ?\b, ?\f, ?\e, ?\d, ?\a] do + defp recur_printable?(<>, character_limit) do + recur_printable?(rest, decrement(character_limit)) + end +end +``` + +Metaprogramming generates one function clause per printable character. The BEAM compiles this into an efficient jump table. This is faster than runtime range checks. + +--- + +## 8. Supervisor and Child Specs + +The OTP supervision tree pattern is central to Elixir applications. The standard library shows exactly how to structure it. + +### `child_spec/1` — the universal entry point + +```elixir +# lib/elixir/lib/gen_server.ex:911-920 +def child_spec(init_arg) do + default = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, [init_arg]} + } + Supervisor.child_spec(default, unquote(Macro.escape(opts))) +end + +defoverridable child_spec: 1 +``` + +Every supervised module defines `child_spec/1`. It returns a map with `:id` and `:start`. Users override it via `defoverridable` to customize restart strategies. + +### Supervisor's multi-clause `init_child` + +```elixir +# lib/elixir/lib/supervisor.ex:816-843 +defp init_child(module) when is_atom(module) do + init_child({module, []}) +end + +defp init_child({module, arg}) when is_atom(module) do + try do + module.child_spec(arg) + rescue + e in UndefinedFunctionError -> + case __STACKTRACE__ do + [{^module, :child_spec, [^arg], _} | _] -> + raise ArgumentError, child_spec_error(module) + stack -> + reraise e, stack + end + end +end + +defp init_child(map) when is_map(map) do + map +end + +defp init_child(other) do + raise ArgumentError, """ + supervisors expect each child to be one of the following: + * a module + * a {module, arg} tuple + * a child specification as a map with at least the :id and :start fields + Got: #{inspect(other)} + """ +end +``` + +Three valid forms, one error clause. The error message tells you exactly what's acceptable. The rescue clause catches the specific case where `child_spec/1` isn't defined and provides a helpful message instead of a cryptic `UndefinedFunctionError`. + +--- + +## 9. GenServer Patterns + +### Separate client API from server callbacks + +```elixir +# lib/elixir/lib/gen_server.ex:39-61 (Stack example in moduledoc) +defmodule Stack do + use GenServer + + # Callbacks (server-side) + @impl true + def init(elements) do # line 44 + initial_state = String.split(elements, ",", trim: true) + {:ok, initial_state} + end + + @impl true + def handle_call(:pop, _from, state) do # line 50 + [to_caller | new_state] = state + {:reply, to_caller, new_state} + end + + @impl true + def handle_cast({:push, element}, state) do # line 56 + new_state = [element | state] + {:noreply, new_state} + end +end +``` + +### Multi-clause `cast/2` for different server locations + +```elixir +# lib/elixir/lib/gen_server.ex:1200-1225 +def cast({:global, name}, request) do + try do + :global.send(name, cast_msg(request)) + :ok + catch + _, _ -> :ok + end +end + +def cast({:via, mod, name}, request) do + try do + mod.send(name, cast_msg(request)) + :ok + catch + _, _ -> :ok + end +end + +def cast({name, node}, request) when is_atom(name) and is_atom(node), + do: do_send({name, node}, cast_msg(request)) + +def cast(dest, request) when is_atom(dest) or is_pid(dest), + do: do_send(dest, cast_msg(request)) +``` + +`cast` always returns `:ok` regardless of whether delivery succeeded — fire-and-forget semantics. Different clauses handle different server location strategies. + +--- + +## 10. Agent as a Thin GenServer Wrapper + +The Agent module demonstrates how to build focused abstractions over GenServer. + +```elixir +# lib/elixir/lib/agent.ex:280-282 +def start_link(fun, options \\ []) when is_function(fun, 0) do + GenServer.start_link(Agent.Server, fun, options) +end +``` + +```elixir +# lib/elixir/lib/agent.ex:344-346 +def get(agent, fun, timeout \\ 5000) when is_function(fun, 1) do + GenServer.call(agent, {:get, fun}, timeout) +end +``` + +```elixir +# lib/elixir/lib/agent.ex:426-428 +def update(agent, fun, timeout \\ 5000) when is_function(fun, 1) do + GenServer.call(agent, {:update, fun}, timeout) +end +``` + +The entire Agent API is thin wrappers around `GenServer.call` and `GenServer.cast`. No custom process loop. No reinvented wheel. This is the "right abstraction depth" — expose a focused API that hides the message-passing mechanics. + +--- + +## 11. Options Validation Pattern + +The standard library has a clear convention for validating keyword-list options. + +### Early, explicit validation with helpful errors + +```elixir +# lib/elixir/lib/registry.ex:380-452 +def start_link(options) do + keys = Keyword.get(options, :keys) + + kind = + case keys do + {:duplicate, partition_strategy} when partition_strategy in [:key, :pid] -> + {:duplicate, partition_strategy} + :unique -> :unique + :duplicate -> {:duplicate, :pid} + _ -> + raise ArgumentError, + "expected :keys to be given and be one of :unique, :duplicate, " <> + "{:duplicate, :key}, or {:duplicate, :pid}, got: #{inspect(keys)}" + end + + name = + case Keyword.fetch(options, :name) do + {:ok, name} when is_atom(name) -> name + {:ok, other} -> + raise ArgumentError, "expected :name to be an atom, got: #{inspect(other)}" + :error -> + raise ArgumentError, "expected :name option to be present" + end + ... +end +``` + +### `Keyword.validate!/2` for simpler cases + +```elixir +# lib/elixir/lib/keyword.ex:272-300 (validate/2 implementation) +def validate(keyword, values) when is_list(keyword) and is_list(values) do + validate(keyword, values, [], keyword, []) +end +# Returns {:ok, keyword_with_defaults} | {:error, invalid_keys} + +# lib/elixir/lib/keyword.ex:355-361 (validate!/2 — the raising wrapper) +def validate!(keyword, values) do + case validate(keyword, values) do + {:ok, keyword} -> keyword + {:error, keys} -> raise ArgumentError, ... + end +end +``` + +Use `Keyword.validate!/2` when your options are simple atoms with defaults. Use explicit `case` chains (like Registry does at `lib/elixir/lib/registry.ex:380-452`) when options have complex constraints or interdependencies. + +--- + +## 12. The Pipe Operator and Pipeline-Friendly APIs + +### How `|>` works + +```elixir +# lib/elixir/lib/kernel.ex:4509-4514 +defmacro left |> right do + fun = fn {x, pos}, acc -> + Macro.pipe(acc, x, pos) + end + + :lists.foldl(fun, left, Macro.unpipe(right)) +end +``` + +The pipe operator is a macro that rewrites `a |> f(b)` into `f(a, b)` at compile time. Zero runtime cost. + +### `tap/2` for side effects in pipelines + +```elixir +# lib/elixir/lib/kernel.ex:1403-1408 +defmacro tap(value, fun) do + quote bind_quoted: [fun: fun, value: value] do + _ = fun.(value) + value + end +end +``` + +`tap` runs a function for its side effect and returns the original value unchanged. The `_ =` suppresses unused-return warnings. + +### `then/2` for non-first-argument piping + +```elixir +# lib/elixir/lib/kernel.ex:2836-2840 +defmacro then(value, fun) do + quote do + unquote(fun).(unquote(value)) + end +end +``` + +`then` passes the piped value to a function that returns a new value. Use it when the piped value isn't the first argument: `value |> then(&Map.get(other, &1))`. + +### Design your APIs pipe-first + +The convention throughout the standard library: the "subject" (the data being transformed) is always the first argument. + +```elixir +# Enum — enumerable always first +Enum.map(list, &transform/1) # lib/elixir/lib/enum.ex:1724 +Enum.filter(list, &predicate/1) # lib/elixir/lib/enum.ex:1119 + +# Map — map always first +Map.put(map, key, value) # lib/elixir/lib/map.ex:646 +Map.get(map, key) # lib/elixir/lib/map.ex:587 + +# String — string always first +String.split(string, pattern) # lib/elixir/lib/string.ex:516 +String.trim(string) # lib/elixir/lib/string.ex:1380 +``` + +This enables natural pipelines: + +```elixir +data +|> Enum.filter(&valid?/1) +|> Enum.map(&transform/1) +|> Enum.sort_by(& &1.priority) +``` + +--- + +## 13. Enumerable Protocol and Reduce as Foundation + +All enumeration in Elixir is built on a single primitive: `reduce/3`. + +### The Enumerable protocol + +```elixir +# lib/elixir/lib/enum.ex:177-180 (from docs) +def reduce(_list, {:halt, acc}, _fun), do: {:halted, acc} +def reduce(list, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(list, &1, fun)} +def reduce([], {:cont, acc}, _fun), do: {:done, acc} +def reduce([head | tail], {:cont, acc}, fun), do: reduce(tail, fun.(head, acc), fun) +``` + +Four clauses define the entire enumeration model. The accumulator is a tagged tuple that controls flow: `:cont` continues, `:halt` stops immediately, `:suspend` pauses for later resumption. + +### Everything is built on reduce + +```elixir +# lib/elixir/lib/enum.ex:2675-2677 +def reduce_while(enumerable, acc, fun) do + Enumerable.reduce(enumerable, {:cont, acc}, fun) |> elem(1) +end +``` + +Even `reduce_while` is a one-liner that leverages the tag-based control of the underlying protocol. + +--- + +## 14. Stream — Lazy Composition + +Streams compose transformations without executing them. Execution happens only when consumed by an eager function. + +```elixir +# lib/elixir/lib/stream.ex:60-66 (moduledoc example) +stream = 1..3 +|> Stream.map(&IO.inspect(&1)) +|> Stream.map(&(&1 * 2)) +|> Stream.map(&IO.inspect(&1)) +Enum.to_list(stream) +# Prints: 1, 2, 2, 4, 3, 6 +# The list was enumerated just once! +``` + +The key insight: `Stream.map` returns a recipe (a struct containing the enumerable + the function), not a result. Only `Enum.to_list/1` (or any `Enum` function) triggers execution. + +--- + +## 15. Collectable — The Dual of Enumerable + +While `Enumerable` defines how to take elements out, `Collectable` defines how to put elements in. + +```elixir +# lib/elixir/lib/collectable.ex:192-206 +defimpl Collectable, for: Map do + def into(map) do + fun = fn + map_acc, {:cont, {key, value}} -> + Map.put(map_acc, key, value) + map_acc, :done -> + map_acc + _map_acc, :halt -> + :ok + end + {map, fun} + end +end +``` + +The `into/1` function returns `{initial_accumulator, collector_function}`. The collector handles three commands: `{:cont, element}` to add, `:done` to finalize, `:halt` to abort. This powers `Enum.into/2` and `for` comprehensions with `into:`. + +--- + +## 16. Task for Concurrent Work + +### Async/Await with ownership tracking + +```elixir +# lib/elixir/lib/task.ex:875-897 +def await(%Task{ref: ref, owner: owner} = task, timeout \\ 5000) when is_timeout(timeout) do + if owner != self() do + raise ArgumentError, invalid_owner_error(task) + end + await_receive(ref, task, timeout) +end + +# lib/elixir/lib/task.ex:883-897 +defp await_receive(ref, task, timeout) do + receive do + {^ref, reply} -> + demonitor(ref) + reply + {:DOWN, ^ref, _, proc, reason} -> + exit({reason(reason, proc), {__MODULE__, :await, [task, timeout]}}) + after + timeout -> + demonitor(ref) + exit({:timeout, {__MODULE__, :await, [task, timeout]}}) + end +end +``` + +Key details: Tasks enforce ownership (only the spawning process can await). The `^ref` pin in `receive` ensures you only match YOUR task's response. The `:DOWN` handler means you get clean exits if the task crashes. + +--- + +## 17. Documentation as Code + +### DocTests — examples that are tests + +```elixir +# lib/elixir/lib/enum.ex:480-496 (Enum.at/3 documentation) +@doc """ +... +## Examples + + iex> Enum.at([2, 4, 6], 0) + 2 + + iex> Enum.at([2, 4, 6], 4) + nil + + iex> Enum.at([2, 4, 6], 4, :none) + :none +""" +``` + +Every `iex>` block in `@doc` is automatically extracted and run as a test by ExUnit. This guarantees documentation examples are always correct. + +### Typespec annotations + +```elixir +# lib/elixir/lib/enum.ex:1722-1724 +@spec map(t, (element -> any)) :: list +def map(enumerable, fun) + +def map(enumerable, fun) when is_list(enumerable) do +``` + +The `@spec` immediately precedes the function head. Use generic typespec variables (`t`, `element`) from `@type` definitions in the module. + +### `@doc since:` for version tracking + +```elixir +# lib/elixir/lib/enum.ex:1763-1765 +@doc since: "1.4.0" +@spec map_every(t, non_neg_integer, (element -> any)) :: list +def map_every(enumerable, nth, fun) +``` + +### Deprecation without removal + +```elixir +# lib/elixir/lib/enum.ex:503-505 +@doc false +@deprecated "Use Enum.chunk_every/2 instead" +def chunk(enumerable, count), do: chunk(enumerable, count, count, nil) +``` + +`@doc false` hides from documentation. `@deprecated` emits compile-time warnings. The function still works — no breaking change. + +--- + +## 18. Parameterized Testing + +```elixir +# lib/elixir/test/elixir/registry_test.exs:13-19 +use ExUnit.Case, + async: true, + parameterize: + for( + keys <- [:unique, :duplicate, {:duplicate, :pid}, {:duplicate, :key}], + partitions <- [1, 8], + do: %{keys: keys, partitions: partitions} + ) +``` + +ExUnit's `parameterize` option (since v1.18) runs the same tests with different configurations. Combined with `async: true`, different parameter sets run concurrently. + +--- + +## 19. Error Messages Tell You What's Acceptable + +Throughout the codebase, error messages don't just say what went wrong — they say what was expected: + +```elixir +# lib/elixir/lib/supervisor.ex:843-856 +raise ArgumentError, """ +supervisors expect each child to be one of the following: + + * a module + * a {module, arg} tuple + * a child specification as a map with at least the :id and :start fields + +Got: #{inspect(other)} +""" +``` + +```elixir +# lib/elixir/lib/registry.ex:402-404 +raise ArgumentError, + "expected :keys to be given and be one of :unique, :duplicate, " <> + "{:duplicate, :key}, or {:duplicate, :pid}, got: #{inspect(keys)}" +``` + +Pattern: `"expected X, got: #{inspect(actual_value)}"`. Always `inspect` the bad value so the developer sees what they actually passed. + +--- + +## 20. `defoverridable` for Extension Points + +```elixir +# lib/elixir/lib/gen_server.ex:911-921 +def child_spec(init_arg) do + ... +end +defoverridable child_spec: 1 + +# lib/elixir/lib/gen_server.ex:1002 +defoverridable code_change: 3, terminate: 2, handle_info: 2, + handle_cast: 2, handle_call: 3 +``` + +`defoverridable` provides sensible defaults that modules can replace. The key insight: inject working defaults first, THEN mark overridable. Users only override what they need. + +--- + +## 21. The `decrement(:infinity)` Idiom + +When a limit might be "no limit," use `:infinity` as a sentinel and handle it in a dedicated clause: + +```elixir +# lib/elixir/lib/string.ex:368-369 +defp decrement(:infinity), do: :infinity +defp decrement(character_limit), do: character_limit - 1 +``` + +This avoids sentinel values like `-1` or `nil`. The `:infinity` atom is self-documenting and impossible to confuse with a valid numeric value. + +--- + +## 22. Guard Definitions with `defguard` + +```elixir +# lib/elixir/lib/kernel.ex:5886-5916 (defguard documentation and definition) +defmodule Integer.Guards do + defguard is_even(value) when is_integer(value) and rem(value, 2) == 0 +end +``` + +Custom guards can be used in function heads and `case`/`cond`/`receive` clauses. They must only use guard-safe expressions (no function calls that might have side effects). + +--- + +## 23. Delegation to Erlang + +The standard library frequently delegates to Erlang when it's the right tool: + +```elixir +# lib/elixir/lib/enum.ex:3221-3223 +def sort(enumerable) when is_list(enumerable) do + :lists.sort(enumerable) +end +``` + +```elixir +# lib/elixir/lib/gen_server.ex:1318-1320 +def reply(client, reply) do + :gen.reply(client, reply) +end +``` + +```elixir +# lib/elixir/lib/json.ex:131-133 +defimpl JSON.Encoder, for: BitString do + def encode(value, _encoder) do + :json.encode_binary(value) + end +end +``` + +Don't rewrite what Erlang already does well. Wrap it with an Elixir-idiomatic API (keyword options, `{:ok, _}` tuples, pipe-friendly argument order). + +--- + +## 24. `sort/2` Accepting Multiple Forms + +A flexible API accepts different input shapes: + +```elixir +# lib/elixir/lib/enum.ex:3305-3325 +def sort(enumerable, sorter) when is_list(enumerable) do + case sorter do + :asc -> :lists.sort(enumerable) + :desc -> :lists.sort(enumerable) |> :lists.reverse() + _ -> :lists.sort(to_sort_fun(sorter), enumerable) + end +end + +defp to_sort_fun(sorter) when is_function(sorter, 2), do: sorter +defp to_sort_fun(:asc), do: &<=/2 +defp to_sort_fun(:desc), do: &>=/2 +defp to_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :gt) +defp to_sort_fun({:asc, module}) when is_atom(module), do: &(module.compare(&1, &2) != :gt) +defp to_sort_fun({:desc, module}) when is_atom(module), do: &(module.compare(&1, &2) != :lt) +``` + +The `sorter` argument accepts: a 2-arity function, `:asc`/`:desc` atoms, a module with `compare/2`, or a `{:asc/:desc, module}` tuple. Private `to_sort_fun` normalizes all forms to a function. The public API is flexible; the internals are uniform. + +--- + +## 25. Naming Conventions + +From the codebase patterns: + +| Convention | Example | Source | +|---|---|---| +| `fetch` returns `{:ok, val} \| :error` | `Access.fetch/2` | `lib/elixir/lib/access.ex:247` | +| `fetch!` raises on missing | `Access.fetch!/2` | `lib/elixir/lib/access.ex:291` | +| `get` returns value or default | `Access.get/3` | `lib/elixir/lib/access.ex:318` | +| `is_` prefix for guards | `Kernel.is_struct/1` | `lib/elixir/lib/kernel.ex:2624` | +| `new` for struct construction | `MapSet.new/0` | `lib/elixir/lib/map_set.ex:82` | +| `to_` for type conversion | `Kernel.to_timeout/1` | `lib/elixir/lib/kernel.ex:6405` | +| `from_` for parsing | `Date.from_iso8601/1` | `lib/elixir/lib/calendar/date.ex:360` | +| Private helpers: `do_*` or `*_list` | `do_split`, `filter_list` | `lib/elixir/lib/option_parser.ex:600`, `lib/elixir/lib/enum.ex:4541` | + +--- + +## Summary: The Elixir Way + +1. **Express logic as function clauses**, not nested conditionals +2. **Put the subject first** for pipe-friendliness +3. **Return `{:ok, _} | {:error, _}`** for operations that can fail; provide `!` variants +4. **Use protocols** for type-based polymorphism, not runtime type checks +5. **Validate options early** with helpful error messages that say what's expected +6. **Delegate to Erlang** when it has the right primitive; wrap with Elixir conventions +7. **Write examples as doctests** — they're documentation and tests simultaneously +8. **Use `@impl true`** on every behaviour callback so the compiler verifies you +9. **Separate client API from server callbacks** in GenServer modules +10. **Design for the pipe** — transformations compose left-to-right + +--- + +*Generated from Elixir source (HEAD, commit as of 2026-04-29). All file paths relative to the repository root. Line numbers verified against the current main branch by direct inspection via `sed -n`.*