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