Files
elixir-patterns/README.md
T
Rodin 21a5ea0d58 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.
2026-04-29 22:57:22 -07:00

1037 lines
30 KiB
Markdown

# 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(<<?%, tail::binary>>, acc, spaces) do
with <<hex1, hex2, tail::binary>> <- tail,
dec1 when is_integer(dec1) <- hex_to_dec(hex1),
dec2 when is_integer(dec2) <- hex_to_dec(hex2) do
unpercent(tail, <<acc::binary, bsl(dec1, 4) + dec2>>, spaces)
else
_ -> unpercent(tail, <<acc::binary, ?%>>, 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(<<?\\, quote, t::binary>>, buffer, acc, quote),
do: do_split(t, <<buffer::binary, quote>>, acc, quote)
# If we have a quote and we were not in a quote, start one
defp do_split(<<quote, t::binary>>, 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(<<quote, t::binary>>, 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(<<?\s, t::binary>>, buffer, acc, nil),
do: do_split(String.trim_leading(t, " "), "", [buffer | acc], nil)
# All other characters are moved to buffer
defp do_split(<<h, t::binary>>, buffer, acc, quote) do
do_split(t, <<buffer::binary, h>>, 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?(<<unquote(char), rest::binary>>, 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?(<<unquote(char), rest::binary>>, 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`.*