Elixir Patterns and Conventions
Pattern Matching
Pattern matching is the primary control flow mechanism in Elixir. Prefer it over conditional statements.
Prefer Pattern Matching Over if/else
Bad:
def process(result) do if result.status == :ok do result.data else nil end end
Good:
def process(%{status: :ok, data: data}), do: data def process(_), do: nil
Use Case for Multiple Patterns
Bad:
def handle_response(response) do if response.status == 200 do {:ok, response.body} else if response.status == 404 do {:error, :not_found} else {:error, :unknown} end end
Good:
def handle_response(%{status: 200, body: body}), do: {:ok, body} def handle_response(%{status: 404}), do: {:error, :not_found} def handle_response(_), do: {:error, :unknown}
Pipe Operator
Use the pipe operator |> to chain function calls for improved readability.
Basic Piping
Bad:
String.upcase(String.trim(user_input))
Good:
user_input |> String.trim() |> String.upcase()
Pipe into Function Heads
Bad:
def process_user(user) do validated = validate_user(user) transformed = transform_user(validated) save_user(transformed) end
Good:
def process_user(user) do user |> validate_user() |> transform_user() |> save_user() end
With Statement
Use with for sequential operations that can fail.
Bad:
def create_post(params) do case validate_params(params) do {:ok, valid_params} -> case create_changeset(valid_params) do {:ok, changeset} -> Repo.insert(changeset) error -> error end error -> error end end
Good:
def create_post(params) do with {:ok, valid_params} <- validate_params(params), {:ok, changeset} <- create_changeset(valid_params), {:ok, post} <- Repo.insert(changeset) do {:ok, post} end end
Immutability
All data structures are immutable. Functions return new values rather than modifying in place.
Always returns a new list
list = [1, 2, 3] new_list = [0 | list] # [0, 1, 2, 3]
list is still [1, 2, 3]
Guards
Use guards for simple type and value checks in function heads.
def calculate(x) when is_integer(x) and x > 0 do x * 2 end
def calculate(_), do: {:error, :invalid_input}
Anonymous Functions
Use the capture operator & for concise anonymous functions.
Verbose:
Enum.map(list, fn x -> x * 2 end)
Concise:
Enum.map(list, &(&1 * 2))
Named function capture:
Enum.map(users, &User.format/1)
List Comprehensions
Use for comprehensions for complex transformations and filtering.
Bad (multiple passes):
list |> Enum.map(&transform/1) |> Enum.filter(&valid?/1) |> Enum.map(&format/1)
Good (single pass):
for item <- list, transformed = transform(item), valid?(transformed) do format(transformed) end
Naming Conventions
-
Module names: PascalCase
-
Function names: snake_case
-
Variables: snake_case
-
Atoms: :snake_case
-
Predicate functions end with ? : valid? , empty?
-
Dangerous functions end with ! : save! , update!
Boolean Checks
Functions returning booleans should end with ? .
def admin?(user), do: user.role == :admin def empty?(list), do: list == []
Error Tuples
Return {:ok, result} or {:error, reason} tuples for operations that can fail.
def fetch_user(id) do case Repo.get(User, id) do nil -> {:error, :not_found} user -> {:ok, user} end end
Documentation
Use @doc for public functions and @moduledoc for modules.
defmodule MyModule do @moduledoc """ This module handles user operations. """
@doc """ Fetches a user by ID.
Returns {:ok, user} or {:error, :not_found}.
"""
def fetch_user(id), do: # ...
end