21a5ea0d58
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.
1037 lines
30 KiB
Markdown
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`.*
|