Pattern Matching Fundamentals
Pattern matching is how you take apart data in Haskell. Other languages give you accessors, conditionals, and a switch statement. Haskell unifies all of that into one mechanism: you describe the shape of the data you're expecting, and the compiler arranges for the right code to run with the right pieces bound to names.
If you've used pattern matching in Rust, OCaml, Scala, or modern Python, you have the basic feel. Haskell's version is the most pervasive of the lot — almost every function is defined by pattern matching at some level.
The Dual of Construction
Here's the framing that makes pattern matching click. When you write Just 42, you're constructing a Maybe Int by applying the Just constructor to the value 42. When you write the pattern Just x, you're deconstructing — saying "if this value was built by applying Just to something, bind that something to the name x."
Construction and pattern matching are mirror operations. Whatever you can build with constructors, you can take apart with patterns.
-- construction
example :: Maybe Int
example = Just 42
-- destruction
extract :: Maybe Int -> Int
extract (Just x) = x
extract Nothing = 0
This dual relationship is why pattern matching feels natural in Haskell — it's the inverse of the operation you used to make the data in the first place.
Patterns in Function Definitions
The most common place you'll see pattern matching is on the left side of a function definition. You write multiple equations, each with a different pattern, and Haskell tries them in order top to bottom.
-- factorial via pattern matching on Int
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)
The first equation matches when the argument is exactly 0. The second matches anything else, binding it to n. This is the same as a base-case-and-recursive-case formulation.
Be careful with this exact pattern — it loops forever on negative inputs. A more defensive version:
factorial :: Int -> Int
factorial n
| n < 0 = error "factorial of negative"
| n == 0 = 1
| otherwise = n * factorial (n - 1)
We'll cover guards on the next page. For now, the pattern-matching idea: each equation is tried in order, the first matching one runs.
Matching on Tuples
Tuples are pattern-matched by their structure:
swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)
distance :: (Double, Double) -> (Double, Double) -> Double
distance (x1, y1) (x2, y2) = sqrt ((x2-x1)^2 + (y2-y1)^2)
The pattern (x, y) matches any pair, binding the first element to x and the second to y. Because tuple pattern matching is so common, you'll see this everywhere.
You can ignore parts of a tuple with the wildcard _:
fst' :: (a, b) -> a
fst' (x, _) = x
snd' :: (a, b) -> b
snd' (_, y) = y
These are already in the standard library as fst and snd.
Matching on Lists
Lists in Haskell are built from two constructors: [] (the empty list) and : (cons, which prepends an element to a list). Every pattern that takes apart a list uses one of these forms.
-- the empty list
isEmpty :: [a] -> Bool
isEmpty [] = True
isEmpty (_:_) = False
-- the head of a non-empty list
firstElement :: [a] -> Maybe a
firstElement [] = Nothing
firstElement (x:_) = Just x
-- recursive: head and tail
length' :: [a] -> Int
length' [] = 0
length' (_:xs) = 1 + length' xs
The pattern (x:xs) reads as "an element x cons'd onto the rest, xs." The convention x:xs (singular for the head, plural for the tail) is so universal that you'll write it without thinking after a week.
Lists can also be matched by exact structure:
exactlyThree :: [a] -> Bool
exactlyThree [_, _, _] = True
exactlyThree _ = False
firstThree :: [a] -> Maybe (a, a, a)
firstThree (x:y:z:_) = Just (x, y, z)
firstThree _ = Nothing
The pattern [a, b, c] is sugar for a : b : c : [] — it requires exactly three elements. The pattern (x:y:z:_) requires at least three. The wildcard at the end says "and possibly more."
A common mistake when starting out: writing firstThree (x:y:z) and expecting it to take three elements. That pattern actually means "an element x followed by an element y followed by a list z" — the third element ends up being the rest of the list, not the third element itself. Patterns have to be structurally correct, not just look right.
Matching on Records
Records can be matched in several ways. The positional form treats the record like a regular product type:
data User = User { name :: String, email :: String, age :: Int }
deriving Show
greet :: User -> String
greet (User n _ _) = "Hello, " ++ n
This works but is brittle — if you add a field, every pattern breaks. The record syntax is more robust:
greet :: User -> String
greet (User { name = n }) = "Hello, " ++ n
This binds name to n and ignores the rest. It tolerates new fields without modification. With the RecordWildCards extension, you can pull all fields into scope at once:
{-# LANGUAGE RecordWildCards #-}
introduce :: User -> String
introduce (User {..}) = name ++ " (" ++ email ++ ")"
-- name and email are in scope here
RecordWildCards is divisive in Haskell. Some teams love it for the ergonomics. Others ban it because it makes it harder to tell where names come from. Mercury and similar production codebases have had this debate. There's no universal answer; pick a convention and stick to it.
Matching on Constructors
The general form: when you have a sum type, you match by constructor.
data Shape = Circle Double | Rectangle Double Double | Triangle Double Double
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rectangle w h) = w * h
area (Triangle b h) = 0.5 * b * h
Each equation handles one constructor. With -Wall enabled, GHC will warn you if you miss a case. This is a major correctness lever — adding a new constructor and forgetting to update a pattern match becomes a compile-time warning, not a runtime crash.
-- if we add Pentagon Double Int and forget to update area:
data Shape = Circle Double | Rectangle Double Double | Triangle Double Double | Pentagon Double Int
-- GHC: "Pattern match(es) are non-exhaustive
-- In an equation for 'area': Patterns not matched: (Pentagon _ _)"
This is one of the headline benefits of ADTs combined with pattern matching. It's how Standard Chartered makes sweeping changes to financial models without fearing missed cases.
The Wildcard
_ matches anything but doesn't bind a name. Use it when you don't care about a value or want to be explicit that you're discarding it.
firstOf3 :: (a, b, c) -> a
firstOf3 (x, _, _) = x
ignoreError :: Either a b -> Maybe b
ignoreError (Left _) = Nothing
ignoreError (Right b) = Just b
The wildcard does not produce an "unused variable" warning, which is one of the reasons to prefer it over a named variable you're going to ignore.
You can also use wildcards as part of a larger pattern — the example (_:xs) matches a non-empty list whose head we don't care about.
The As-Pattern
Sometimes you want to match a pattern and keep a reference to the whole thing. The as-pattern, written name@pattern, does this:
duplicate :: [a] -> [a]
duplicate [] = []
duplicate all@(x:_) = x : all
-- "all" refers to the whole list, "x" refers to its first element
ghci> duplicate [1, 2, 3]
[1, 1, 2, 3]
You'd otherwise have to reconstruct the list with (x:xs) -> x : (x:xs), which both repeats the work and creates an extra allocation. The as-pattern keeps the original around.
A more practical example. Suppose you're processing a list of events and want to flag the first error along with everything that came after it:
data Event = Ok Int | Err String
splitAtError :: [Event] -> ([Event], [Event])
splitAtError [] = ([], [])
splitAtError rest@(Err _ : _) = ([], rest)
splitAtError (e : es) = let (before, after) = splitAtError es
in (e : before, after)
The middle equation matches when the head is an error and binds rest to the entire remaining list including the error.
Why Pattern Matching Beats Conditionals
Compare these two ways of writing the same function:
-- with conditionals
safeDivide :: Double -> Double -> Maybe Double
safeDivide x y = if y == 0
then Nothing
else Just (x / y)
-- with pattern matching (over a richer type)
data Divisor = Zero | NonZero Double
safeDivide :: Double -> Divisor -> Double
safeDivide _ Zero = error "this is impossible — caller can't pass Zero meaningfully"
safeDivide x (NonZero y) = x / y
The second isn't strictly better here, but it illustrates the principle: pattern matching forces you to engage with the cases. Conditionals are easy to mis-nest, easy to forget a case, and don't get checked for exhaustiveness. Pattern matching does.
This is why idiomatic Haskell tends to make illegal cases unrepresentable through types and exhaust them through pattern matching, rather than guarding against them with if.
Common Pitfalls
Non-exhaustive patterns. If your patterns don't cover every case, the program will throw an exception at runtime when it hits a value that doesn't match. Always enable -Wall so you get warnings about this.
Pattern order. Equations are tried top to bottom. A more general pattern at the top will shadow more specific ones below. Put specific cases first, general cases last.
-- WRONG: the wildcard catches everything before later equations run
classify :: Int -> String
classify _ = "unknown"
classify 0 = "zero" -- never reached
classify 1 = "one" -- never reached
Missing the empty case in list recursion. A common bug:
-- this crashes on empty lists
firstElement :: [a] -> a
firstElement (x:_) = x
-- forgot: firstElement [] = ???
There's no good answer for the empty case if you're returning a, which is why head from the standard library is partial and modern code uses Maybe a instead.
Over-using positional record patterns. User n _ _ breaks when you add a field. Use record-syntax patterns or RecordWildCards for stable code.
Confusing [x, y] with (x:y). The first matches a list of exactly two elements. The second matches a non-empty list with x as its head and y as the rest of the list (which is itself a list).
Key Takeaways
Pattern matching is the dual of construction. Anything you can build with constructors, you can take apart with patterns. The most common patterns: tuples (x, y), lists (x:xs) and [], constructors Just x, records User { name = n }, the wildcard _, and the as-pattern name@pattern. Multi-equation function definitions are a clean way to handle each case separately. Always enable -Wall to catch non-exhaustive patterns at compile time.