Phase 1, Session 2: Pattern Matching
Overview
Pattern matching is THE most important concept in Elixir and Erlang. It’s not just a feature - it’s the foundation of how you write code in these languages.
What you’ll learn:
-
The match operator
=(it’s NOT assignment!) - Destructuring tuples, lists, and maps
-
The pin operator
^ - Why pattern matching makes message handling elegant
Why Pattern Matching Matters for Your Agent Framework
In your multi-agent system, agents will constantly:
- Receive messages of different types
- Extract data from those messages
- Decide how to respond based on message structure
Pattern matching lets you do all this declaratively - you describe the shape of data you expect, and Elixir extracts what you need automatically.
Section 1: The Match Operator
1.1 The = is NOT Assignment
In most languages, = means “assign the value on the right to the variable on the left.”
In Elixir, = means “match the left side to the right side.”
# This looks like assignment, but it's actually matching
x = 1
# Elixir asks: "Can I make the left side equal the right side?"
# Yes! By binding x to 1.
x
# Here's the proof that = is matching, not assignment
# If x is 1, then 1 = x should work (because 1 matches 1)
1 = x # This works!
# But this will fail - 2 doesn't match 1
# Uncomment to see the error:
# 2 = x # ** (MatchError) no match of right hand side value: 1
1.2 The Mental Model
Think of = as an equation that Elixir tries to make true:
# Elixir tries to make left = right
# It can bind unbound variables to make the equation work
a = 5 # Bind a to 5 to make equation true
{b, c} = {1, 2} # Bind b to 1, c to 2
{a, b, c}
Section 2: Matching Tuples
Tuples are perfect for pattern matching because they have fixed, known structure.
2.1 Basic Tuple Matching
# Extract values from a tuple
{status, message} = {:ok, "Operation successful"}
{status, message}
# Match specific values and extract others
{:ok, result} = {:ok, 42}
result # We extracted the 42
# This will fail - the atoms don't match
# {:ok, result} = {:error, "Something went wrong"}
# ** (MatchError)
2.2 The :ok / :error Pattern
This is THE most common pattern in Elixir. Functions return {:ok, value} or {:error, reason}.
# Simulating a function that might succeed or fail
successful_result = {:ok, %{data: "user info", id: 123}}
failed_result = {:error, :not_found}
# Extract data from successful result
{:ok, data} = successful_result
data
# Extract reason from failed result
{:error, reason} = failed_result
reason
2.3 Nested Tuple Matching
# You can match nested structures
{:ok, {name, age}} = {:ok, {"Alice", 30}}
{name, age}
# Agent message example
message = {:task, "task-001", {:summarize, "Some long text..."}}
{:task, task_id, {action, content}} = message
{task_id, action, content}
Section 3: Matching Lists
Lists use the special [head | tail] syntax for matching.
3.1 Head and Tail
# Every non-empty list has a head (first element) and tail (rest)
list = [1, 2, 3, 4, 5]
[head | tail] = list
{head, tail}
# Get first two elements
[first, second | rest] = [1, 2, 3, 4, 5]
{first, second, rest}
# Match specific first element
[1 | rest] = [1, 2, 3] # Works - first element is 1
rest
# This would fail - first element isn't 1
# [1 | rest] = [2, 3, 4]
# ** (MatchError)
3.2 The Underscore _ Wildcard
Use _ when you don’t care about a value:
# We only want the head, ignore the tail
[head | _] = [1, 2, 3, 4, 5]
head
# Get second element, ignore first and rest
[_, second | _] = [1, 2, 3, 4, 5]
second
# In tuples too
{:ok, _, important} = {:ok, "ignore this", "keep this"}
important
3.3 Processing Agent Inbox
# Agent inbox with messages
inbox = [
{:task, "003", %{action: :analyze}},
{:task, "002", %{action: :summarize}},
{:task, "001", %{action: :search}}
]
# Get next message to process
[current_message | remaining_inbox] = inbox
{current_message, length(remaining_inbox)}
Section 4: Matching Maps
Maps match on a subset of keys - you don’t need to match all keys.
4.1 Basic Map Matching
# Extract specific keys from a map
agent = %{name: "Researcher", state: :idle, inbox: [], memory: %{}}
%{name: agent_name} = agent
agent_name
# Extract multiple keys
%{name: n, state: s} = agent
{n, s}
# The map can have MORE keys than you match
%{name: name} = %{name: "Alice", age: 30, role: "admin"}
name # Works! We only matched the keys we cared about
4.2 Matching Nested Maps
# Agent with nested memory
agent = %{
name: "Researcher",
state: :busy,
memory: %{
context: "Researching Elixir",
facts: ["Elixir runs on BEAM", "Pattern matching is powerful"]
}
}
%{memory: %{context: ctx}} = agent
ctx
4.3 Matching with String Keys
# Maps can have string keys (common with JSON)
json_data = %{"name" => "Bob", "age" => 25}
%{"name" => name} = json_data
name
4.4 Empty Map Matches Any Map
# An empty pattern matches any map
%{} = %{a: 1, b: 2, c: 3} # Matches!
# Useful for "is this a map?" checks
:matched
Section 5: The Pin Operator ^
By default, variables on the left side of = get rebound. The pin operator ^ prevents this.
5.1 The Problem: Rebinding
# x gets rebound to new value
x = 1
x = 2 # x is now 2
x
5.2 Pin to Match Existing Value
# Use ^ to match against existing value instead of rebinding
x = 1
# This MATCHES x's value (1) against the right side
^x = 1 # Works! 1 = 1
:matched
# This will fail - trying to match 1 against 2
x = 1
# ^x = 2 # ** (MatchError) no match of right hand side value: 2
5.3 Pin in Patterns
# Common use: match a specific ID you're looking for
expected_task_id = "task-001"
message = {:response, "task-001", "completed"}
{:response, ^expected_task_id, result} = message
result
# This would fail if IDs don't match
expected_id = "task-001"
wrong_message = {:response, "task-999", "data"}
# {:response, ^expected_id, result} = wrong_message
# ** (MatchError) - "task-999" doesn't match "task-001"
:skipped_to_avoid_error
5.4 Why Pin Matters for Agents
# Scenario: You sent a task and are waiting for its specific response
sent_task_id = "abc-123"
incoming_messages = [
{:response, "xyz-789", "wrong task"},
{:response, "abc-123", "correct response!"},
{:response, "def-456", "another task"}
]
# Find the response matching our task
Enum.find(incoming_messages, fn
{:response, ^sent_task_id, _result} -> true
_ -> false
end)
Section 6: Pattern Matching in Practice
6.1 Case Expressions
case uses pattern matching to choose a code path:
message = {:task, "001", %{action: :search, query: "Elixir tutorials"}}
case message do
{:task, id, %{action: :search, query: q}} ->
"Searching for '#{q}' (task #{id})"
{:task, id, %{action: :summarize}} ->
"Summarizing content (task #{id})"
{:response, id, result} ->
"Got response for #{id}: #{inspect(result)}"
_ ->
"Unknown message type"
end
6.2 Multiple Patterns in One Case
result = {:error, :timeout}
case result do
{:ok, value} ->
"Success: #{inspect(value)}"
{:error, :not_found} ->
"Item not found"
{:error, :timeout} ->
"Operation timed out, will retry"
{:error, reason} ->
"Error: #{inspect(reason)}"
end
6.3 Guards Add Extra Conditions
value = 15
case value do
x when x < 0 -> "negative"
x when x == 0 -> "zero"
x when x > 0 and x < 10 -> "small positive"
x when x >= 10 -> "large positive"
end
Section 7: Hands-On Exercises
Exercise 1: Extract Agent Info
Given an agent map, extract the name and current state:
agent = %{
name: "Analyzer",
state: :processing,
inbox: [{:task, "001", %{}}],
memory: %{last_action: :search}
}
# Your solution: use pattern matching to extract name and state
%{name: name, state: state} = agent
{name, state}
Exercise 2: Process Different Message Types
Write a case expression that handles three message types:
# Test with different messages
message = {:task, "t-001", %{action: :analyze, data: [1,2,3]}}
# message = {:response, "t-001", {:ok, "analysis complete"}}
# message = {:error, "t-001", :timeout}
case message do
{:task, id, payload} ->
"Processing task #{id} with action: #{payload.action}"
{:response, id, {:ok, result}} ->
"Task #{id} succeeded: #{result}"
{:response, id, {:error, reason}} ->
"Task #{id} failed: #{reason}"
{:error, id, reason} ->
"Error on task #{id}: #{reason}"
other ->
"Unknown message: #{inspect(other)}"
end
Exercise 3: Find Matching Response
Given a list of responses, find the one matching a specific task ID:
waiting_for = "task-42"
responses = [
{:response, "task-10", {:ok, "data1"}},
{:response, "task-42", {:ok, "this is the one!"}},
{:response, "task-99", {:error, :failed}}
]
# Use Enum.find with pattern matching and pin operator
Enum.find(responses, fn
{:response, ^waiting_for, _} -> true
_ -> false
end)
Exercise 4: Destructure Nested Message
Extract the action and query from this nested message:
message = {:task, "search-001", %{
action: :web_search,
params: %{
query: "Elixir GenServer tutorial",
max_results: 10
},
priority: :high
}}
# Destructure to get action and query
{:task, _id, %{action: action, params: %{query: query}}} = message
{action, query}
Summary
| Concept | Syntax | Purpose |
|---|---|---|
| Match operator |
= |
Make left equal right by binding variables |
| Tuple match |
{a, b} = {1, 2} |
Extract tuple elements |
| List head/tail | [h | t] = list | Split list into first and rest |
| Map match | %{key: v} = map | Extract map values (subset) |
| Wildcard | _ | Ignore a value |
| Pin operator | ^var | Match existing value, don’t rebind |
| Case | case x do ... end | Branch based on pattern |
Key Takeaways for Agent Framework
-
Message routing: Use
casewith pattern matching to route different message types -
Response correlation: Use
^pin operator to match responses to sent tasks - Data extraction: Destructure payloads to get exactly what you need
-
Error handling: Match
{:ok, result}and{:error, reason}patterns
Next Session
Session 3: Functions & Pipe Operator - Define functions with pattern matching in their heads, and chain operations elegantly with |>.