| name | elixir-pattern-matching |
| description | Use when Elixir pattern matching including function clauses, case statements, with statements, and destructuring. Use for elegant control flow. |
| allowed-tools | Bash, Read |
Elixir Pattern Matching
Master pattern matching in Elixir to write elegant, declarative code. This skill covers function patterns, case statements, guards, and destructuring across various data structures.
Basic Pattern Matching
# Simple assignment is pattern matching
x = 1
1 = x # This works because x matches 1
# Pattern matching with tuples
{:ok, value} = {:ok, "success"}
value # => "success"
# Will raise MatchError if patterns don't match
# {:error, _} = {:ok, "success"} # MatchError
# Pin operator to use existing value
x = 1
^x = 1 # Works
# ^x = 2 # MatchError
# Ignore values with underscore
{:ok, _} = {:ok, "any value"}
{_, _, third} = {1, 2, 3}
third # => 3
Function Pattern Matching
defmodule Calculator do
def add(a, b), do: a + b
def factorial(0), do: 1
def factorial(n) when n > 0, do: n * factorial(n - 1)
def describe_tuple({:ok, value}) do
"Success: #{value}"
end
def describe_tuple({:error, reason}) do
"Error: #{reason}"
end
def describe_tuple(_) do
"Unknown tuple format"
end
end
# Usage
Calculator.factorial(5) # => 120
Calculator.describe_tuple({:ok, "done"}) # => "Success: done"
Guards in Pattern Matching
defmodule NumberChecker do
def check(x) when is_integer(x) and x > 0 do
"Positive integer"
end
def check(x) when is_integer(x) and x < 0 do
"Negative integer"
end
def check(0), do: "Zero"
def check(x) when is_float(x), do: "Float"
def check(_), do: "Not a number"
end
defmodule Validator do
def valid_email?(email) when is_binary(email) do
String.contains?(email, "@")
end
def valid_email?(_), do: false
def in_range?(num, min, max)
when is_number(num) and num >= min and num <= max do
true
end
def in_range?(_, _, _), do: false
end
Case Statements
defmodule ResponseHandler do
def handle(response) do
case response do
{:ok, data} ->
{:success, data}
{:error, :not_found} ->
{:failure, "Resource not found"}
{:error, :timeout} ->
{:failure, "Request timed out"}
{:error, reason} ->
{:failure, "Error: #{inspect(reason)}"}
_ ->
{:failure, "Unknown response"}
end
end
def parse_number(str) do
case Integer.parse(str) do
{num, ""} -> {:ok, num}
{num, _remainder} -> {:ok, num}
:error -> {:error, "Not a valid number"}
end
end
end
With Statement for Pipeline Pattern Matching
defmodule UserService do
def create_user(params) do
with {:ok, email} <- validate_email(params["email"]),
{:ok, password} <- validate_password(params["password"]),
{:ok, user} <- insert_user(email, password),
{:ok, _} <- send_welcome_email(user) do
{:ok, user}
else
{:error, reason} -> {:error, reason}
_ -> {:error, "Unknown error"}
end
end
defp validate_email(email) when is_binary(email) do
if String.contains?(email, "@") do
{:ok, email}
else
{:error, "Invalid email"}
end
end
defp validate_email(_), do: {:error, "Email required"}
defp validate_password(pass) when is_binary(pass) do
if String.length(pass) >= 8 do
{:ok, pass}
else
{:error, "Password too short"}
end
end
defp validate_password(_), do: {:error, "Password required"}
defp insert_user(email, password) do
{:ok, %{id: 1, email: email}}
end
defp send_welcome_email(_user) do
{:ok, "sent"}
end
end
List Pattern Matching
defmodule ListOps do
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)
def first([head | _tail]), do: head
def first([]), do: nil
def second([_, second | _]), do: second
def second(_), do: nil
def take_first_three([a, b, c | _rest]) do
[a, b, c]
end
def take_first_three(list), do: list
def split_at_middle(list) do
middle = div(length(list), 2)
{Enum.take(list, middle), Enum.drop(list, middle)}
end
end
Map Pattern Matching
defmodule UserHandler do
def greet(%{name: name, age: age}) do
"Hello #{name}, you are #{age} years old"
end
def greet(%{name: name}) do
"Hello #{name}"
end
def admin?(%{role: "admin"}), do: true
def admin?(_), do: false
def process_user(%{id: id, name: name} = user) do
# Can use both the whole user and destructured parts
IO.puts("Processing user #{id}: #{name}")
user
end
def update_status(%{status: old_status} = user, new_status) do
%{user | status: new_status}
end
end
defmodule ConfigParser do
def get_database_url(config) do
case config do
%{database: %{host: host, port: port, name: db}} ->
"postgresql://#{host}:#{port}/#{db}"
%{database: %{url: url}} ->
url
_ ->
"postgresql://localhost:5432/default"
end
end
end
Struct Pattern Matching
defmodule User do
defstruct [:id, :name, :email, role: "user"]
end
defmodule StructMatcher do
def display_user(%User{name: name, email: email}) do
"#{name} <#{email}>"
end
def is_admin?(%User{role: "admin"}), do: true
def is_admin?(%User{}), do: false
def update_email(%User{} = user, new_email) do
%User{user | email: new_email}
end
end
# Usage
user = %User{id: 1, name: "Alice", email: "alice@example.com"}
StructMatcher.display_user(user)
Binary Pattern Matching
defmodule BinaryParser do
def parse_header(<<
magic::binary-size(4),
version::16,
flags::8,
rest::binary
>>) do
%{
magic: magic,
version: version,
flags: flags,
payload: rest
}
end
def parse_ipv4(<<a, b, c, d>>) do
"#{a}.#{b}.#{c}.#{d}"
end
def parse_utf8(<<codepoint::utf8, rest::binary>>) do
{codepoint, rest}
end
def extract_first_byte(<<first::8, _::binary>>) do
first
end
end
Cond for Multiple Conditions
defmodule GradeCalculator do
def letter_grade(score) do
cond do
score >= 90 -> "A"
score >= 80 -> "B"
score >= 70 -> "C"
score >= 60 -> "D"
true -> "F"
end
end
def describe_number(n) do
cond do
n < 0 -> "negative"
n == 0 -> "zero"
n > 0 and n < 10 -> "small positive"
n >= 10 and n < 100 -> "medium positive"
true -> "large positive"
end
end
end
Advanced Pattern Matching
defmodule AdvancedMatcher do
# Pattern matching in function arguments with multiple clauses
def process([]), do: :empty
def process([_]), do: :single
def process([_, _]), do: :pair
def process([h | t]) when length(t) > 1, do: :multiple
# Pattern matching with maps and guards
def format_response(%{status: status, body: body})
when status >= 200 and status < 300 do
{:ok, body}
end
def format_response(%{status: status, body: body})
when status >= 400 do
{:error, body}
end
# Nested pattern matching
def extract_user_city(%{
user: %{address: %{city: city}}
}) do
{:ok, city}
end
def extract_user_city(_), do: {:error, :no_city}
# Pattern matching in for comprehensions
def extract_ok_values(results) do
for {:ok, value} <- results, do: value
end
end
When to Use This Skill
Use elixir-pattern-matching when you need to:
- Write expressive, declarative control flow
- Handle different data shapes with function clauses
- Extract values from complex data structures
- Validate data formats at function boundaries
- Implement clean error handling with tagged tuples
- Parse binary data or protocols
- Build robust, maintainable Elixir applications
- Leverage Elixir's functional programming strengths
- Create clear, self-documenting code
Best Practices
- Use pattern matching instead of if/else when possible
- Order function clauses from most specific to most general
- Use guards to add constraints to patterns
- Leverage the pin operator when you need existing values
- Use underscore for values you don't care about
- Prefer pattern matching over accessor functions
- Use with statements for complex validation pipelines
- Keep patterns readable and not overly complex
- Document complex pattern matching logic
- Use tagged tuples {:ok, val} and {:error, reason} consistently
Common Pitfalls
- Forgetting that = is pattern matching, not assignment
- Not ordering function clauses correctly (specific to general)
- Overusing guards when simpler patterns would work
- Not handling all possible pattern cases
- Creating MatchErrors by not handling edge cases
- Forgetting to use pin operator when needed
- Making patterns too complex and hard to read
- Not using with statement for multi-step validations
- Ignoring compiler warnings about unused variables
- Not leveraging pattern matching for cleaner code