Pattern matching is one of Elixir's most powerful features, allowing you to match against data structures and extract values in a single, elegant operation. Think of it as an advanced form of assignment that can also check the shape and content of your data. In this comprehensive article, we'll explore pattern matching in depth - from basic concepts to advanced techniques, covering everything you need to master this fundamental Elixir feature.
Note: The examples in this article use Elixir 1.18.3. While most operations should work across different versions, some functionality might vary.
Table of Contents
- Introduction
- Understanding Pattern Matching
- Basic Pattern Matching
- Pattern Matching with Data Structures
- Pattern Matching in Function Definitions
- Guards: Adding Conditions to Patterns
- Advanced Matching Techniques
- Common Patterns and Best Practices
- Conclusion
- Further Reading
- Next Steps
Introduction
Pattern matching is like a sophisticated puzzle solver. Instead of just assigning values to variables, pattern matching lets you:
- Check if data has a specific structure
- Extract values from complex data structures
- Create multiple function clauses that handle different cases
- Write more declarative and readable code
Unlike traditional assignment in other languages, pattern matching in Elixir is about asserting that the right side of the =
operator matches the pattern on the left side.
Understanding Pattern Matching
The Match Operator
In Elixir, =
is not just an assignment operator—it's a match operator. When we write x = 1
, we're actually saying "match the pattern x
with the value 1
". Since x
is an unbound variable, it matches any value and binds to it.
# Simple matching
x = 1
1 = x # This works! We're asserting that 1 matches x
2 = x # This fails with MatchError
Testing in IEx:
iex> x = 1
1
iex> 1 = x
1
iex> 2 = x
** (MatchError) no match of right hand side value: 1
Pattern Matching is Structural
Pattern matching works by comparing the structure and values of data:
# Matching tuples
{:ok, result} = {:ok, 42}
# result is now bound to 42
# The pattern must match the structure
{:ok, result} = {:error, "failed"} # MatchError!
# You can match partial structures
{:user, _, age} = {:user, "Alice", 25}
# age is now 25, we ignored the name with _
Testing in IEx:
iex> {:ok, result} = {:ok, 42}
{:ok, 42}
iex> result
42
iex> {:ok, result} = {:error, "failed"}
** (MatchError) no match of right hand side value: {:error, "failed"}
iex> {:user, _, age} = {:user, "Alice", 25}
{:user, "Alice", 25}
iex> age
25
Basic Pattern Matching
Matching Simple Values
defmodule BasicMatching do
def describe_number(0), do: "zero"
def describe_number(1), do: "one"
def describe_number(2), do: "two"
def describe_number(n) when n > 0, do: "positive"
def describe_number(n) when n < 0, do: "negative"
def process_result({:ok, value}), do: "Success: #{value}"
def process_result({:error, reason}), do: "Failed: #{reason}"
def process_result(_), do: "Unknown result"
end
Testing in IEx:
iex> BasicMatching.describe_number(0)
"zero"
iex> BasicMatching.describe_number(42)
"positive"
iex> BasicMatching.describe_number(-5)
"negative"
iex> BasicMatching.process_result({:ok, "data"})
"Success: data"
iex> BasicMatching.process_result({:error, "timeout"})
"Failed: timeout"
The Underscore Pattern
The underscore _
is a special pattern that matches anything but doesn't bind the value:
defmodule UnderscoreExamples do
# Match any three-element tuple starting with :data
def extract_id({:data, id, _}), do: id
# Match a map but only extract specific keys
def get_name(%{name: name, age: _, email: _}), do: name
# Use multiple underscores - each is independent
def third_element({_, _, third}), do: third
end
Testing in IEx:
iex> UnderscoreExamples.extract_id({:data, 123, "extra info"})
123
iex> UnderscoreExamples.get_name(%{name: "Bob", age: 30, email: "[email protected]"})
"Bob"
iex> UnderscoreExamples.third_element({:a, :b, :c})
:c
Pattern Matching with Data Structures
Lists
Lists have special pattern matching syntax using the [head | tail]
notation:
defmodule ListPatterns do
def empty?([]), do: true
def empty?([_ | _]), do: false
def first([head | _]), do: {:ok, head}
def first([]), do: {:error, "empty list"}
def second([_, second | _]), do: {:ok, second}
def second(_), do: {:error, "list too short"}
# Recursive list processing
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)
# Match specific patterns
def starts_with_zero?([0 | _]), do: true
def starts_with_zero?(_), do: false
end
Testing in IEx:
iex> ListPatterns.empty?([])
true
iex> ListPatterns.empty?([1, 2, 3])
false
iex> ListPatterns.first([10, 20, 30])
{:ok, 10}
iex> ListPatterns.second([10, 20, 30])
{:ok, 20}
iex> ListPatterns.sum([1, 2, 3, 4, 5])
15
iex> ListPatterns.starts_with_zero?([0, 1, 2])
true
Maps
Map pattern matching is particularly useful for extracting values:
defmodule MapPatterns do
# Match required keys
def greet(%{name: name}), do: "Hello, #{name}!"
# Match multiple keys
def full_name(%{first: first, last: last}), do: "#{first} #{last}"
# Optional keys with defaults
def user_info(%{name: name} = user) do
age = Map.get(user, :age, "unknown")
"#{name} (age: #{age})"
end
# Nested map matching
def get_city(%{address: %{city: city}}), do: {:ok, city}
def get_city(_), do: {:error, "no city found"}
# Update specific fields
def birthday(%{age: age} = person) do
%{person | age: age + 1}
end
end
Testing in IEx:
iex> MapPatterns.greet(%{name: "Alice", age: 30})
"Hello, Alice!"
iex> MapPatterns.full_name(%{first: "John", last: "Doe", age: 25})
"John Doe"
iex> MapPatterns.user_info(%{name: "Bob"})
"Bob (age: unknown)"
iex> MapPatterns.get_city(%{address: %{city: "Paris", country: "France"}})
{:ok, "Paris"}
iex> MapPatterns.birthday(%{name: "Carol", age: 29})
%{age: 30, name: "Carol"}
Structs
Structs are maps with predefined keys and can be pattern matched like maps:
defmodule User do
defstruct [:name, :email, :age]
end
defmodule StructPatterns do
# Match against struct type
def process(%User{} = user), do: {:user, user}
def process(%{}), do: {:map, "regular map"}
# Extract specific fields
def adult?(%User{age: age}) when age >= 18, do: true
def adult?(%User{}), do: false
# Match and update
def anonymize(%User{} = user) do
%{user | name: "Anonymous", email: "[email protected]"}
end
end
Testing in IEx:
iex> user = %User{name: "Alice", email: "[email protected]", age: 25}
%User{age: 25, email: "[email protected]", name: "Alice"}
iex> StructPatterns.process(user)
{:user, %User{age: 25, email: "[email protected]", name: "Alice"}}
iex> StructPatterns.process(%{name: "Bob"})
{:map, "regular map"}
iex> StructPatterns.adult?(user)
true
iex> StructPatterns.anonymize(user)
%User{age: 25, email: "[email protected]", name: "Anonymous"}
Pattern Matching in Function Definitions
Multiple Function Clauses
Pattern matching in function definitions is idiomatic in Elixir and allows you to write different implementations for different input patterns:
defmodule Calculator do
# Pattern match on operation atoms
def calculate(:add, a, b), do: a + b
def calculate(:subtract, a, b), do: a - b
def calculate(:multiply, a, b), do: a * b
def calculate(:divide, a, 0), do: {:error, "division by zero"}
def calculate(:divide, a, b), do: {:ok, a / b}
# Pattern match on data structure
def area({:rectangle, width, height}), do: width * height
def area({:circle, radius}), do: 3.14159 * radius * radius
def area({:triangle, base, height}), do: 0.5 * base * height
end
Testing in IEx:
iex> Calculator.calculate(:add, 10, 5)
15
iex> Calculator.calculate(:divide, 10, 0)
{:error, "division by zero"}
iex> Calculator.calculate(:divide, 10, 2)
{:ok, 5.0}
iex> Calculator.area({:rectangle, 4, 5})
20
iex> Calculator.area({:circle, 3})
28.27431
Pattern Matching in Anonymous Functions
defmodule AnonymousPatterns do
def process_list(list) do
Enum.map(list, fn
{:ok, value} -> value * 2
{:error, _} -> 0
value -> value
end)
end
def classify_numbers(numbers) do
Enum.map(numbers, fn
0 -> :zero
n when n > 0 -> :positive
_ -> :negative
end)
end
end
Testing in IEx:
iex> AnonymousPatterns.process_list([{:ok, 5}, {:error, "failed"}, 10])
[10, 0, 10]
iex> AnonymousPatterns.classify_numbers([-5, 0, 3, -2, 7])
[:negative, :zero, :positive, :negative, :positive]
Guards: Adding Conditions to Patterns
Guards allow you to add additional conditions to pattern matches:
defmodule GuardExamples do
# Type guards
def double(x) when is_number(x), do: x * 2
def double(x) when is_binary(x), do: x <> x
# Comparison guards
def grade(score) when score >= 90, do: "A"
def grade(score) when score >= 80, do: "B"
def grade(score) when score >= 70, do: "C"
def grade(score) when score >= 60, do: "D"
def grade(_), do: "F"
# Multiple conditions
def can_vote?(age, citizen) when age >= 18 and citizen == true, do: true
def can_vote?(_, _), do: false
# Guards with pattern matching
def process_user(%{age: age, status: :active} = user) when age >= 18 do
{:ok, "Adult user: #{user.name}"}
end
def process_user(%{age: age}) when age < 18 do
{:error, "User must be 18 or older"}
end
def process_user(_), do: {:error, "Invalid user"}
end
Testing in IEx:
iex> GuardExamples.double(5)
10
iex> GuardExamples.double("hello")
"hellohello"
iex> GuardExamples.grade(85)
"B"
iex> GuardExamples.can_vote?(20, true)
true
iex> GuardExamples.can_vote?(17, true)
false
iex> GuardExamples.process_user(%{name: "Alice", age: 20, status: :active})
{:ok, "Adult user: Alice"}
Advanced Matching Techniques
The Pin Operator (^)
The pin operator ^
allows you to match against existing variable values instead of rebinding:
defmodule PinOperator do
def match_exact_value(list, target) do
Enum.any?(list, fn
^target -> true
_ -> false
end)
end
def update_if_matches(map, key, old_value, new_value) do
case Map.get(map, key) do
^old_value -> Map.put(map, key, new_value)
_ -> {:error, "Value doesn't match"}
end
end
# Finds the first duplicated value and returns its positions
def find_duplicate_position(list) do
list
|> Enum.with_index()
|> Enum.reduce_while(nil, fn {value, index}, _ ->
case Enum.find_index(list, fn x -> x == value end) do
^index -> {:cont, nil} # First occurrence
other -> {:halt, {value, other, index}} # Found duplicate
end
end)
end
end
Testing in IEx:
iex> PinOperator.match_exact_value([1, 2, 3, 4], 3)
true
iex> target = 5
5
iex> PinOperator.match_exact_value([1, 2, 3, 4], target)
false
iex> PinOperator.update_if_matches(%{status: :pending}, :status, :pending, :active)
%{status: :active}
iex> PinOperator.update_if_matches(%{status: :active}, :status, :pending, :active)
{:error, "Value doesn't match"}
iex> PinOperator.find_duplicate_position([1, 2, 3, 2, 5])
{2, 1, 3}
Matching with case
The case
expression is perfect for pattern matching against multiple possibilities:
defmodule CasePatterns do
def handle_response(response) do
case response do
{:ok, data} when is_list(data) ->
"Got #{length(data)} items"
{:ok, data} when is_map(data) ->
"Got a map with #{map_size(data)} keys"
{:ok, data} ->
"Got data: #{inspect(data)}"
{:error, :not_found} ->
"Resource not found"
{:error, reason} when is_binary(reason) ->
"Error: #{reason}"
_ ->
"Unknown response"
end
end
def parse_command(input) do
case String.split(input, " ", parts: 2) do
["get", key] -> {:get, key}
["set", rest] ->
case String.split(rest, "=", parts: 2) do
[key, value] -> {:set, key, value}
_ -> {:error, "Invalid set syntax"}
end
["delete", key] -> {:delete, key}
[cmd | _] -> {:error, "Unknown command: #{cmd}"}
[] -> {:error, "Empty command"}
end
end
end
Testing in IEx:
iex> CasePatterns.handle_response({:ok, [1, 2, 3]})
"Got 3 items"
iex> CasePatterns.handle_response({:ok, %{a: 1, b: 2}})
"Got a map with 2 keys"
iex> CasePatterns.handle_response({:error, "Connection timeout"})
"Error: Connection timeout"
iex> CasePatterns.parse_command("get user:123")
{:get, "user:123"}
iex> CasePatterns.parse_command("set name=Alice")
{:set, "name", "Alice"}
iex> CasePatterns.parse_command("invalid")
{:error, "Unknown command: invalid"}
Common Patterns and Best Practices
1. Use Pattern Matching for Control Flow
Instead of nested if statements, use pattern matching:
# Good: Clear pattern matching
def process_age(age) do
case age do
age when age >= 65 -> :senior
age when age >= 18 -> :adult
age when age >= 13 -> :teen
_ -> :child
end
end
# Less idiomatic: Nested ifs
def process_age_imperative(age) do
if age >= 65 do
:senior
else
if age >= 18 do
:adult
else
if age >= 13 do
:teen
else
:child
end
end
end
end
2. Order Matters
Put more specific patterns before general ones:
defmodule OrderMatters do
# Correct order: specific to general
def classify([]), do: :empty
def classify([_]), do: :single
def classify([_, _]), do: :pair
def classify(list) when is_list(list), do: :many
# Wrong order: general pattern would catch everything
# def classify(list) when is_list(list), do: :many # This would match all lists!
# def classify([]), do: :empty # Never reached
end
3. Use Guards for Additional Constraints
defmodule SafeOperations do
def safe_divide(a, b) when is_number(a) and is_number(b) and b != 0 do
{:ok, a / b}
end
def safe_divide(_, 0), do: {:error, :division_by_zero}
def safe_divide(_, _), do: {:error, :invalid_arguments}
def validate_email(email) when is_binary(email) do
if String.contains?(email, "@") do
{:ok, email}
else
{:error, :invalid_format}
end
end
def validate_email(_), do: {:error, :not_a_string}
end
4. Destructure Only What You Need
defmodule EfficientMatching do
# Good: Only extract what you need
def get_user_name(%{user: %{name: name}}), do: name
# Less efficient: Extracting entire structures unnecessarily
def get_user_name_verbose(%{user: user}) do
user.name
end
# Good: Ignore irrelevant data
def process_event({:event, type, _timestamp, _metadata}) do
"Processing #{type} event"
end
end
Conclusion
Pattern matching is a cornerstone of Elixir programming that makes code more declarative, readable, and robust. By matching on the structure of data rather than imperatively checking conditions, you can:
- Write cleaner, more expressive code
- Handle different cases elegantly with function clauses
- Extract values from complex data structures easily
- Ensure data has the expected shape before processing
- Create powerful abstractions with minimal code
Key takeaways:
- The
=
operator is for matching, not just assignment - Patterns can include literals, variables, and underscore
- Function clauses with pattern matching replace complex conditionals
- Guards add extra power to pattern matching
- The pin operator
^
lets you match against existing values - Order matters—put specific patterns before general ones
Master pattern matching, and you'll write Elixir code that's both powerful and beautiful.
Further Reading
- Elixir Documentation - Pattern Matching
- Elixir School - Pattern Matching
- Programming Elixir 1.6 - Pattern Matching Chapter
Next Steps
Now that we've covered pattern matching comprehensively, in the next article we'll explore Elixir's data structures:
Lists in Elixir
- Basic list operations and manipulations
- Head/tail decomposition
- List comprehensions
- Performance characteristics
- Common list algorithms
Lists are fundamental to functional programming, and understanding them deeply will help you write more efficient Elixir code.
Top comments (0)