Back to course

CS101

Week 3: Pattern Matching & Control Flow

Week 3: Pattern Matching & Control Flow

What You’ll Learn

  • How pattern matching drives control flow
  • Using tuples and lists with pattern matching
  • Guards for adding conditions to patterns
  • Writing expressive and safe function definitions
  • Eliminating complex conditionals using pattern matching

Concept

In imperative programming, control flow is usually written using:

  • if / else
  • switch statements

In functional programming (Elixir), control flow is driven by pattern matching.

Instead of checking conditions inside a function, we define multiple function clauses that match different patterns.

Basic Pattern Matching

def parse_user({:user, name, age}) do
  "#{name} is #{age} years old"
end

def parse_user(_), do: "Invalid input"

Here:

  • The first function matches a specific tuple shape
  • The second acts as a fallback

Pattern Matching with Multiple Cases

def get_status({:ok}), do: "success"
def get_status({:error}), do: "failure"

Each function clause handles a specific case.

Why This Matters in Real Systems

In real systems:

  • Data comes in structured formats (tuples, maps, events)
  • Systems need to handle multiple scenarios safely

Pattern matching helps by:

  • making all cases explicit
  • preventing invalid states
  • reducing nested conditionals

This leads to:

  • clearer logic
  • fewer runtime errors
  • easier debugging

Examples

Example 1: Event Parsing

def parse_event({:login, name}), do: "#{name} logged in"
def parse_event({:logout, name}), do: "#{name} logged out"
def parse_event({:purchase, name, quantity}), do: "#{name} purchased #{quantity} items"
def parse_event(_), do: "invalid event"

Example 2: Result Handling

def describe_result({:ok, value}), do: "Result is #{value}"
def describe_result({:error, :divide_by_zero}), do: "Cannot divide by zero"
def describe_result({:error, :not_a_number}), do: "Input was not a number"
def describe_result(_), do: "unknown result"

Example 3: Guards

def check_even_odd(x) when rem(x, 2) == 0, do: "even"
def check_even_odd(_x), do: "odd"

Guards allow us to add conditions to pattern matching.

Problems

Q1

Write sum_even(list)

sum_even([1,2,3,4,5,6]) # 12
sum_even([1,3,5]) # 0
sum_even([]) # 0

Q2

Write a function permission/1 that:

  • allows {:user, :read}
  • allows {:guest, :read}
  • allows {:admin, :read} and {:admin, :write}
  • denies everything else

Q3

Write a function count_errors/1 that counts how many elements match {:error, _} in a list.

[
  {:info, "started"},
  {:error, "failed to connect"},
  {:info, "retrying"},
  {:error, "timeout"}
]
# Output: 2

Solutions

Show Solutions

Q1

def sum_even(list), do: sum_even(list, 0)
defp sum_even([], sum), do: sum
defp sum_even([head | tail], sum) when rem(head, 2) == 0, do: sum_even(tail, sum + head)
defp sum_even([_head | tail], sum), do: sum_even(tail, sum)

Q2

def permission({role, :read}) when role in [:user, :guest], do: :allowed
def permission({:admin, action}) when action in [:read, :write], do: :allowed
def permission(_), do: :denied

Q3

def count_errors(logs), do: count_errors(logs, 0)
defp count_errors([], count), do: count
defp count_errors([{:error, _msg} | tail], count), do: count_errors(tail, count + 1)
defp count_errors([_head | tail], count), do: count_errors(tail, count)

Sliding Window Problems

SW1 — Max consecutive duplicates

max_consecutive([1,1,1,2,2,3,3,3,3])
# → 4

SW2 — Count increasing pairs

count_increasing([1,2,1,3,4])
# pairs:
# (1,2) ✔
# (2,1) ✖
# (1,3) ✔
# (3,4) ✔
# → 3

SW3 — Longest increasing streak

longest_increasing([1,2,3,1,2,3,4])
# → 4

Sliding Window Solutions

Show Solutions

SW1

def max_consecutive(list), do: max_consecutive(list, 1, 1)
defp max_consecutive([], _current, max), do: max
defp max_consecutive([_], _current, max), do: max
defp max_consecutive([head, next | tail], current, max) when head == next do
  new_current = current + 1
  new_max = max(new_current, max)
  max_consecutive([next | tail], new_current, new_max)
end
defp max_consecutive([_head | tail], _current, max) do
  max_consecutive(tail, 1, max)
end

SW2

# def count_increasing(list), do: count_increasing(list, 0)
# defp count_increasing([], count), do: count
# defp count_increasing([_], count), do: count
# defp count_increasing([head, next | tail], count) when head < next do
#   count_increasing([next | tail], count + 1)
# end
# defp count_increasing([_head | tail], count) do
#   count_increasing(tail, count)
# end

def count_increasing([]), do: 0
def count_increasing([_]), do: 0
def count_increasing([head, next | tail]) when head < next do
  1 + count_increasing([next | tail])
end
def count_increasing([_head | tail]) do
  count_increasing(tail)
end

SW3

def longest_increasing(list), do: longest_increasing(list, 1, 1)
defp longest_increasing([], _current, max), do: max
defp longest_increasing([_], _current, max), do: max
defp longest_increasing([head, next | tail], current, max) when head < next do
  new_current = current + 1
  new_max = max(new_current, max)
  longest_increasing([next | tail], new_current, new_max)
end
defp longest_increasing([_head | tail], _current, max) do
  longest_increasing(tail, 1, max)
end

Summary

  • Pattern matching replaces conditional-heavy logic
  • Function clauses define behavior for different inputs
  • Guards add extra validation to patterns
  • Complex data can be safely handled using matching
  • This approach leads to clearer and more reliable systems