DEV Community

Cover image for Learning Elixir: Pattern Matching
João Paulo Abreu
João Paulo Abreu

Posted on

Learning Elixir: Pattern Matching

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

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 
Enter fullscreen mode Exit fullscreen mode

Testing in IEx:

iex> x = 1 1 iex> 1 = x 1 iex> 2 = x ** (MatchError) no match of right hand side value: 1 
Enter fullscreen mode Exit fullscreen mode

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 _ 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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"} 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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"} 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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] 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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"} 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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} 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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"} 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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

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)