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: "bob@example.com"}) "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: "hidden@example.com"} end end
Testing in IEx:
iex> user = %User{name: "Alice", email: "alice@example.com", age: 25} %User{age: 25, email: "alice@example.com", name: "Alice"} iex> StructPatterns.process(user) {:user, %User{age: 25, email: "alice@example.com", name: "Alice"}} iex> StructPatterns.process(%{name: "Bob"}) {:map, "regular map"} iex> StructPatterns.adult?(user) true iex> StructPatterns.anonymize(user) %User{age: 25, email: "hidden@example.com", 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)