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.