Pattern Matching Function Clauses

Pattern Matching with Multiple Function Clauses

Welcome. In this course on advanced pattern matching in Elixir, we start by using multiple function clauses to route different inputs to the correct logic. If you are new to this idea, think of it as matching the “shape” of data right in the function head. This builds on core Elixir fundamentals and prepares you for later topics like guards, case statements, and the pin operator.

Walkthrough: A Calculator Using Function Clauses

defmodule Calculator do
  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, b}) when b != 0, do: a / b
  def calculate({:divide, _a, 0}), do: {:error, "Cannot divide by zero"}
end

IO.puts(Calculator.calculate({:add, 5, 3}))
IO.puts(Calculator.calculate({:multiply, 4, 7}))
IO.inspect(Calculator.calculate({:divide, 10, 0}))

Explanation:

  • defmodule Calculator do … end defines a module named Calculator.
  • Multiple def calculate clauses each match a different tuple pattern:
    • {:add, a, b} returns a + b
    • {:subtract, a, b} returns a - b
    • {:multiply, a, b} returns a * b
  • Division is handled with two clauses:
    • {:divide, a, b} when b != 0 uses a guard to ensure b is not zero, then returns a / b.
    • {:divide, _a, 0} matches divide-by-zero and returns an error tuple. The underscore in _a means “ignore this value.”
  • Order matters. Elixir picks the first clause whose pattern matches and whose guard (if any) evaluates to true.
  • The last three lines run the function:
    • IO.puts prints the results of addition (8) and multiplication (28).
    • IO.inspect prints the error tuple {:error, "Cannot divide by zero"} in a readable form.

This approach keeps the logic simple and readable. Instead of one function with many if/else statements, each operation becomes a clear, separate clause that matches a specific input shape.

Beyond Tuples: Matching Lists, Maps, Structs, and Binaries

Pattern matching in function heads is not limited to tuples. You can match on lists, maps, structs, and binaries, allowing for highly expressive and safe APIs.

Matching Lists

defmodule ListOps do
  # Base case: empty list
  def sum([]), do: 0
  # Recursive case: non-empty list
  def sum([head | tail]), do: head + sum(tail)
end

IO.puts(ListOps.sum([1, 2, 3]))  # 6

Explanation:

  • The base case sum([]) matches an empty list and returns 0, stopping recursion.
  • The recursive case sum([head | tail]) uses the [head | tail] pattern to split a non-empty list. head is the first element, and tail is the rest of the list.
  • This pattern is the standard way to traverse and process collections in functional programming.

Matching Maps

defmodule Greeter do
  def greet(%{name: name, language: :en}), do: "Hello, #{name}!"
  def greet(%{name: name, language: :es}), do: "¡Hola, #{name}!"
  def greet(%{name: name}), do: "Hi, #{name}!"
end

IO.puts(Greeter.greet(%{name: "Ana", language: :es}))  # ¡Hola, Ana!
IO.puts(Greeter.greet(%{name: "Bob", language: :en}))  # Hello, Bob!
IO.puts(Greeter.greet(%{name: "Eve", language: :fr}))  # Hi, Eve!

Explanation:

  • Map matching is "partial": the pattern % {name: name} matches any map that has a :name key, even if it contains dozens of other keys.
  • You can match literal values (like :en or :es) alongside variables (like name).
  • The last clause greet(%{name: name}) acts as a fallback for any map that has a :name key but doesn't match the specific languages above.

Matching Structs

defmodule User do
  defstruct [:name, :role]
end

defmodule Permissions do
  def can_edit?(%User{role: :admin}), do: true
  def can_edit?(%User{}), do: false
end

defmodule Test do
  def run do
    IO.puts(Permissions.can_edit?(%User{name: "Sam", role: :admin}))  # true
    IO.puts(Permissions.can_edit?(%User{name: "Alex", role: :user}))  # false
  end
end

Test.run()

Explanation:

  • Struct matching uses the %ModuleName{} syntax.
  • Unlike maps, matching against %User{} ensures that the input is specifically a User struct and not a plain map with the same keys.
  • can_edit?(%User{role: :admin}) matches only when the role is exactly :admin, while can_edit?(%User{}) matches any other User struct.

Matching Binaries

defmodule FileType do
  def type(<<"GIF8", _rest::binary>>), do: :gif
  def type(<<"%PDF", _rest::binary>>), do: :pdf
  def type(_), do: :unknown
end

IO.inspect(FileType.type("%PDF-1.4 ..."))  # :pdf
IO.inspect(FileType.type("GIF89a..."))     # :gif
IO.inspect(FileType.type("random"))        # :unknown

Explanation:

  • Binaries (and strings) can be matched using the << >> syntax.
  • <<"GIF8", _rest::binary>> matches any binary that starts with the literal string "GIF8".
  • The ::binary modifier tells Elixir that _rest can be any sequence of bits/bytes of any length following the prefix. This is commonly used for parsing file headers or network protocols.

Failure Modes: FunctionClauseError and Safe Fallbacks

If no function clause matches, Elixir raises a FunctionClauseError. To avoid this, provide a catch-all clause to handle unexpected inputs safely.

defmodule SafeCalc do
  def op({:add, a, b}), do: a + b
  def op({:subtract, a, b}), do: a - b
  def op(_), do: {:error, :invalid_operation}
end

IO.inspect(SafeCalc.op({:multiply, 2, 3}))  # {:error, :invalid_operation}

Overlapping Clauses, Order, and Guards

Order matters: Elixir matches clauses from top to bottom. More specific patterns (or those with guards) should come before more general ones.

Elixir matches the structural pattern first; if it matches, the guard is then evaluated. When patterns overlap, order still decides which clause is tried first; a failing guard causes Elixir to continue trying later clauses.

defmodule Overlap do
  def check([1, 2, 3]), do: :exact
  def check([1 | _]), do: :starts_with_one
  def check(_), do: :other
end

IO.puts(Overlap.check([1, 2, 3]))      # exact
IO.puts(Overlap.check([1, 5, 9]))      # starts_with_one
IO.puts(Overlap.check([2, 3, 4]))      # other

Guards can make clauses more precise, but they don’t change which clause is tried first—order still governs which structural pattern is tested before guards are evaluated.

defmodule GuardDemo do
  def classify(n) when is_integer(n) and n > 0, do: :positive_integer
  def classify(n) when is_integer(n), do: :nonpositive_integer
  def classify(_), do: :not_integer
end

IO.puts(GuardDemo.classify(5))    # positive_integer
IO.puts(GuardDemo.classify(-2))   # nonpositive_integer
IO.puts(GuardDemo.classify("hi")) # not_integer

Summary and Next Steps

You learned how to:

  • Match tuple shapes in function heads to dispatch logic (Calculator).
  • Use guards to protect clauses (b != 0) and add safe fallbacks for invalid inputs.
  • Reason about clause order and overlapping patterns; Elixir matches structure first, then evaluates guards, and continues to later clauses when a guard fails.
  • Match lists, maps, structs, and binaries in function heads.

This pattern is common in real Elixir codebases because it is fast to read, safe, and extensible. Ready to solidify this skill? Head over to the practice section and try it out.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal