Elixir Pattern Matching: Basic to Super Advanced with Cheatsheets
Pattern matching isn’t just a feature in Elixir—it’s the foundation of how you think about data and control flow. With Elixir 1.6.0’s improvements, pattern matching has become even more powerful.
The Basics: Beyond Simple Assignment
Traditional languages use assignment. Elixir uses pattern matching:
# Not assignment—pattern matching!
x = 1 # Matches x to 1
1 = x # Validates x equals 1
2 = x # MatchError!
This fundamental shift changes everything.
Level 1: Destructuring Data
Lists and Tuples
# List destructuring
[head | tail] = [1, 2, 3, 4]
# head = 1, tail = [2, 3, 4]
[first, second | rest] = [1, 2, 3, 4, 5]
# first = 1, second = 2, rest = [3, 4, 5]
# Tuple destructuring
{:ok, result} = {:ok, "Success"}
# result = "Success"
{:error, reason} = {:error, "Failed"}
# reason = "Failed"
Maps and Structs
# Map destructuring
%{name: name, age: age} = %{name: "Alice", age: 30, city: "NYC"}
# name = "Alice", age = 30
# Partial matching
%{name: name} = %{name: "Bob", age: 25, occupation: "Developer"}
# name = "Bob", other fields ignored
# Struct pattern matching
defmodule User do
defstruct [:name, :email, :role]
end
%User{name: name, role: :admin} = %User{name: "Alice", email: "[email protected]", role: :admin}
Level 2: Function Pattern Matching
This is where Elixir becomes magical:
defmodule Math do
# Pattern match on function arguments
def factorial(0), do: 1
def factorial(n) when n > 0, do: n * factorial(n - 1)
# Multiple clauses for different patterns
def describe_list([]), do: "Empty list"
def describe_list([_]), do: "Single item"
def describe_list([_, _]), do: "Two items"
def describe_list(_), do: "Many items"
end
HTTP Response Handling
defmodule ApiClient do
def handle_response({:ok, %{status: 200, body: body}}) do
{:success, Jason.decode!(body)}
end
def handle_response({:ok, %{status: 404}}) do
{:error, :not_found}
end
def handle_response({:ok, %{status: status}}) when status >= 500 do
{:error, :server_error}
end
def handle_response({:error, reason}) do
{:error, reason}
end
end
Level 3: Guards and Advanced Constraints
Guards add powerful constraints to pattern matching:
defmodule Validator do
# Number guards
def classify(n) when n > 0, do: :positive
def classify(n) when n < 0, do: :negative
def classify(0), do: :zero
# Type guards
def process(data) when is_binary(data), do: String.upcase(data)
def process(data) when is_integer(data), do: data * 2
def process(data) when is_list(data), do: Enum.reverse(data)
# Complex guards
def valid_user(%{age: age, name: name})
when is_integer(age) and age >= 18 and byte_size(name) > 0 do
:valid
end
def valid_user(_), do: :invalid
end
Custom Guard Functions (Elixir 1.6.0+)
defmodule Guards do
# Custom guard macros
defguard is_even(n) when is_integer(n) and rem(n, 2) == 0
defguard is_adult(age) when is_integer(age) and age >= 18
def process_adult_id(id) when is_even(id) do
"Processing even adult ID: #{id}"
end
end
Level 4: Case Statements and With Expressions
Powerful Case Statements
defmodule FileProcessor do
def process_file(filename) do
case File.read(filename) do
{:ok, content} ->
case Jason.decode(content) do
{:ok, data} -> {:success, data}
{:error, _} -> {:error, :invalid_json}
end
{:error, :enoent} -> {:error, :file_not_found}
{:error, reason} -> {:error, reason}
end
end
end
With Expressions for Complex Pipelines
defmodule UserRegistration do
def register_user(params) do
with {:ok, email} <- validate_email(params["email"]),
{:ok, password} <- validate_password(params["password"]),
{:ok, user} <- create_user(email, password),
{:ok, _} <- send_welcome_email(user) do
{:ok, user}
else
{:error, reason} -> {:error, reason}
end
end
end
Level 5: Pin Operator and Advanced Techniques
The pin operator ^ matches against existing values:
# Without pin - rebinding
user_id = 123
user_id = 456 # user_id is now 456
# With pin - pattern matching
user_id = 123
^user_id = fetch_user_id() # Only matches if result equals 123
Advanced Patterns
defmodule AdvancedPatterns do
# Nested pattern matching
def extract_user_data(%{
user: %{
profile: %{name: name, settings: %{theme: theme}}
}
}) do
{name, theme}
end
# String pattern matching (Elixir 1.6.0+)
def parse_command("GET " <> path), do: {:get, path}
def parse_command("POST " <> path), do: {:post, path}
def parse_command("DELETE " <> path), do: {:delete, path}
# Binary pattern matching
def parse_packet(<<version::8, type::8, data::binary>>) do
%{version: version, type: type, data: data}
end
end
Pattern Matching Cheatsheet
Basic Patterns
# Values
x = 1
^x = 1
# Lists
[] = [] # Empty list
[h | t] = [1, 2, 3] # h=1, t=[2,3]
[a, b] = [1, 2] # a=1, b=2
[a, b | _] = [1, 2, 3, 4] # a=1, b=2, ignore rest
# Tuples
{} = {} # Empty tuple
{a, b} = {1, 2} # a=1, b=2
{:ok, result} = {:ok, "done"} # result="done"
# Maps
%{} = %{} # Empty map
%{a: x} = %{a: 1, b: 2} # x=1
%{a: 1, b: x} = %{a: 1, b: 2} # x=2
Guard Clauses
# Type guards
when is_atom(x)
when is_binary(x)
when is_integer(x)
when is_list(x)
when is_map(x)
# Comparison guards
when x > 0
when x in [1, 2, 3]
when length(x) > 0
# Boolean guards
when is_integer(x) and x > 0
when is_binary(x) or is_atom(x)
Common Patterns
# Result tuples
{:ok, data}
{:error, reason}
# Option types
{:some, value}
:none
# GenServer responses
{:reply, result, new_state}
{:noreply, new_state}
Real-World Example: JSON API Parser
defmodule ApiParser do
def parse_response(response) do
with {:ok, %{status: status, body: body}} <- validate_response(response),
{:ok, json} <- decode_json(body),
{:ok, data} <- extract_data(json, status) do
{:ok, data}
end
end
defp validate_response({:ok, %{status: status} = response})
when status in [200, 201, 404] do
{:ok, response}
end
defp validate_response(_), do: {:error, :invalid_response}
defp decode_json(body) when is_binary(body) do
case Jason.decode(body) do
{:ok, json} -> {:ok, json}
{:error, _} -> {:error, :invalid_json}
end
end
defp extract_data(%{"data" => data}, status) when status in [200, 201] do
{:ok, data}
end
defp extract_data(%{"error" => error}, 404) do
{:error, error}
end
defp extract_data(_, _), do: {:error, :unexpected_format}
end
Performance Notes
Pattern matching is zero-cost at runtime. The BEAM VM compiles patterns into efficient jump tables and binary operations.
Next Steps
Master these patterns and you’ll write Elixir like a native. Pattern matching isn’t just syntax—it’s a new way of thinking about data transformation and control flow.
Coming up: Diving deep into Lists and Maps with more cheatsheets and performance tips.