Compare commits

..

1 Commits

Author SHA1 Message Date
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
14 changed files with 1030 additions and 13587 deletions
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Aaron Weiker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1030 -9
View File
File diff suppressed because it is too large Load Diff
-681
View File
@@ -1,681 +0,0 @@
# Behaviour Patterns
How behaviours are designed, implemented, and used in Elixir core and Phoenix.
## 1. Behaviour Definition with `@callback`
**Source:** [lib/elixir/lib/gen_server.ex#L577](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L577) (all callback definitions)
```elixir
@callback init(init_arg :: term) ::
{:ok, state}
| {:ok, state, timeout | :hibernate | {:continue, continue_arg}}
| :ignore
| {:stop, reason :: any}
@callback handle_call(request :: term, from, state :: term) ::
{:reply, reply, new_state}
| {:noreply, new_state}
| {:stop, reason, reply, new_state}
| {:stop, reason, new_state}
when reply: term, new_state: term, reason: term
```
**Why:** Callbacks with full type unions document every valid return. Named parameters (`init_arg`, `request`, `state`) serve as documentation. The `when` clause defines type variables used across the union.
**Anti-pattern:** Defining callbacks with `@callback handle_call(term, term, term) :: term` — provides zero guidance to implementors.
### When to Use
**Triggers:**
- You're defining a contract that multiple modules will implement differently
- You want compile-time guarantees that implementors provide required functions
- The return type has multiple valid shapes that implementors must choose between
**Example — before:**
```elixir
# No formal contract — just "convention" in a README
defmodule MyApp.PaymentGateway do
def charge(amount, card), do: raise "implement me"
end
```
**Example — after:**
```elixir
defmodule MyApp.PaymentGateway do
@callback charge(amount :: pos_integer(), card :: card_token()) ::
{:ok, transaction_id :: String.t()}
| {:declined, reason :: String.t()}
| {:error, :network_timeout | :invalid_card}
when card_token: String.t()
end
```
### When NOT to Use
**Don't use this when:**
- There's only one implementation and no plans for more (just use a module)
- The "contract" is so simple it's a single function with one return type — a protocol or simple module works better
- You need runtime dispatch based on data type (use protocols instead)
**Over-application example:**
```elixir
defmodule MyApp.Config do
@callback get(key :: atom()) :: term()
@callback put(key :: atom(), value :: term()) :: :ok
end
# Only ever one implementation:
defmodule MyApp.Config.Env do
@behaviour MyApp.Config
def get(key), do: Application.get_env(:my_app, key)
def put(key, value), do: Application.put_env(:my_app, key, value)
end
```
**Better alternative:**
```elixir
# Just a module — no behaviour ceremony needed for a singleton
defmodule MyApp.Config do
def get(key), do: Application.get_env(:my_app, key)
def put(key, value), do: Application.put_env(:my_app, key, value)
end
```
**Why:** Behaviours add value through polymorphism — multiple implementations behind one contract. A behaviour with exactly one implementation is indirection without benefit. Add the behaviour when the second implementation arrives.
---
## 2. `@optional_callbacks` for Extensibility
**Source:** [lib/phoenix/channel.ex#L442](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L442)
```elixir
@optional_callbacks handle_in: 3,
handle_out: 3,
handle_info: 2,
handle_call: 3,
handle_cast: 2,
code_change: 3,
terminate: 2
```
**Why:** Only `join/3` is required for a Channel. Everything else has sensible defaults. This keeps the minimum implementation surface small — a Channel that just joins and broadcasts needs only one function.
**Anti-pattern:** Making all callbacks required when most have reasonable defaults — forces implementors to write boilerplate they don't need.
### When to Use
**Triggers:**
- Your behaviour has callbacks where most implementors will use a default (e.g., `terminate/2`, `code_change/3`)
- You want a minimal "get started" experience — implement one function, everything else works
- Some callbacks are only needed for advanced use cases
**Example — before:**
```elixir
defmodule MyApp.Worker do
@callback init(args :: term()) :: {:ok, state :: term()}
@callback handle_task(task :: term(), state :: term()) :: {:ok, state :: term()}
@callback on_error(error :: term(), state :: term()) :: {:ok, state :: term()}
@callback on_shutdown(reason :: term(), state :: term()) :: :ok
end
# Implementors MUST define all 4, even if on_error and on_shutdown are no-ops
```
**Example — after:**
```elixir
defmodule MyApp.Worker do
@callback init(args :: term()) :: {:ok, state :: term()}
@callback handle_task(task :: term(), state :: term()) :: {:ok, state :: term()}
@callback on_error(error :: term(), state :: term()) :: {:ok, state :: term()}
@callback on_shutdown(reason :: term(), state :: term()) :: :ok
@optional_callbacks on_error: 2, on_shutdown: 2
end
```
### When NOT to Use
**Don't use this when:**
- The callback is essential to correctness (if skipping it would break the system, it's required)
- Every implementor WILL need to customize the behavior (making it optional hides a real requirement)
- You have only 1-2 callbacks total — if they're all optional, why is it a behaviour?
**Over-application example:**
```elixir
defmodule MyApp.Serializer do
@callback encode(term()) :: {:ok, binary()} | {:error, term()}
@callback decode(binary()) :: {:ok, term()} | {:error, term()}
@optional_callbacks encode: 1, decode: 1 # Both optional?!
end
```
**Better alternative:**
```elixir
defmodule MyApp.Serializer do
@callback encode(term()) :: {:ok, binary()} | {:error, term()}
@callback decode(binary()) :: {:ok, term()} | {:error, term()}
# Both are required — a serializer that can't encode or decode isn't a serializer
end
```
**Why:** If ALL callbacks are optional, the behaviour provides no compile-time guarantees. At least one callback should be required to justify the behaviour's existence. Optional callbacks are for extensions, not the core contract.
---
## 3. `@behaviour` Declaration in `__using__`
**Source:** [lib/phoenix/channel.ex#L450](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L450)
```elixir
defmacro __using__(opts \\ []) do
quote do
opts = unquote(opts)
@behaviour unquote(__MODULE__)
@on_definition unquote(__MODULE__)
@before_compile unquote(__MODULE__)
...
end
end
```
**Source:** [lib/elixir/lib/gen_server.ex#L836](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L836)
```elixir
quote location: :keep, bind_quoted: [opts: opts] do
@behaviour GenServer
...
end
```
**Why:** Setting `@behaviour` inside `use` means users get compile-time warnings about missing callbacks automatically. They don't need to know about the behaviour mechanism — `use Phoenix.Channel` handles it.
**Anti-pattern:** Requiring users to manually add both `use MyModule` AND `@behaviour MyModule`.
### When to Use
**Triggers:**
- Your behaviour requires boilerplate that every implementor would write identically
- You want `use MyBehaviour` to "just work" with compile-time callback verification
- The behaviour has associated module attributes, process setup, or struct definitions
**Example — before:**
```elixir
# Users must remember both steps
defmodule MyWorker do
@behaviour MyApp.Worker
# Easy to forget @behaviour and lose compile-time checks
def init(_), do: {:ok, %{}}
end
```
**Example — after:**
```elixir
defmodule MyApp.Worker do
defmacro __using__(_opts) do
quote do
@behaviour MyApp.Worker
# Compile-time checks are automatic
end
end
end
defmodule MyWorker do
use MyApp.Worker
def init(_), do: {:ok, %{}}
end
```
### When NOT to Use
**Don't use this when:**
- The behaviour has no associated boilerplate — a bare `@behaviour` declaration is sufficient
- You're creating a `use` macro that only sets `@behaviour` and nothing else (unnecessary indirection)
- The module being "used" doesn't define callbacks (it's a utility, not a behaviour)
**Over-application example:**
```elixir
defmodule MyApp.Formatter do
@callback format(term()) :: String.t()
defmacro __using__(_opts) do
quote do
@behaviour MyApp.Formatter
# That's it — nothing else generated
end
end
end
```
**Better alternative:**
```elixir
defmodule MyApp.Formatter do
@callback format(term()) :: String.t()
end
# Users just add @behaviour directly — simpler, more explicit
defmodule HtmlFormatter do
@behaviour MyApp.Formatter
def format(data), do: ...
end
```
**Why:** `use` implies "this macro generates code for you." If it only sets `@behaviour`, the indirection hides what's happening without saving any work. Use `use` when there's actual code generation; use bare `@behaviour` when there isn't.
---
## 4. Default Implementations via `defoverridable`
**Source:** [lib/elixir/lib/gen_server.ex#L849](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L849)
```elixir
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
```
**Why:** `defoverridable` provides a working default that users CAN customize but don't HAVE to. The generated function works for the 90% case. The 10% can override it.
**Anti-pattern:** Not using `defoverridable` — users who need custom behavior must bypass the `use` macro entirely.
### When to Use
**Triggers:**
- The 90% case has an obvious default implementation (e.g., `child_spec/1`, `terminate/2`)
- You want users to opt-in to customization rather than requiring it
- The default is non-trivial enough that users shouldn't have to copy-paste it
**Example — before:**
```elixir
defmacro __using__(_opts) do
quote do
@behaviour MyApp.Plugin
# No default — every plugin must implement format_output/1
# even though 90% just want to call inspect()
end
end
```
**Example — after:**
```elixir
defmacro __using__(_opts) do
quote do
@behaviour MyApp.Plugin
def format_output(data), do: inspect(data, pretty: true)
defoverridable format_output: 1
end
end
```
### When NOT to Use
**Don't use this when:**
- The default implementation would be wrong for most cases (forces users to override = same as required)
- The function is the core purpose of the behaviour (e.g., `handle_call` in GenServer has no default because the POINT is to define it)
- You're providing a default that silently swallows errors or does nothing
**Over-application example:**
```elixir
defmacro __using__(_opts) do
quote do
@behaviour MyApp.EventHandler
def handle_event(_event, state), do: {:ok, state}
defoverridable handle_event: 2
end
end
# Now users can "implement" the behaviour without handling ANY events
# Bugs hide because unhandled events silently succeed
```
**Better alternative:**
```elixir
defmacro __using__(_opts) do
quote do
@behaviour MyApp.EventHandler
# handle_event/2 is required — no default
# If you don't handle events, you don't need this behaviour
end
end
```
**Why:** A default implementation that does nothing for the CORE callback creates a pit of failure — modules compile cleanly while silently dropping events. Defaults should be for auxiliary concerns (logging, shutdown, serialization), not the primary contract.
---
## 5. Phoenix Channel: Behaviour + Process + Protocol
**Source:** [lib/phoenix/channel.ex#L364](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L364) (full callback set)
The Channel behaviour combines:
1. **Required callback:** `join/3` (authorization gate)
2. **Optional callbacks:** `handle_in/3`, `handle_info/2`, etc. (event handlers)
3. **Process semantics:** Each channel is a GenServer (line 476-479)
4. **Configuration via module attributes:** `@phoenix_log_join`, `@phoenix_hibernate_after`
```elixir
# From __using__ — configures the process
@phoenix_hibernate_after Keyword.get(opts, :hibernate_after, 15_000)
@phoenix_shutdown Keyword.get(opts, :shutdown, 5000)
def child_spec(init_arg) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [init_arg]},
shutdown: @phoenix_shutdown,
restart: :temporary
}
end
def start_link(triplet) do
GenServer.start_link(Phoenix.Channel.Server, triplet,
hibernate_after: @phoenix_hibernate_after
)
end
```
**Why:** The Channel behaviour demonstrates layering — it's a behaviour (compile-time contract), a process (runtime entity), and configurable (via options to `use`). Each concern is handled by the appropriate mechanism.
**Anti-pattern:** Trying to encode runtime configuration in the behaviour contract itself, or conflating compile-time and runtime concerns.
### When to Use
**Triggers:**
- Your behaviour involves a running process (GenServer, Agent, Task-like)
- The module needs both compile-time contracts AND runtime configuration
- Different options affect process lifecycle (timeouts, shutdown strategies, restart policies)
**Example — before:**
```elixir
defmodule MyApp.Worker do
@callback handle_job(job :: term()) :: :ok | {:error, term()}
# No process semantics, no configuration — just a callback
end
```
**Example — after:**
```elixir
defmodule MyApp.Worker do
@callback init(args :: term()) :: {:ok, state :: term()}
@callback handle_job(job :: term(), state :: term()) :: {:ok, state :: term()} | {:error, term(), state :: term()}
defmacro __using__(opts) do
quote do
@behaviour MyApp.Worker
@worker_timeout Keyword.get(unquote(opts), :timeout, 30_000)
@worker_max_retries Keyword.get(unquote(opts), :max_retries, 3)
def child_spec(init_arg) do
%{id: __MODULE__, start: {__MODULE__, :start_link, [init_arg]}, restart: :transient}
end
defoverridable child_spec: 1
end
end
end
```
### When NOT to Use
**Don't use this when:**
- The behaviour is purely functional (no process, no state) — keep it simple
- You're conflating too many concerns (behaviour + process + config + routing) in one module
- The "configuration" is better handled at runtime via application config rather than compile-time module attributes
**Over-application example:**
```elixir
defmodule MyApp.Formatter do
@callback format(term()) :: String.t()
defmacro __using__(opts) do
quote do
@behaviour MyApp.Formatter
@formatter_timeout Keyword.get(unquote(opts), :timeout, 5000)
def child_spec(_) do
%{id: __MODULE__, start: {__MODULE__, :start_link, []}}
end
def start_link do
GenServer.start_link(__MODULE__, [])
end
end
end
end
# A formatter doesn't need to be a process!
```
**Better alternative:**
```elixir
defmodule MyApp.Formatter do
@callback format(term()) :: String.t()
end
# Pure behaviour — implementors are just modules with a format/1 function
# No process needed for a synchronous data transformation
```
**Why:** Not everything needs to be a process. Adding GenServer semantics to a behaviour that does synchronous data transformation is over-engineering. Reserve process+behaviour combinations for things that genuinely need state, concurrency, or lifecycle management.
---
## 6. Callback Documentation Pattern
**Source:** [lib/phoenix/channel.ex#L350](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L350) (join callback doc)
```elixir
@doc """
Handle channel joins by `topic`.
...
## Example
def join("room:lobby", payload, socket) do
if authorized?(payload) do
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
"""
@callback join(topic :: binary, payload :: payload, socket :: Socket.t()) ::
{:ok, Socket.t()}
| {:ok, reply :: payload, Socket.t()}
| {:error, reason :: map}
```
**Why:** Every callback gets:
1. A `@doc` explaining when it's called and what it should do
2. A concrete example
3. The full type spec with all valid returns
This trio (doc + example + spec) gives implementors everything they need.
**Anti-pattern:** Defining callbacks without documentation — implementors have to read source code to understand when callbacks fire.
### When to Use
**Triggers:**
- You're defining a behaviour that users must implement
- Each callback has non-obvious semantics (when it fires, what params mean, what returns cause)
- The behaviour is public and will be implemented by people who didn't write it
**Example — before:**
```elixir
@callback on_connect(params :: map(), state :: term()) :: {:ok, term()} | {:error, term()}
```
**Example — after:**
```elixir
@doc """
Called when a client establishes a new connection.
`params` contains the query parameters from the connection URL.
`state` is initialized to the value returned by the transport's init.
Return `{:ok, state}` to accept the connection with updated state.
Return `{:error, reason}` to reject — `reason` is sent to the client
as the close frame payload.
## Example
def on_connect(%{"token" => token}, state) do
case verify_token(token) do
{:ok, user_id} -> {:ok, Map.put(state, :user_id, user_id)}
:error -> {:error, :unauthorized}
end
end
"""
@callback on_connect(params :: map(), state :: term()) ::
{:ok, state :: term()}
| {:error, reason :: term()}
```
### When NOT to Use
**Don't use this when:**
- The callback is internal and only your own code implements it
- The callback name and spec are completely self-explanatory (`@callback format(String.t()) :: String.t()`)
- You're writing a one-off behaviour for test mocking — extensive docs are wasted effort
**Over-application example:**
```elixir
@doc """
Converts the value to a string.
## Parameters
- `value` — the value to convert (term)
## Returns
- `String.t()` — the converted string
## Examples
def to_string(123), do: "123"
def to_string(:hello), do: "hello"
## Notes
This callback is required.
"""
@callback to_string(value :: term()) :: String.t()
```
**Better alternative:**
```elixir
@doc "Converts `value` to its string representation for display."
@callback to_string(value :: term()) :: String.t()
```
**Why:** Documentation depth should match callback complexity. A single-purpose callback with one obvious return type needs one sentence, not a full reference page. Save detailed docs for callbacks with multiple return shapes and non-obvious triggering conditions.
---
## 7. Phoenix.Endpoint: Behaviour as Interface Contract
**Source:** [lib/phoenix/endpoint.ex#L408](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/endpoint.ex#L408)
```elixir
defmacro __using__(opts) do
quote do
@behaviour Phoenix.Endpoint
unquote(config(opts))
unquote(pubsub())
unquote(plug())
unquote(server())
end
end
```
**Why:** The Endpoint uses `@behaviour` to define what an endpoint MUST provide (like `config/2`), then `__using__` generates the common implementation. The behaviour is the interface; the macro provides the default implementation.
**Anti-pattern:** Using only a behaviour without a `use` macro when significant boilerplate is required — forces every implementor to write the same code.
### When to Use
**Triggers:**
- Your behaviour defines a contract AND requires significant generated code
- The "interface" is simple but the implementation wiring is complex (plugs, routing, supervision)
- Users of the behaviour shouldn't need to understand the plumbing — just implement callbacks
**Example — before:**
```elixir
# User has to wire everything manually
defmodule MyEndpoint do
@behaviour Phoenix.Endpoint
use Plug.Builder
# ... 50 lines of boilerplate
# Easy to get wrong
end
```
**Example — after:**
```elixir
defmodule MyEndpoint do
use Phoenix.Endpoint, otp_app: :my_app
# All wiring generated — just configure and add plugs
plug MyAppWeb.Router
end
```
### When NOT to Use
**Don't use this when:**
- The generated code is minimal (just `@behaviour` — see pattern #3)
- The magic is hard to debug when things go wrong (transparency > convenience)
- Users need to understand what's generated to use the module correctly
**Over-application example:**
```elixir
defmodule MyApp.Validator do
defmacro __using__(_opts) do
quote do
@behaviour MyApp.Validator
import MyApp.Validator.DSL
Module.register_attribute(__MODULE__, :validations, accumulate: true)
@before_compile MyApp.Validator
# 40 lines of generated code for "validation framework"
# Users need a PhD in macros to debug validation errors
end
end
end
```
**Better alternative:**
```elixir
defmodule MyApp.Validator do
@callback validate(term()) :: :ok | {:error, [String.t()]}
end
# Simple behaviour — implementors write plain Elixir
defmodule UserValidator do
@behaviour MyApp.Validator
def validate(%{name: name}) when byte_size(name) > 0, do: :ok
def validate(_), do: {:error, ["name is required"]}
end
```
**Why:** The more code a `use` macro generates, the harder it is to debug. If users regularly need to read the generated code to understand failures, the abstraction is leaking. Reserve heavy `use` macros for well-established patterns (GenServer, Endpoint, Channel) where the community has internalized the mental model.
<!-- PATTERN_COMPLETE -->
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1108
View File
File diff suppressed because it is too large Load Diff
-593
View File
@@ -1,593 +0,0 @@
# Module Organization Patterns
How modules are structured, named, and organized in Elixir core and Phoenix.
## 1. One Module per Concept, Nested for Sub-Concepts
**Source:** `lib/elixir/lib/` directory structure
```
gen_server.ex — GenServer (the main module)
task.ex — Task (the main module)
task/supervised.ex — Task.Supervised (internal implementation)
supervisor.ex — Supervisor (the main module)
```
**Source:** `lib/phoenix/` directory structure
```
channel.ex — Phoenix.Channel
channel/ — Phoenix.Channel.* submodules
server.ex — Phoenix.Channel.Server
router.ex — Phoenix.Router
router/ — Phoenix.Router.* submodules
route.ex — Phoenix.Router.Route
scope.ex — Phoenix.Router.Scope
resource.ex — Phoenix.Router.Resource
endpoint.ex — Phoenix.Endpoint
endpoint/ — Phoenix.Endpoint.* submodules
supervisor.ex — Phoenix.Endpoint.Supervisor
```
**Why:** The parent module is the public API. Submodules handle implementation details. File layout mirrors module hierarchy — `Phoenix.Router.Route` lives in `router/route.ex`.
**Anti-pattern:** Putting all modules in a flat directory, or nesting too deeply (more than 3 levels is usually a sign of over-engineering).
### When to Use
**Triggers:**
- Your module has grown beyond ~300 lines with distinct sub-responsibilities
- External code only needs the parent module but implementation is complex
- You find yourself prefixing private functions with a concept name (e.g., `scope_push`, `scope_pop`)
**Example — before:**
```elixir
# Everything crammed into one flat module
defmodule MyApp.Router do
# 800 lines mixing route compilation, scope tracking, and helper generation
def compile_route(...), do: # ...
def push_scope(...), do: # ...
def pop_scope(...), do: # ...
def generate_helper(...), do: # ...
end
```
**Example — after:**
```elixir
# Parent module is the public API
defmodule MyApp.Router do
# Public API delegates to focused submodules
def compile(routes), do: MyApp.Router.Compiler.compile(routes)
end
# Submodules handle implementation
defmodule MyApp.Router.Compiler do
@moduledoc false
# ...
end
defmodule MyApp.Router.Scope do
@moduledoc false
# ...
end
```
### When NOT to Use
**Don't use this when:**
- The module is small and cohesive (< 200 lines)
- Nesting would exceed 3 levels (`A.B.C.D` is usually too deep)
- The "submodule" has its own independent public API (make it a sibling instead)
**Over-application example:**
```elixir
# Over-nesting a simple utility
defmodule MyApp.Utils.String.Formatting.Case do
def upcase(s), do: String.upcase(s)
end
```
**Better alternative:**
```elixir
defmodule MyApp.StringUtils do
def upcase(s), do: String.upcase(s)
end
```
**Why:** Nesting should reflect genuine conceptual hierarchy. If you're creating submodules for 2-3 functions that don't have independent complexity, you're adding navigational overhead without architectural benefit.
---
## 2. Public API at the Top, Private Functions at the Bottom
**Source:** `lib/elixir/lib/agent.ex` (full module structure)
```elixir
defmodule Agent do
@moduledoc "..."
# Types
@type on_start :: ...
@type name :: ...
@type agent :: ...
@type state :: ...
# child_spec (for supervisors)
def child_spec(arg) do ... end
# __using__ macro
defmacro __using__(opts) do ... end
# Public API
def start_link(fun, options \\ []) do ... end
def start(fun, options \\ []) do ... end
def get(agent, fun, timeout \\ 5000) do ... end
def get_and_update(agent, fun, timeout \\ 5000) do ... end
def update(agent, fun, timeout \\ 5000) do ... end
def cast(agent, fun) do ... end
def stop(agent, reason \\ :normal, timeout \\ :infinity) do ... end
end
```
The order:
1. `@moduledoc`
2. Types
3. `child_spec` / `__using__`
4. `start_link` / `start` (lifecycle)
5. Public API functions (alphabetical or logical grouping)
6. `stop` (lifecycle end)
**Anti-pattern:** Mixing private helpers between public functions, or putting `start_link` at the bottom where supervisors have to hunt for it.
### When to Use
**Triggers:**
- You're writing a new module and need to decide function ordering
- A module has grown organically and functions are scattered randomly
- You're reviewing code and finding it hard to locate the public API
**Example — before:**
```elixir
defmodule UserService do
defp hash_password(pw), do: # ...
def create(attrs) do
# uses hash_password
end
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
defp validate(attrs), do: # ...
def get(id), do: # ...
end
```
**Example — after:**
```elixir
defmodule UserService do
# Lifecycle
def start_link(opts), do: GenServer.start_link(__MODULE__, opts)
# Public API
def create(attrs), do: # ...
def get(id), do: # ...
# Private helpers
defp validate(attrs), do: # ...
defp hash_password(pw), do: # ...
end
```
### When NOT to Use
**Don't use this when:**
- You have a tiny module (< 5 functions) where ordering doesn't matter much
- The module is a pure data module (just a struct + typespec)
- "Logical grouping" puts closely related public+private pairs together for readability
**Over-application example:**
```elixir
# Forcing start_link to the top in a module that isn't an OTP process
defmodule MyApp.Parser do
# This module has no lifecycle — don't force OTP ordering
def start_link(_), do: raise "not a process" # Just to match the pattern?
def parse(input), do: # ...
end
```
**Better alternative:**
```elixir
defmodule MyApp.Parser do
@moduledoc "Parses input format X into structs"
def parse(input), do: # ...
def parse!(input), do: # ...
defp tokenize(input), do: # ...
end
```
**Why:** The ordering convention exists to make OTP-aware modules predictable. For non-OTP modules, lead with the primary public function (the one callers reach for first) and let the rest follow logically.
---
## 3. `@moduledoc false` for Internal Modules
**Source:** [lib/phoenix/router/route.ex#L5](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/router/route.ex#L5)
```elixir
# This module defines the Route struct that is used
# throughout Phoenix's router. This struct is private
# as it contains internal routing information.
@moduledoc false
```
**Why:** Internal modules that exist for code organization but aren't part of the public API get `@moduledoc false`. They won't appear in generated documentation.
**Anti-pattern:** Documenting internal modules and confusing users about what's public API vs implementation detail.
### When to Use
**Triggers:**
- A module exists purely for internal code organization
- Users of your library should never call this module directly
- The module is a helper that could change or disappear between versions
**Example — before:**
```elixir
defmodule MyApp.Repo.QueryBuilder do
@moduledoc """
Builds Ecto queries for the Repo module.
"""
# Now appears in docs, users try to call it directly
end
```
**Example — after:**
```elixir
defmodule MyApp.Repo.QueryBuilder do
@moduledoc false
# Hidden from docs, clearly internal
end
```
### When NOT to Use
**Don't use this when:**
- The module is part of your public API (even if rarely used)
- Users need to implement callbacks or extend the module
- The module defines a behaviour or protocol that others implement
**Over-application example:**
```elixir
# Hiding a module that users actually need
defmodule MyApp.Errors do
@moduledoc false # But users need to pattern-match on these!
defmodule NotFound do
defexception [:message]
end
end
```
**Better alternative:**
```elixir
defmodule MyApp.Errors do
@moduledoc "Error types raised by MyApp operations."
defmodule NotFound do
@moduledoc "Raised when a resource cannot be found."
defexception [:message]
end
end
```
**Why:** `@moduledoc false` means "this is not for you." If users catch your exceptions or match on your structs, they need documentation. Hide implementation details, not public contracts.
---
## 4. Struct Definition Conventions
**Source:** [lib/elixir/lib/task.ex#L279](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/task.ex#L279)
```elixir
@enforce_keys [:mfa, :owner, :pid, :ref]
defstruct @enforce_keys
@type t :: %__MODULE__{
mfa: mfa(),
owner: pid(),
pid: pid() | nil,
ref: ref()
}
```
**Source:** [lib/phoenix/router/route.ex#L30](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/router/route.ex#L30)
```elixir
defstruct [
:verb,
:line,
:kind,
:path,
:hosts,
:plug,
:plug_opts,
:helper,
:private,
:pipe_through,
:assigns,
:metadata,
:trailing_slash?,
:warn_on_verify?
]
@type t :: %Route{}
```
Two patterns:
1. **Task:** `@enforce_keys` + minimal struct — all fields required, enforced at creation
2. **Route:** All optional fields listed — flexible construction
**Why:** Use `@enforce_keys` when a struct is meaningless without certain fields. Omit it for structs built incrementally.
**Anti-pattern:** Never using `@enforce_keys` — allows creating invalid structs that crash later when a required field is `nil`.
### When to Use
**Triggers:**
- Your struct has fields that make no sense as `nil` (creating one without them is a bug)
- You're modeling a value object where all fields define its identity
- Incomplete structs would cause confusing runtime errors later
**Example — before:**
```elixir
defmodule Order do
defstruct [:id, :customer_id, :items, :total]
# Can create %Order{} with everything nil — meaningless
end
```
**Example — after:**
```elixir
defmodule Order do
@enforce_keys [:customer_id, :items, :total]
defstruct [:id | @enforce_keys]
# %Order{} without required fields -> immediate compile/runtime error
end
```
### When NOT to Use
**Don't use this when:**
- The struct is built incrementally (e.g., a changeset or builder pattern)
- Most fields have sensible defaults
- The struct represents configuration where partial specs are valid
**Over-application example:**
```elixir
# Enforcing keys on a struct that's built in stages
defmodule FormState do
@enforce_keys [:step, :name, :email, :address, :payment]
defstruct @enforce_keys
# Can't create a partial form state for step 1!
end
```
**Better alternative:**
```elixir
defmodule FormState do
defstruct step: 1, name: nil, email: nil, address: nil, payment: nil
# Built incrementally as user progresses through steps
end
```
**Why:** `@enforce_keys` is for structs that represent *complete* values. If your struct represents an evolving state or has legitimate intermediate forms, enforcing all keys makes construction impossible at early stages.
---
## 5. Selective Imports in `__using__`
**Source:** [lib/phoenix/channel.ex#L463](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/channel.ex#L463)
```elixir
import unquote(__MODULE__)
import Phoenix.Socket, only: [assign: 3, assign: 2]
```
**Source:** [lib/phoenix/router.ex#L271](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/router.ex#L271)
```elixir
import Phoenix.Router
import Plug.Conn
import Phoenix.Controller
```
**Why:** The `use` macro sets up the module's environment — importing the functions you'll need. Phoenix.Channel imports `assign` from Socket because channels work with sockets constantly.
**Anti-pattern:** Importing everything without restriction — namespace pollution and hard-to-trace function origins.
### When to Use
**Triggers:**
- Your `use` macro needs to give the caller access to specific functions
- You want to control exactly which functions enter the caller's namespace
- The imported functions are central to the DSL or workflow the module enables
**Example — before:**
```elixir
defmacro __using__(_opts) do
quote do
# Imports EVERYTHING from three modules — namespace soup
import MyApp.Router.Helpers
import MyApp.Router.Scoping
import MyApp.Router.Compilation
end
end
```
**Example — after:**
```elixir
defmacro __using__(_opts) do
quote do
import MyApp.Router, only: [get: 2, post: 2, resources: 2, scope: 2]
import MyApp.Conn, only: [assign: 3, put_status: 2]
end
end
```
### When NOT to Use
**Don't use this when:**
- The caller could just `import` what they need themselves
- You're importing utility functions that aren't part of your module's "DSL"
- The imports create naming conflicts with common functions
**Over-application example:**
```elixir
defmacro __using__(_opts) do
quote do
import MyApp.Utils # 50+ utility functions dumped into caller
import Enum # Why? Caller can do this themselves
import Map # Polluting namespace with standard lib
end
end
```
**Better alternative:**
```elixir
defmacro __using__(_opts) do
quote do
# Only import what THIS module's workflow requires
import MyApp.DSL, only: [field: 2, validate: 1]
end
end
```
**Why:** `use` should import the *minimum* needed for the module's intended workflow. If you're importing generic utilities, you're making decisions for the caller that they should make themselves.
---
## 6. Alias at Module Scope for Readability
**Source:** [lib/phoenix/router.ex#L268](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/router.ex#L268)
```elixir
alias Phoenix.Router.{Resource, Scope, Route, Helpers}
```
**Why:** Multi-alias reduces repetition and groups related modules. The curly-brace syntax makes it clear these all share a parent namespace.
**Anti-pattern:** Using full module paths everywhere (`Phoenix.Router.Resource.new(...)`) — verbose and hard to read.
### When to Use
**Triggers:**
- Multiple modules from the same parent namespace are used together
- Full module paths are making code hard to read
- The aliased modules are used frequently (3+ times in the file)
**Example — before:**
```elixir
def process(input) do
Phoenix.Router.Route.new(input)
|> Phoenix.Router.Scope.apply_scope(Phoenix.Router.Scope.current())
|> Phoenix.Router.Helpers.generate()
end
```
**Example — after:**
```elixir
alias Phoenix.Router.{Route, Scope, Helpers}
def process(input) do
Route.new(input)
|> Scope.apply_scope(Scope.current())
|> Helpers.generate()
end
```
### When NOT to Use
**Don't use this when:**
- A module is referenced only once (inline the full path)
- The alias would be ambiguous (two `Route` modules from different namespaces)
- You're in a test file and the full path makes assertions clearer
**Over-application example:**
```elixir
# Aliasing a module used exactly once
alias MyApp.Workers.BatchProcessor
def run do
BatchProcessor.start() # Only reference — alias adds noise
end
```
**Better alternative:**
```elixir
def run do
MyApp.Workers.BatchProcessor.start() # One use — full path is fine
end
```
**Why:** Aliases trade verbosity for indirection. When a module appears once, the full path is documentation. When it appears many times, the alias is readability. Find the crossover point (typically 2-3 uses).
---
## 7. Boolean-Suffixed Fields in Structs
**Source:** [lib/phoenix/router/route.ex#L43](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/phoenix/router/route.ex#L43)
```elixir
:trailing_slash?,
:warn_on_verify?
```
**Why:** The `?` suffix on struct fields mirrors the Elixir convention for boolean-returning functions. It makes the field's type obvious without checking the typespec.
**Anti-pattern:** Using bare names like `:trailing_slash` or `:has_trailing_slash` — the `?` convention is more idiomatic and self-documenting.
### When to Use
**Triggers:**
- A struct field stores a boolean value
- The field answers a yes/no question about the struct
- You want the field's type to be self-evident without checking typespecs
**Example — before:**
```elixir
defstruct [:path, :trailing_slash, :verified]
# Is :trailing_slash the slash character? A boolean? The position?
```
**Example — after:**
```elixir
defstruct [:path, :trailing_slash?, :verified?]
# Immediately clear these are booleans
```
### When NOT to Use
**Don't use this when:**
- The field isn't a boolean (e.g., `:status` that can be `:active`/`:inactive`)
- You're working with external serialization that can't handle `?` in keys
- The field represents a count, enum, or value rather than a yes/no question
**Over-application example:**
```elixir
defstruct [:user?, :admin?, :count?]
# :user? — is this "is user present?" or "the user value"?
# :count? — definitely not a boolean
```
**Better alternative:**
```elixir
defstruct [:user, :admin?, :count]
# :user is the user struct, :admin? is a boolean, :count is an integer
```
**Why:** The `?` suffix should only mark genuine booleans. Using it on non-boolean fields creates confusion about the field's type and breaks the convention's usefulness as a type signal.
<!-- PATTERN_COMPLETE -->
File diff suppressed because it is too large Load Diff
-1728
View File
File diff suppressed because it is too large Load Diff
-801
View File
@@ -1,801 +0,0 @@
# Typespecs Patterns
Patterns extracted from the Elixir standard library source code.
---
## 1. Public Type with @typedoc
**Source:** [lib/elixir/lib/gen_server.ex#L862](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L862)
**What it does:** Every public `@type` is preceded by a `@typedoc` that explains what the type represents, often referencing which functions use it.
**Why:** Types are part of the public API. Without documentation, users must guess what a type means from its definition alone. The `@typedoc` bridges intent and implementation.
**Anti-pattern:** Defining `@type` without any `@typedoc`, leaving users to decipher complex union types.
**Code example:**
```elixir
@typedoc "Return values of `start*` functions"
@type on_start :: {:ok, pid} | :ignore | {:error, {:already_started, pid} | term}
@typedoc "The GenServer name"
@type name :: nil | atom | {:global, term} | {:via, module, term}
@typedoc """
The server reference.
This is either a plain PID or a value representing a registered name.
...
"""
@type server :: pid | name | {atom, node}
@typedoc """
Tuple describing the client of a call request.
`pid` is the PID of the caller and `tag` is a unique term used to identify the
call.
"""
@type from :: {pid, tag :: term}
```
### When to Use
**Triggers:**
- You define a public `@type` that appears in any `@spec` or callback signature
- The type has more than one clause or is a tagged tuple
- Other modules will reference this type
**Example — before:**
```elixir
@type status :: :pending | :active | :suspended | :terminated
```
**Example — after:**
```elixir
@typedoc """
Account lifecycle status.
Used by `activate/1`, `suspend/1`, and `terminate/1` to indicate
the current state of a user account.
"""
@type status :: :pending | :active | :suspended | :terminated
```
### When NOT to Use
**Don't use this when:**
- The type is private (`@typep`) and only used internally within the module
- The type name is completely self-explanatory and has a single, obvious clause (e.g., `@type id :: pos_integer()`)
**Over-application example:**
```elixir
@typedoc "A boolean value"
@type enabled :: boolean()
@typedoc "The name"
@type name :: String.t()
```
**Better alternative:**
```elixir
# Self-explanatory single-clause types don't need @typedoc
@type enabled :: boolean()
@type name :: String.t()
```
**Why:** Adding a `@typedoc` that merely restates the type name and definition adds noise without information. Reserve `@typedoc` for types where the name alone doesn't convey the full meaning.
---
## 2. Private Types with @typep
**Source:** [lib/elixir/lib/macro.ex#L84](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/macro.ex#L84), 97
**What it does:** Uses `@typep` for internal recursive type definitions that are implementation details not meant for external consumers.
**Why:** Keeps the public type surface small while allowing internal type reuse. Private types can reference themselves recursively (e.g., for AST node structure) without polluting the documentation.
**Anti-pattern:** Making all types public "just in case." This bloats documentation and creates implicit public API guarantees.
**Code example:**
```elixir
@typedoc "The inputs of a macro"
@type input ::
input_expr
| {input, input}
| [input]
| atom
| number
| binary
@typep input_expr :: {input_expr | atom, metadata, atom | [input]}
@typedoc "The output of a macro"
@type output ::
output_expr
| {output, output}
| [output]
| atom
| number
| binary
| captured_remote_function
| pid
@typep output_expr :: {output_expr | atom, metadata, atom | [output]}
```
### When to Use
**Triggers:**
- A type is used only within the defining module (helper type for recursion, intermediate structure)
- You want to DRY up repeated type expressions inside the module without exposing them
- The type represents an internal data structure that could change without notice
**Example — before:**
```elixir
# Repeated inline in multiple specs
@spec parse(String.t()) :: {atom | {atom, list, list}, map(), atom | list}
@spec transform({atom | {atom, list, list}, map(), atom | list}) :: String.t()
```
**Example — after:**
```elixir
@typep node :: {atom | {atom, list, node}, map(), atom | [node]}
@spec parse(String.t()) :: node()
@spec transform(node()) :: String.t()
```
### When NOT to Use
**Don't use this when:**
- Other modules need to reference the type in their own specs
- The type is part of a public struct or protocol contract
- You want Dialyzer to catch misuse across module boundaries (opaque types are better for this)
**Over-application example:**
```elixir
defmodule Parser do
@typep ast :: {atom, keyword(), [ast]}
# But then in another module...
end
defmodule Transformer do
# Can't reference Parser.ast() — it's private!
@spec transform(term()) :: term() # forced to use term()
def transform(ast), do: ...
end
```
**Better alternative:**
```elixir
defmodule Parser do
@type ast :: {atom, keyword(), [ast]}
# OR use @opaque if you want to hide the structure but allow cross-module reference
end
defmodule Transformer do
@spec transform(Parser.ast()) :: Parser.ast()
def transform(ast), do: ...
end
```
**Why:** `@typep` hides the type from other modules entirely. If cross-module usage exists, you need `@type` (public) or `@opaque` (hidden structure, public name).
---
## 3. @opaque Types (Protocol t())
**Source:** [lib/elixir/lib/protocol.ex#L150](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/protocol.ex#L150) (documentation), runtime generation
**What it does:** Protocols auto-generate an opaque `@type t :: term()` type that represents "any value implementing this protocol."
**Why:** The type is opaque because the concrete implementations are open-ended — any module can implement the protocol. Making it opaque prevents users from pattern-matching on the internal representation (which could be anything) and signals that you should only use the protocol functions.
**Anti-pattern:** Defining `@opaque` for types where consumers *need* to destructure the value. Use `@opaque` only when the internal representation is truly hidden.
**Code example:**
```elixir
# Generated automatically by defprotocol — documented behavior:
defprotocol Size do
@doc "Calculates the size (and not the length!) of a data structure"
def size(data)
end
# Usage in specs:
@spec print_size(Size.t()) :: :ok
def print_size(data) do
# ...
end
```
### When to Use
**Triggers:**
- You build a data structure whose internals should never be accessed directly (e.g., wrapper around ETS, NIF resource, or complex nested map)
- You provide a complete set of accessor/manipulation functions and want to enforce using them
- You define a protocol where the implementing type is unknowable at definition time
**Example — before:**
```elixir
@type t :: %__MODULE__{items: list(), size: non_neg_integer()}
# Users can (and will) do: queue.items |> Enum.reverse()
```
**Example — after:**
```elixir
@opaque t :: %__MODULE__{items: list(), size: non_neg_integer()}
# Dialyzer warns if users access .items directly
# They must use Queue.push/2, Queue.pop/1, Queue.size/1
```
### When NOT to Use
**Don't use this when:**
- Users legitimately need to pattern-match or destructure the value (e.g., tagged tuples like `{:ok, value}`)
- The struct fields are part of the documented public API
- The type is simple enough that hiding it adds ceremony without protection
**Over-application example:**
```elixir
defmodule Config do
@opaque t :: %__MODULE__{timeout: pos_integer(), retries: non_neg_integer()}
defstruct [:timeout, :retries]
# Now users can't do: config.timeout — Dialyzer complains
# You're forced to write getters for every field
def timeout(%__MODULE__{timeout: t}), do: t
def retries(%__MODULE__{retries: r}), do: r
end
```
**Better alternative:**
```elixir
defmodule Config do
@type t :: %__MODULE__{timeout: pos_integer(), retries: non_neg_integer()}
defstruct [:timeout, :retries]
# Users access fields directly — it's just config, not a black box
end
```
**Why:** `@opaque` is a contract that says "you may not look inside." If users need direct field access and the struct layout is stable, a regular `@type` is the right choice.
---
## 4. Union Types in @spec Return Values
**Source:** [lib/elixir/lib/gen_server.ex#L577](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L577), 647658
**What it does:** Uses union types with descriptive tagged tuples in callback specs, making all possible return shapes explicit.
**Why:** OTP callbacks accept multiple return shapes (e.g., `{:reply, ...}`, `{:noreply, ...}`, `{:stop, ...}`). Spelling out every variant in the spec enables Dialyzer and makes the contract self-documenting.
**Anti-pattern:** Using `term()` as a catch-all return type when specific shapes are known. This defeats the purpose of typespecs.
**Code example:**
```elixir
@callback init(init_arg :: term) ::
{:ok, state}
| {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}}
| :ignore
| {:stop, reason :: term}
when state: term
@callback handle_call(request :: term, from, state :: term) ::
{:reply, reply, new_state}
| {:reply, reply, new_state,
timeout | :hibernate | {:continue, continue_arg :: term}}
| {:noreply, new_state}
| {:noreply, new_state, timeout | :hibernate | {:continue, continue_arg :: term}}
| {:stop, reason, reply, new_state}
| {:stop, reason, new_state}
when reply: term, new_state: term, reason: term
```
### When to Use
**Triggers:**
- A function can return multiple distinct shapes (tagged tuples, atoms, or different structures)
- You're defining a behaviour callback where implementers need to know all valid returns
- The function has error cases that should be visible in the type signature
**Example — before:**
```elixir
@spec fetch(key :: String.t()) :: term()
```
**Example — after:**
```elixir
@spec fetch(key :: String.t()) ::
{:ok, value :: term()}
| {:error, :not_found}
| {:error, :expired, expired_at :: DateTime.t()}
```
### When NOT to Use
**Don't use this when:**
- The function truly returns an unbounded set of types (e.g., a deserializer that can return any Elixir term)
- You're listing so many variants that the spec becomes harder to read than the function body itself
- The union has more than ~6-8 clauses — consider defining a named `@type` instead
**Over-application example:**
```elixir
@spec process(input()) ::
{:ok, result()}
| {:error, :invalid_input}
| {:error, :timeout}
| {:error, :network_error}
| {:error, :parse_error}
| {:error, :rate_limited}
| {:error, :unauthorized}
| {:error, :not_found}
| {:error, :server_error}
| {:error, :unknown}
```
**Better alternative:**
```elixir
@type error_reason ::
:invalid_input | :timeout | :network_error | :parse_error
| :rate_limited | :unauthorized | :not_found | :server_error | :unknown
@spec process(input()) :: {:ok, result()} | {:error, error_reason()}
```
**Why:** Extract large unions into named types. The spec stays readable, the error taxonomy is reusable, and you can add `@typedoc` to explain each reason.
---
## 5. `when` Constraints in Specs
**Source:** [lib/elixir/lib/kernel.ex#L635](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/kernel.ex#L635), 1072, 1455
**What it does:** Uses the `when` clause in `@spec` to bind type variables, expressing relationships between parameters and return values.
**Why:** When the same type variable appears in multiple positions, it communicates that those values are related (same type). This is critical for generic functions like `hd/1`, `max/2`.
**Anti-pattern:** Using unrelated type names in different argument positions when the same variable would express the contract more precisely.
**Code example:**
```elixir
@spec hd(nonempty_maybe_improper_list(elem, term)) :: elem when elem: term
@spec max(first, second) :: first | second when first: term, second: term
@spec tl(nonempty_maybe_improper_list(elem, last)) :: maybe_improper_list(elem, last) | last
when elem: term, last: term
```
### When to Use
**Triggers:**
- A function's return type depends on its input type (generic/polymorphic functions)
- The same value flows through unchanged and you want to express that
- Multiple parameters must share a type constraint
**Example — before:**
```elixir
@spec get_or_default(map(), atom(), term()) :: term()
```
**Example — after:**
```elixir
@spec get_or_default(map(), atom(), default) :: term() | default when default: term()
```
### When NOT to Use
**Don't use this when:**
- The relationship between input and output types is not actually enforced at runtime
- You have a single type variable that only appears once (it adds syntax without expressing a relationship)
- The constraint is always `when x: term` with no actual narrowing — it's just noise
**Over-application example:**
```elixir
@spec log(message) :: :ok when message: String.t()
```
**Better alternative:**
```elixir
@spec log(String.t()) :: :ok
```
**Why:** A `when` clause with a single variable that appears only once in the spec is equivalent to inlining the type. The `when` syntax only adds value when the same variable appears in multiple positions, expressing a type relationship.
---
## 6. Map Types with required/optional Keys
**Source:** [lib/elixir/lib/supervisor.ex#L602](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/supervisor.ex#L602), 644652
**What it does:** Uses map type syntax with `required()` and `optional()` keys to define struct-like specs where some fields have defaults.
**Why:** Supervisors have a child_spec map where `:id` and `:start` are mandatory but `:restart`, `:shutdown` etc. are optional with defaults. The type system reflects this precisely.
**Anti-pattern:** Using a plain `map()` type or making all keys required when some have sensible defaults.
**Code example:**
```elixir
@type sup_flags() :: %{
strategy: strategy(),
intensity: non_neg_integer(),
period: pos_integer(),
auto_shutdown: auto_shutdown()
}
@type child_spec :: %{
required(:id) => atom() | term(),
required(:start) => {module(), function_name :: atom(), args :: [term()]},
optional(:restart) => restart(),
optional(:shutdown) => shutdown(),
optional(:type) => type(),
optional(:modules) => [module()] | :dynamic,
optional(:significant) => boolean()
}
```
### When to Use
**Triggers:**
- You accept a map (not a struct) with a mix of mandatory and optional keys
- The function has a "config" or "opts" map parameter with defaults for some keys
- You're documenting a protocol/behaviour where implementers return option maps
**Example — before:**
```elixir
@spec connect(map()) :: {:ok, pid()}
```
**Example — after:**
```elixir
@type connect_opts :: %{
required(:host) => String.t(),
required(:port) => pos_integer(),
optional(:timeout) => pos_integer(),
optional(:ssl) => boolean(),
optional(:pool_size) => pos_integer()
}
@spec connect(connect_opts()) :: {:ok, pid()}
```
### When NOT to Use
**Don't use this when:**
- You're defining a struct — structs already enforce their fields via `defstruct` and `@enforce_keys`
- The map is truly open-ended (arbitrary keys) — use `%{optional(atom()) => term()}`
- A keyword list would be more idiomatic for the use case (most Elixir option APIs use keyword lists, not maps)
**Over-application example:**
```elixir
# Defining required/optional for a struct
@type t :: %__MODULE__{
required(:name) => String.t(),
optional(:email) => String.t()
}
```
**Better alternative:**
```elixir
# Structs use the standard struct type syntax
@type t :: %__MODULE__{
name: String.t(),
email: String.t() | nil
}
```
**Why:** Struct types in Elixir use `field: type` syntax, not `required/optional`. The `required()`/`optional()` syntax is for plain maps. Mixing them up confuses Dialyzer and readers.
---
## 7. Keyword List Types for Options
**Source:** [lib/logger/lib/logger.ex#L509](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/logger/lib/logger.ex#L509)
**What it does:** Defines option types as keyword lists with specific key-value constraints, sometimes nested.
**Why:** Many Elixir APIs accept keyword options. Typing them precisely documents valid options and their expected value types without forcing users to read function documentation.
**Anti-pattern:** Typing options as `keyword()` (untyped keyword list), which provides zero guidance.
**Code example:**
```elixir
@type configure_opts :: [
level: level(),
translator_inspect_opts: Inspect.Opts.t(),
sync_threshold: non_neg_integer(),
discard_threshold: non_neg_integer(),
truncate: non_neg_integer() | :infinity,
utc_log: boolean()
]
@type formatter_opts :: [
colors: [
enabled: boolean(),
debug: atom(),
info: atom(),
warning: atom(),
error: atom()
],
format: String.t() | {module(), atom()},
metadata: :all | [atom()],
truncate: pos_integer() | :infinity,
utc_log: boolean()
]
```
### When to Use
**Triggers:**
- A function accepts a keyword list of options (the `opts \\ []` pattern)
- You have more than 2-3 options and want to document valid keys and their types
- The options type is reused across multiple functions in the module
**Example — before:**
```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```
**Example — after:**
```elixir
@type start_opts :: [
name: GenServer.name(),
timeout: pos_integer(),
debug: [:trace | :log | :statistics]
]
@spec start_link(start_opts()) :: GenServer.on_start()
```
### When NOT to Use
**Don't use this when:**
- The function takes 1-2 simple options that are obvious from the function's `@doc`
- The options are passed through to another function unchanged (type the pass-through, not the intermediate)
- You're defining it but Dialyzer can't actually check keyword list shapes deeply — don't give false confidence
**Over-application example:**
```elixir
@type greet_opts :: [name: String.t()]
@spec greet(greet_opts()) :: String.t()
def greet(opts \\ []) do
"Hello, #{opts[:name] || "world"}"
end
```
**Better alternative:**
```elixir
@spec greet(name :: String.t()) :: String.t()
def greet(name \\ "world") do
"Hello, #{name}"
end
```
**Why:** A single optional value is better expressed as a default argument. Keyword list types shine when you have many options with different types. One option wrapped in a keyword list is over-engineering.
---
## 8. Parameterized Types (t/1)
**Source:** [lib/elixir/lib/enum.ex#L58](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/enum.ex#L58) (Enumerable protocol)
**What it does:** Defines a parameterized type `t(_element)` that allows expressing the element type of an enumerable in function specs.
**Why:** Enables downstream functions to express type flow: "takes an enumerable of integers, returns an enumerable of strings." The parameter communicates element type even though the protocol dispatch erases it at runtime.
**Anti-pattern:** Always using `t()` without parameters when the element type is known and useful for documentation.
**Code example:**
```elixir
@typedoc """
An enumerable of elements of type `element`.
This type is equivalent to `t:t/0` but is especially useful for documentation.
For example, imagine you define a function that expects an enumerable of
integers and returns an enumerable of strings:
@spec integers_to_strings(Enumerable.t(integer())) :: Enumerable.t(String.t())
def integers_to_strings(integers) do
Stream.map(integers, &Integer.to_string/1)
end
"""
@typedoc since: "1.14.0"
@type t(_element) :: t()
```
### When to Use
**Triggers:**
- You define a container/collection type and want specs to express what's inside
- Functions transform the element type (e.g., map, filter, convert)
- Your library exports a generic data structure (queue, tree, ring buffer)
**Example — before:**
```elixir
@type queue :: %__MODULE__{items: list()}
@spec push(queue(), term()) :: queue()
@spec pop(queue()) :: {term(), queue()}
```
**Example — after:**
```elixir
@type t(element) :: %__MODULE__{items: [element]}
@spec push(t(element), element) :: t(element) when element: term()
@spec pop(t(element)) :: {element, t(element)} when element: term()
```
### When NOT to Use
**Don't use this when:**
- The container always holds a fixed type (e.g., a `TokenBucket` that only holds integers — just use `integer()` directly)
- Dialyzer can't actually track the parameter through your implementation (it mostly can't for complex cases) — the value is documentation-only
- The parameterization would require more than one type variable and the spec becomes unreadable
**Over-application example:**
```elixir
@type config(value_type) :: %{key: atom(), value: value_type}
# But it's always used with String.t() in practice:
@spec get_config() :: config(String.t())
@spec set_config(config(String.t())) :: :ok
```
**Better alternative:**
```elixir
@type config :: %{key: atom(), value: String.t()}
@spec get_config() :: config()
@spec set_config(config()) :: :ok
```
**Why:** Parameterized types add value when the parameter actually varies across usage sites. If every call site uses the same concrete type, the parameter is abstraction without benefit.
---
## 9. Named Parameters in Specs (:: annotation)
**Source:** [lib/elixir/lib/gen_server.ex#L577](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/gen_server.ex#L577), [lib/elixir/lib/supervisor.ex#L562](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/supervisor.ex#L562)
**What it does:** Uses `name :: type` syntax in callback/spec parameter positions to give meaningful names to parameters.
**Why:** `init(init_arg :: term)` is vastly more readable than `init(term)`. The name serves as inline documentation within the spec itself.
**Anti-pattern:** Writing specs with only types and no parameter names, especially for callbacks that users must implement.
**Code example:**
```elixir
@callback init(init_arg :: term) ::
{:ok, state}
| {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}}
| :ignore
| {:stop, reason :: term}
when state: term
@callback terminate(reason, state :: term) :: term
when reason: :normal | :shutdown | {:shutdown, term} | term
```
### When to Use
**Triggers:**
- A parameter's type alone doesn't convey its purpose (e.g., `term()`, `String.t()`, `integer()`)
- You're defining a callback that other developers must implement
- The function has multiple parameters of the same type
**Example — before:**
```elixir
@spec transfer(String.t(), String.t(), pos_integer()) :: {:ok, reference()} | {:error, term()}
```
**Example — after:**
```elixir
@spec transfer(from_account :: String.t(), to_account :: String.t(), amount :: pos_integer()) ::
{:ok, reference()} | {:error, term()}
```
### When NOT to Use
**Don't use this when:**
- The type itself is descriptive enough (e.g., `pid()`, `module()`, `boolean()`)
- The function has a single parameter and the function name makes it obvious
- The name would just repeat the type (e.g., `string :: String.t()`)
**Over-application example:**
```elixir
@spec alive?(pid :: pid()) :: boolean()
@spec start(module :: module()) :: {:ok, pid()}
```
**Better alternative:**
```elixir
@spec alive?(pid()) :: boolean()
@spec start(module()) :: {:ok, pid()}
```
**Why:** `pid()` and `module()` are self-documenting types. Adding `pid :: pid()` is like writing `# increments x` above `x += 1`. The annotation should add information the type alone doesn't convey.
---
## 10. @typedoc since: Annotation
**Source:** [lib/elixir/lib/supervisor.ex#L669](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/supervisor.ex#L669), [lib/elixir/lib/enum.ex#L72](https://github.com/elixir-lang/elixir/blob/f4e1b34617ef92052b65781f18eae5b88a490098/lib/elixir/lib/enum.ex#L72)
**What it does:** Attaches a `since:` metadata annotation to `@typedoc` indicating when a type was introduced.
**Why:** Helps library consumers know version requirements. The pattern mirrors `@doc since:` used on functions.
**Anti-pattern:** Omitting `since:` on types added after the initial release, leaving users guessing about compatibility.
**Code example:**
```elixir
@typedoc since: "1.16.0"
@type module_spec :: {module(), args :: term()} | module()
@typedoc since: "1.14.0"
@type t(_element) :: t()
```
### When to Use
**Triggers:**
- You're adding a new public type to an existing library (post-1.0 release)
- Your library follows semantic versioning and users need to know minimum version requirements
- You're maintaining a changelog and want types to be traceable to releases
**Example — before:**
```elixir
@typedoc "A validated email address"
@type email :: String.t()
```
**Example — after:**
```elixir
@typedoc since: "2.3.0"
@typedoc "A validated email address"
@type email :: String.t()
```
### When NOT to Use
**Don't use this when:**
- The type has existed since the library's initial release (no version ambiguity)
- You're in an application (not a library) where version tracking of types is meaningless
- The library doesn't follow semver or publish versioned docs
**Over-application example:**
```elixir
# In a Phoenix app's context module
defmodule MyApp.Accounts do
@typedoc since: "0.1.0"
@type user_id :: pos_integer()
end
```
**Better alternative:**
```elixir
defmodule MyApp.Accounts do
@type user_id :: pos_integer()
end
```
**Why:** `since:` annotations are for library consumers checking compatibility across versions. Application code doesn't have "consumers" checking which version introduced a type — it's all deployed together.
<!-- PATTERN_COMPLETE -->
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff