elixir-patterns

SKILL.md

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
Weekly Installs
6
GitHub Stars
75
First Seen
Jan 29, 2026
Installed on
codex5
github-copilot4
amp3
opencode3
kimi-cli3
gemini-cli3