Function Clauses and Guards
Use multiple function heads to make branching explicit. Guards keep each clause narrow and readable.
defmodule AccessLevel do
def label(%{role: :admin}), do: "full access"
def label(%{role: :editor, active: true}), do: "edit access"
def label(%{role: role}) when role in [:viewer, :guest], do: "read only"
def label(_), do: "no access"
end
This keeps branching close to the function definition, which makes Elixir code easier to scan than one large conditional.
Pipelines
The pipe operator passes the previous result as the first argument to the next function.
users
|> Enum.filter(& &1.active)
|> Enum.map(& &1.email)
|> Enum.sort()
Prefer pipes when each step transforms the same value. Avoid hiding complex branching inside a long pipeline.
This reads top to bottom like a data flow. Each step takes the previous result and transforms it a little more.
Structs
Structs are maps with a fixed shape and a module name. They are useful for domain data.
defmodule User do
defstruct [:id, :email, active: true]
end
user = %User{id: 1, email: "[email protected]"}
case user do
%User{active: true, email: email} -> {:ok, email}
%User{} -> {:error, :inactive}
end
This shows a struct as a predictable map shape for domain data, plus pattern matching on that struct in a case.
With
with flattens a sequence of pattern matches that may fail.
with {:ok, user} <- fetch_user(id),
{:ok, plan} <- fetch_plan(user),
true <- plan.active do
{:ok, %{user: user, plan: plan}}
else
false -> {:error, :inactive_plan}
{:error, reason} -> {:error, reason}
end
Use it when each successful step feeds the next one.
This is useful when several steps can fail and each successful result feeds the next one.
Processes and Messages
Elixir processes are lightweight and communicate by sending messages.
parent = self()
spawn(fn ->
send(parent, {:result, 42})
end)
receive do
{:result, value} -> value
after
1_000 -> {:error, :timeout}
end
This demonstrates Elixir’s concurrency model: lightweight processes send messages instead of sharing mutable state.
GenServer Shape
A GenServer keeps state behind a process and exposes a client API.
defmodule Counter do
use GenServer
def start_link(initial), do: GenServer.start_link(__MODULE__, initial)
def increment(pid), do: GenServer.call(pid, :increment)
@impl true
def init(initial), do: {:ok, initial}
@impl true
def handle_call(:increment, _from, count) do
next = count + 1
{:reply, next, next}
end
end
This is the usual GenServer layout: public API at the top, callbacks below, and state kept inside the process.