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: "[email protected]"})
"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: "[email protected]"}
  end
end
Enter fullscreen mode Exit fullscreen mode

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"}
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)