12 pattern/smell files covering GenServer, process design, data transforms, error handling, testing, typespecs, documentation, behaviours, macros, modules, anti-patterns, and common mistakes. All patterns cite specific Elixir source files and line numbers.
6.0 KiB
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:577-812 (all callback definitions)
@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.
2. @optional_callbacks for Extensibility
Source: lib/phoenix/channel.ex:442-448
@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.
3. @behaviour Declaration in __using__
Source: lib/phoenix/channel.ex:450-453
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:836
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.
4. Default Implementations via defoverridable
Source: lib/elixir/lib/gen_server.ex:849
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.
5. Phoenix Channel: Behaviour + Process + Protocol
Source: lib/phoenix/channel.ex:364-448 (full callback set)
The Channel behaviour combines:
- Required callback:
join/3(authorization gate) - Optional callbacks:
handle_in/3,handle_info/2, etc. (event handlers) - Process semantics: Each channel is a GenServer (line 476-479)
- Configuration via module attributes:
@phoenix_log_join,@phoenix_hibernate_after
# 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.
6. Callback Documentation Pattern
Source: lib/phoenix/channel.ex:350-363 (join callback doc)
@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:
- A
@docexplaining when it's called and what it should do - A concrete example
- 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.
7. Phoenix.Endpoint: Behaviour as Interface Contract
Source: lib/phoenix/endpoint.ex:408
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.