Pattern Matching
What Is Pattern Matching?
Section titled “What Is Pattern Matching?”Pattern matching is a mechanism for checking data against a pattern and deconstructing data into its components. It is one of the most powerful features in Haskell, enabling concise and expressive code that directly reflects the structure of data types.
When you write a function definition with multiple equations, each equation has a pattern on the left side. The compiler matches the argument against these patterns in order, executing the right side of the first matching equation.
Matching on Literals
Section titled “Matching on Literals”Literals are the simplest patterns — they match specific constant values:
-- Matching on integer literalsisZero :: Int -> StringisZero 0 = "zero"isZero _ = "not zero"
-- Matching on character literalsvowel :: Char -> Boolvowel "a' = Truevowel 'e' = Truevowel 'i' = Truevowel 'o' = Truevowel 'u' = Truevowel _ = False
-- Matching on string literals (which are lists of characters)sayHello :: String -> StringsayHello "world" = "Hello, World!"sayHello "haskell" = "Hello, Haskell!"sayHello _ = "Hello, stranger!"Variable Patterns and Wildcards
Section titled “Variable Patterns and Wildcards”Variable Patterns
Section titled “Variable Patterns”A variable pattern matches anything and binds the matched value to that variable:
-- 'x' matches any value and binds itdescribe :: Int -> Stringdescribe x = "The number is " ++ show x
-- 'xs' matches any list and binds itlistLength :: [a] -> IntlistLength xs = length xsWildcard Pattern
Section titled “Wildcard Pattern”The wildcard _ matches anything but does not bind the value. It signals that the matched value is not needed:
-- Using wildcard for the second element of a pairfirstOf :: (a, b) -> afirstOf (a, _) = a
-- Using wildcard for unused partsthird :: (a, b, c) -> cthird (_, _, c) = c
-- Wildcard as catch-allclassify :: Int -> Stringclassify 0 = "zero"classify 1 = "one"classify _ = "other"Important: Variable vs Wildcard
Section titled “Important: Variable vs Wildcard”-- Variable pattern: binds the value-- Using 'x' in multiple equations means DIFFERENT bindingsf True = show x -- ERROR: x is not in scope heref False = show x -- ERROR: x is not in scope here
-- Correct: wildcard or different variablesg True = "true"g False = "false"
-- A variable 'x' in a pattern binds the whole valueh :: (Int, Int) -> Inth (x, _) = x -- x is bound to the first elementh (_, x) = x -- x is bound to the second element-- These are DIFFERENT equations with different bindingsConstructor Patterns
Section titled “Constructor Patterns”Matching on Data Constructors
Section titled “Matching on Data Constructors”Data constructors are the primary mechanism for pattern matching in Haskell. Each constructor defines a shape that can be matched:
data Bool = False | True
data Maybe a = Nothing | Just a
isJust :: Maybe a -> BoolisJust (Just _) = TrueisJust Nothing = False
fromMaybe :: a -> Maybe a -> afromMaybe def Nothing = deffromMaybe _ (Just x) = xNested Constructor Patterns
Section titled “Nested Constructor Patterns”Patterns can be nested arbitrarily deep:
data Tree a = Leaf a | Branch (Tree a) (Tree a)
-- Count leaves in a treecountLeaves :: Tree a -> IntcountLeaves (Leaf _) = 1countLeaves (Branch l r) = countLeaves l + countLeaves r
-- Check if a value exists in a treecontains :: Eq a => a -> Tree a -> Boolcontains x (Leaf y) = x == ycontains x (Branch l r) = contains x l || contains x r
-- Sum of all leaf valuestreeSum :: Num a => Tree a -> atreeSum (Leaf x) = xtreeSum (Branch l r) = treeSum l + treeSum rMatching on Tuples
Section titled “Matching on Tuples”Tuples have a fixed structure that can be deconstructed in patterns:
-- Destructuring a pairswap :: (a, b) -> (b, a)swap (a, b) = (b, a)
-- Extracting componentsgetName :: (String, Int, String) -> StringgetName (name, _, _) = name
getAge :: (String, Int, String) -> IntgetAge (_, age, _) = age
-- Nested tuple destructuringinnerFirst :: ((a, b), c) -> ainnerFirst ((a, _), _) = a
-- Using tuple patterns in list comprehensionspairs :: [(Int, Int)]pairs = [(x, y) | x <- [1..3], y <- [1..3], x /= y]-- => [(1,2),(1,3),(2,1),(2,3),(3,1),(3,2)]Matching on Lists
Section titled “Matching on Lists”List Constructors
Section titled “List Constructors”Lists are built from two constructors: [] (empty list) and : (cons — prepend an element to a list). Pattern matching on lists uses these constructors:
-- Empty listisEmpty :: [a] -> BoolisEmpty [] = TrueisEmpty _ = False
-- Non-empty list: x is head, xs is tailheadOrDefault :: a -> [a] -> aheadOrDefault def [] = defheadOrDefault _ (x:_) = x
-- Matching the first two elementssecond :: [a] -> asecond (_:x:_) = x
-- Matching exactly two elementspairToList :: (a, a) -> [a]pairToList (a, b) = [a, b]
-- Counting length via pattern matchingmyLength :: [a] -> IntmyLength [] = 0myLength (_:xs) = 1 + myLength xsCommon List Patterns
Section titled “Common List Patterns”-- Singleton listisSingleton :: [a] -> BoolisSingleton [_] = TrueisSingleton _ = False
-- Exactly three elementsfirstOfThree :: [a] -> afirstOfThree (x:_) = x
-- Splitting at a specific positionsplitAtTwo :: [a] -> ([a], [a])splitAtTwo (a:b:rest) = ([a, b], rest)splitAtTwo xs = (xs, [])
-- Matching a range of elementsstartsWith :: Eq a => [a] -> [a] -> BoolstartsWith [] _ = TruestartsWith _ [] = FalsestartsWith (x:xs) (y:ys) = x == y && startsWith xs ysAs-Patterns
Section titled “As-Patterns”The as-pattern (@) binds the entire matched value while also allowing deconstruction:
-- Without as-pattern: we lose access to the originalsumFirstTwo :: Num a => [a] -> asumFirstTwo (x:y:_) = x + y
-- With as-pattern: we keep the whole list and also its partssumAndKeep :: Num a => [a] -> (a, [a])sumAndKeep xs@(_:_:_) = (head xs + head (tail xs), xs)sumAndKeep xs = (0, xs)
-- More practical examplecapitaliseFirst :: String -> StringcapitaliseFirst [] = []capitaliseFirst s@(c:cs) = toUpper c : cs-- 's' gives us the whole string, 'c' and 'cs' give us head and tail
-- Transforming while keeping the originalfirstAndRest :: [a] -> (a, [a])firstAndRest xs@(x:_) = (x, xs)firstAndRest [] = error "empty list"
-- Checking a property of the whole while deconstructinglongEnough :: Int -> String -> Maybe StringlongEnough n s@(c:cs) | length s >= n = Just s | otherwise = NothinglongEnough _ [] = NothingCase Expressions
Section titled “Case Expressions”Case expressions allow pattern matching anywhere, not just in function definitions:
-- Case expression: match on any valuedescribeNumber :: Int -> StringdescribeNumber n = case n of 0 -> "zero" 1 -> "one" _ -> "other"
-- Case is useful inside other expressionsaddOrDouble :: Maybe Int -> IntaddOrDouble mx = case mx of Nothing -> 0 Just x -> x + x
-- Nested case expressionsclassifyPair :: (Int, Int) -> StringclassifyPair (a, b) = case (a, b) of (0, 0) -> "origin" (0, _) -> "on y-axis" (_, 0) -> "on x-axis" _ -> case compare a b of LT -> "below diagonal" EQ -> "on diagonal" GT -> "above diagonal"Case vs Function Equations
Section titled “Case vs Function Equations”-- These are equivalent:
-- Using multiple equationsfactorial :: Integer -> Integerfactorial 0 = 1factorial n = n * factorial (n - 1)
-- Using case expressionfactorial :: Integer -> Integerfactorial n = case n of 0 -> 1 _ -> n * factorial (n - 1)
-- Case is needed when the value to match comes from-- an expression, not a function argumentprocessPair :: (Int, Int) -> IntprocessPair (a, b) = case a + b of result | result > 10 -> result * 2 | result > 5 -> result | otherwise -> 0Pattern Guards
Section titled “Pattern Guards”Pattern guards refine pattern matching with boolean conditions:
-- Regular guardsclassify xs | null xs = "empty" | length xs > 5 = "long" | otherwise = "short"
-- Pattern guards: combine patterns with boolean conditions-- Requires PatternGuards extension (enabled by default in GHC)sortPair :: Ord a => (a, a) -> (a, a)sortPair p | (a, b) <- p, a <= b = (a, b) | (a, b) <- p, a > b = (b, a)
-- More useful example: parsing a commandparseCommand :: String -> Maybe (String, String)parseCommand s | (cmd, ': ":args) <- break (== '':") s, not (null cmd) = Just (cmd, args) | otherwise = NothingPattern Matching in Let and Where
Section titled “Pattern Matching in Let and Where”Let Patterns
Section titled “Let Patterns”-- Destructuring in letfirstAndSecond :: [a] -> (a, a)firstAndSecond xs = let (x:y:_) = xs in (x, y)
-- Using let with MaybeprocessMaybe :: Maybe (Int, String) -> StringprocessMaybe m = let (Just (n, s)) = m -- partial! crashes on Nothing in show n ++ ": " ++ s
-- Safe version using caseprocessMaybeSafe :: Maybe (Int, String) -> StringprocessMaybeSafe m = case m of Just (n, s) -> show n ++ ": " ++ s Nothing -> "no data"Where Patterns
Section titled “Where Patterns”-- Where clauses can also use pattern matching-- (though this is less common)bmiTell :: Double -> Double -> Double -> StringbmiTell weight height bmi | bmi < 18.5 = "underweight" | bmi < 25.0 = "normal" | otherwise = "overweight" where bmi = weight / height ^ 2
-- Pattern matching in wheresumFirstTwo :: (Int, Int, Int) -> IntsumFirstTwo triple = a + b where (a, b, _) = tripleExhaustive Matching
Section titled “Exhaustive Matching”Non-Exhaustive Patterns
Section titled “Non-Exhaustive Patterns”When patterns do not cover all possible values, the compiler warns (with -Wall) and the program may crash at runtime:
-- Non-exhaustive: what happens with Nothing?unsafeHead :: Maybe a -> aunsafeHead (Just x) = x-- GHC warning: Pattern match(es) are non-exhaustive-- Runtime error on Nothing: *** Exception: ...Making Patterns Exhaustive
Section titled “Making Patterns Exhaustive”-- Add a catch-all wildcardsafeHead :: Maybe a -> Maybe asafeHead (Just x) = Just xsafeHead Nothing = Nothing
-- Or use Maybe explicitlysafeHead2 :: Maybe a -> Maybe asafeHead2 Nothing = NothingsafeHead2 (Just x) = Just xExhaustiveness and Custom Types
Section titled “Exhaustiveness and Custom Types”data Direction = North | South | East | West
-- Exhaustive: covers all constructorsopposite :: Direction -> Directionopposite North = Southopposite South = Northopposite East = Westopposite West = East
-- With -Wall, GHC warns if any constructor is missingOverlapping Patterns
Section titled “Overlapping Patterns”Patterns are matched top to bottom. More specific patterns should come before general ones:
-- Correct: specific patterns firstdescribe :: Int -> Stringdescribe 0 = "zero"describe 1 = "one"describe 42 = "the answer"describe _ = "other"
-- This works the same but is less readable if specific cases-- are buried among general onesWith guards, order matters because the first True guard wins:
-- Order matters heregrade :: Int -> Chargrade score | score >= 90 = 'A' | score >= 80 = 'B' | score >= 70 = 'C' | score >= 60 = 'D' | otherwise = 'F'
-- This would be wrong if reordered:-- | score >= 60 = 'D'-- | score >= 90 = 'A' -- unreachable!View Patterns
Section titled “View Patterns”The ViewPatterns extension allows pattern matching through a function (a “view”):
{-# LANGUAGE ViewPatterns #-}
-- Instead of:process :: String -> Stringprocess s = case length s of 0 -> "empty" 1 -> "singleton" n | n > 10 -> "long" | otherwise -> "medium"
-- With view patterns:process :: String -> Stringprocess (length -> 0) = "empty"process (length -> 1) = "singleton"process (length -> n) | n > 10 = "long" | otherwise = "medium"
-- Custom view functionsisPositive :: Int -> Maybe IntisPositive n | n > 0 = Just n | otherwise = Nothing
safeDiv :: Int -> Int -> Maybe IntsafeDiv _ 0 = NothingsafeDiv x (isPositive -> Just y) = Just (x `div` y)safeDiv _ _ = NothingData vs Newtype
Section titled “Data vs Newtype”data Declaration
Section titled “data Declaration”The data keyword introduces a new algebraic data type. It creates a new type with new constructors:
-- data introduces a new type with runtime overhead-- (a wrapper is allocated on the heap)data Score = Score Int deriving (Show, Eq)
getScore :: Score -> IntgetScore (Score n) = n
-- Multiple constructorsdata Shape = Circle Double Double Double -- x, y, radius | Rectangle Double Double Double Double -- x, y, width, height | Triangle Double Double Double Double Double Double -- three points deriving (Show, Eq)newtype Declaration
Section titled “newtype Declaration”The newtype keyword creates a type that is identical to an existing type at runtime but is a distinct type at compile time. There is zero runtime overhead:
-- newtype has no runtime overhead-- It is erased during compilationnewtype UserId = UserId Int deriving (Show, Eq)
newtype Username = Username String deriving (Show, Eq, Eq)
-- These are different types -- you cannot mix them-- UserId 5 and Int 5 are not interchangeable-- This prevents bugs like passing a UserId where an Int is expecteddata vs newtype Comparison
Section titled “data vs newtype Comparison”-- data: can have multiple constructorsdata Maybe a = Nothing | Just a
-- newtype: exactly one constructor, one fieldnewtype Age = Age Int
-- data: constructor adds a layer of boxingdata Wrapper a = Wrapper a-- Wrapper x evaluates to Wrapper x (lazy in field)
-- newtype: no boxing, isomorphic to the wrapped typenewtype NewWrapper a = NewWrapper a-- NewWrapper x is identical to x at runtimeKey differences:
| Property | data | newtype |
|---|---|---|
| Constructors | One or more | Exactly one |
| Fields | Zero or more | Exactly one |
| Runtime overhead | Yes (heap allocation) | No (erased) |
| Strictness | Lazy by default | Lazy by default |
| Matching | Always matches | Always matches |
| deriving | Full support | Full support + GeneralizedNewtypeDeriving |
GeneralizedNewtypeDeriving
Section titled “GeneralizedNewtypeDeriving”{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype Score = Score Double deriving (Show, Eq, Ord, Num, Enum)
-- This gives Score all Num methods automatically-- (+), (*), (-), abs, signum, fromInteger all work
addScores :: Score -> Score -> ScoreaddScores a b = a + b
-- newtype deriving works because Score is isomorphic to Double-- The compiler directly coerces between Score and DoubleRecord Syntax
Section titled “Record Syntax”Records provide named fields for data types:
-- Basic record typedata Person = Person { personName :: String , personAge :: Int , personEmail :: String } deriving (Show, Eq)
-- Creating recordsalice :: Personalice = Person { personName = "Alice" , personAge = 30 , personEmail = "alice@example.com" }
-- Record field access (automatically generated)getName :: Person -> StringgetName p = personName p
-- Record field update (creates a copy)birthday :: Person -> Personbirthday p = p { personAge = personAge p + 1 }
-- Pattern matching on recordsgreet :: Person -> Stringgreet Person { personName = name, personAge = age } | age < 18 = "Hey " ++ name | otherwise = "Hello " ++ name
-- Record puns: when variable name matches field name-- With RecordWildCards extension{-# LANGUAGE RecordWildCards #-}
makeOlder :: String -> Int -> String -> PersonmakeOlder personName personAge personEmail = Person{..}-- All three fields are bound by their namesRecord Pattern Matching
Section titled “Record Pattern Matching”-- Matching specific fieldsisAdult :: Person -> BoolisAdult Person { personAge = age } = age >= 18
-- Matching multiple fieldscanVote :: Person -> BoolcanVote Person { personAge = age, personEmail = email } = age >= 18 && not (null email)
-- Wildcards for unneeded fieldsgetAge :: Person -> IntgetAge Person { personAge = age } = age-- Only match the age field; name and email are ignoredPattern Matching on Booleans
Section titled “Pattern Matching on Booleans”-- Simple boolean matchingabsolute :: Int -> Intabsolute n | n >= 0 = n | otherwise = -n
-- Using pattern matching directlyclassify :: Bool -> Stringclassify True = "yes"classify False = "no"
-- In case expressionsdescribe :: Bool -> Bool -> Stringdescribe a b = case (a, b) of (True, True) = "both true" (True, False) = "first true" (False, True) = "second true" (False, False) = "both false"Pattern Matching and Recursion
Section titled “Pattern Matching and Recursion”Pattern matching and recursion are deeply intertwined in Haskell:
-- Recursive pattern matching on listsmyMap :: (a -> b) -> [a] -> [b]myMap _ [] = []myMap f (x:xs) = f x : myMap f xs
-- Recursive pattern matching on treesdepth :: Tree a -> Intdepth (Leaf _) = 0depth (Branch l r) = 1 + max (depth l) (depth r)
-- Mutual recursion with pattern matchingisEven, isOdd :: Integral a => a -> BoolisEven 0 = TrueisEven n = isOdd (n - 1)isOdd 0 = FalseisOdd n = isEven (n - 1)
-- Tail-recursive with pattern matching and accumulatormyReverse :: [a] -> [a]myReverse = go [] where go acc [] = acc go acc (x:xs) = go (x : acc) xsIntuition
Section titled “Intuition”Pattern matching is structural sorting: Imagine a mailroom where each letter is checked against a template. The _ wildcard is the “miscellaneous” bin — anything that doesn’t fit a specific template goes there. Nested patterns are like checking the envelope, then the letter inside, then the signature — each layer deconstructs further. Case expressions are the decision tree: the compiler generates a fast lookup table from your patterns, checking the most specific ones first.
Why it matters: Pattern matching replaces defensive type-checking with structural verification. Instead of if (x is Just && x.value > 0) you write Just x | x > 0 — the structure is the logic, making impossible states unrepresentable.
The key insight: Haskell’s pattern matching is total — when you cover all constructors, the compiler guarantees no runtime crashes from unmatched cases. Use -Wall to enforce this discipline.
Pattern Matching Best Practices
Section titled “Pattern Matching Best Practices”- List the most specific patterns first: Patterns are matched top to bottom; a general pattern before specific ones will shadow them.
- Handle all constructors: Use
-Wallto catch non-exhaustive patterns. - Use wildcards
_for unneeded values: This makes the intent clear and avoids unused variable warnings. - Prefer data constructors over guards when the structure is being matched: Constructors make the structure explicit.
- Use newtype for type wrappers: No runtime overhead and clearer intent.
- Use
casewhen matching on computed values: Function equations match only on arguments. - Consider
-Wincomplete-patterns: GHC flag that turns incomplete pattern warnings into errors.
Cross-References
Section titled “Cross-References”- Types and Functions: Foundational type system and function composition used in pattern matching.
- Monads and Functors: Monadic pattern matching with do-notation and bind.
- Advanced Types: GADTs and phantom types that enable type-safe pattern matching.
Common Mistakes
Section titled “Common Mistakes”Writing non-exhaustive patterns: Missing a constructor in a pattern match causes runtime crashes. Always enable -Wall and add a catch-all _ pattern or handle every constructor.
Placing general patterns before specific ones: Patterns match top to bottom. A wildcard _ before a specific constructor makes the specific case unreachable, silently producing wrong results.
Using let patterns instead of case for partial matches: let (x:y:_) = xs crashes if xs has fewer than two elements. Use case with exhaustive patterns to handle all possibilities safely.