Pattern matching
Pattern matching is first-class in Haskell — pervasive, integrated with the type system, and the principal mechanism for working with algebraic data types. Patterns appear in function definitions, case expressions, let/where bindings, do-notation bindings, and lambda parameters. The pattern grammar is rich: literals, variables, wildcards, constructors, as-patterns, irrefutable patterns, and (with extensions) view patterns and pattern synonyms. Combined with the language’s algebraic data types, the mechanism subsumes much of what other languages express through if/switch, type tests, and visitor patterns.
This page covers the pattern grammar, the contexts where patterns appear, exhaustiveness, and the conventions for using each form. The deeper relationship between patterns and ADTs is in Algebraic data types.
The pattern grammar
A pattern is matched against a value; if the match succeeds, any variables in the pattern are bound to the corresponding parts of the value. The principal pattern forms:
| Pattern | Example | Matches |
|---|---|---|
| Literal | 0, 'a', "hello", True | The exact value |
| Variable | x, name | Anything; binds to the variable |
| Wildcard | _ | Anything; binds nothing |
| Constructor | Just x, Cons h t, (a, b) | Values of the matching constructor |
| Empty list | [] | The empty list |
| Cons | (x:xs) | A non-empty list (x is the head, xs is the tail) |
| List literal | [a, b, c] | A list of exactly three elements |
| Tuple | (a, b, c) | A tuple of the matching arity |
| As-pattern | all@(x:_) | Like the inner pattern, but binds the whole to all |
| Irrefutable | ~(x, y) | Lazily; never fails (extensions / advanced) |
Patterns nest: each component of a constructor pattern may itself be a pattern, recursively.
Patterns in function definitions
The conventional form. Multiple equations match against the function’s arguments:
factorial :: Int -> Int
factorial 0 = 1
factorial n = n * factorial (n - 1)
length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs
describe :: Maybe Int -> String
describe Nothing = "no value"
describe (Just n) = "value: " ++ show n
unzip :: [(a, b)] -> ([a], [b])
unzip [] = ([], [])
unzip ((a, b):rest) =
let (as, bs) = unzip rest
in (a:as, b:bs)
Each equation is tried top-to-bottom; the first matching equation produces the result. The compiler emits a warning if the patterns are not exhaustive (a value exists that no equation matches).
Patterns in case expressions
The general pattern-matching expression:
classify :: Either String Int -> String
classify x = case x of
Left err -> "error: " ++ err
Right n
| n > 0 -> "positive: " ++ show n
| n < 0 -> "negative: " ++ show n
| otherwise -> "zero"
Each arm is <pattern> -> <expression>. Arms may have guards (the |-prefixed conditions). The arms are tried top-to-bottom; the first matching arm’s expression is the result.
case is the lower-level mechanism; function-equation patterns desugar to case. For multi-equation pattern matching, the function form is conventional; for inline matching against derived values, case is appropriate.
Patterns in let and where
Bindings admit patterns:
main = do
line <- getLine
let (key, value) = break (== '=') line
-- key and value are bound from the tuple result
putStrLn ("Key: " ++ key)
putStrLn ("Value: " ++ drop 1 value)
quadratic :: Double -> Double -> Double -> Double -> (Double, Double)
quadratic a b c x = (root1, root2)
where
root1 = (-b + sqrt disc) / (2 * a)
root2 = (-b - sqrt disc) / (2 * a)
disc = b * b - 4 * a * c
The (key, value) = ... is a pattern binding. If the pattern fails to match (e.g., the right-hand side isn’t a tuple), a runtime error results. The pattern must be irrefutable in let/where for safety in lazy contexts; conventional irrefutable patterns are tuples and single-constructor types.
Patterns in do blocks
The <- admits monadic bindings with patterns:
main = do
line <- getLine
let (key, value) = break (== '=') line
putStrLn key
main = do
Just user <- lookupUser id -- pattern-matching <-
process user -- bound user is the contents of Just
If the pattern fails, the MonadFail instance’s fail is invoked — for IO, this throws an exception; for Maybe, it produces Nothing; for lists, it produces [].
The conventional discipline is to prefer explicit case for non-trivial monadic patterns; the <- with a non-trivial pattern is occasionally surprising in failure handling.
Patterns in lambda
A lambda may have a pattern:
addPair :: (Int, Int) -> Int
addPair = \(x, y) -> x + y
unwrap :: Maybe a -> a
unwrap = \(Just x) -> x -- partial; fails on Nothing
For multi-argument lambdas, each argument is a separate pattern:
zipped :: [a] -> [b] -> [(a, b)]
zipped = \xs ys -> case (xs, ys) of
([], _) -> []
(_, []) -> []
(x:xs, y:ys) -> (x, y) : zipped xs ys
The LambdaCase extension admits a pattern-matching lambda directly:
{-# LANGUAGE LambdaCase #-}
describe :: Maybe Int -> String
describe = \case
Nothing -> "no value"
Just n -> "value: " ++ show n
Wildcard _
The wildcard pattern _ matches anything but binds nothing:
fst :: (a, b) -> a
fst (x, _) = x
snd :: (a, b) -> b
snd (_, y) = y
const :: a -> b -> a
const x _ = x
The wildcard is the conventional way to indicate “I don’t care about this part” — the compiler emits a warning if a parameter is unused, and the wildcard suppresses the warning.
A name beginning with _ similarly suppresses the unused-variable warning while admitting the variable’s use:
verbose :: Int -> String
verbose _n = "ok" -- _n acts like _ but is named
As-patterns
An as-pattern binds the whole value while also matching against a sub-pattern:
duplicate :: [a] -> [a]
duplicate [] = []
duplicate all@(x:_) = x : all
-- duplicate [1, 2, 3] = 1 : [1, 2, 3] = [1, 1, 2, 3]
The pattern all@(x:_) matches a non-empty list, binds x to the head, and binds all to the whole list. The conventional uses are:
- Reusing the whole value: avoid reconstructing
(x:xs)when the original is already there. - Documentation: the named binding makes the structure explicit.
Irrefutable patterns
A pattern may be made irrefutable with ~:
take :: Int -> [a] -> [a]
take 0 _ = []
take n ~(x:xs) = x : take (n - 1) xs -- ~ defers the match
The irrefutable pattern postpones the actual match until the variables are used. The example admits take 0 _ to work on infinite lists or undefined inputs because the second argument’s structure is not actually examined when the result is [].
Irrefutable patterns are rare in routine code; they admit certain advanced patterns (e.g., always-succeed pattern matching for performance reasons) that the conventional eager matching does not.
The let and where pattern bindings are implicitly irrefutable — they postpone the match to admit lazy evaluation:
let (a, b) = computeBoth in foo a -- b is never forced if foo doesn't use it
View patterns and pattern synonyms
The ViewPatterns extension admits matching against the result of a function call:
{-# LANGUAGE ViewPatterns #-}
f :: [Int] -> Int
f (sort -> (x:_)) = x -- match the head of the sorted list
f [] = 0
length :: String -> Int
length (T.unpack -> s) = Prelude.length s -- view a Text as a String
The (<function> -> <pattern>) applies the function to the value and matches the result against the pattern. The mechanism is rare in routine code but admits substantial flexibility for libraries.
The PatternSynonyms extension admits abstracting patterns:
{-# LANGUAGE PatternSynonyms #-}
pattern Empty :: [a]
pattern Empty = []
pattern Cons :: a -> [a] -> [a]
pattern Cons x xs = x : xs
pattern Singleton :: a -> [a]
pattern Singleton x = [x]
After the synonyms, code can write Cons x xs instead of (x:xs) and Empty instead of []. The mechanism is principally useful for libraries that want to expose abstract patterns over private representations.
Exhaustiveness checking
The compiler tracks which patterns have been matched and warns when a function or case expression is non-exhaustive:
classify :: Direction -> String
classify North = "north"
classify South = "south"
-- warning: incomplete patterns; East and West are not handled
The warning is enabled by -Wall (typically on for new code). The conventional discipline is to handle every case explicitly or to add a default arm.
For sealed-set types like enumerations and small ADTs, exhaustive matching catches missing cases. For open types (an extensible set), a wildcard or default arm is the conventional fallback.
GHC supports several refinement extensions:
OverloadedLists,OverloadedStrings: literal patterns admit polymorphism.ViewPatterns,PatternSynonyms: as above.BangPatterns: strict patterns (treated in Laziness).
Pattern matching as the conditional mechanism
Most Haskell discrimination is pattern matching rather than explicit if/else:
-- Imperative-style (less idiomatic):
greet :: Maybe String -> String
greet name = if name == Nothing then "Hello, stranger." else "Hello, " ++ fromJust name ++ "."
-- Pattern-matching (idiomatic):
greet :: Maybe String -> String
greet Nothing = "Hello, stranger."
greet (Just name) = "Hello, " ++ name ++ "."
The pattern-matching form is more declarative — each case is a separate equation, the binding of name is local to its case, the fromJust is unnecessary. The conventional contemporary advice is to use pattern matching for ADT discrimination and if/else for boolean discrimination.
Common patterns
Destructuring records
data Person = Person
{ personName :: String
, personAge :: Int
}
greet :: Person -> String
greet (Person { personName = n }) = "Hello, " ++ n
-- or, with RecordWildCards:
{-# LANGUAGE RecordWildCards #-}
greet :: Person -> String
greet (Person {..}) = "Hello, " ++ personName
-- or the OverloadedRecordDot extension (Java/C# syntax):
{-# LANGUAGE OverloadedRecordDot #-}
greet :: Person -> String
greet p = "Hello, " ++ p.personName
Multi-tuple destructuring
swap :: (a, b) -> (b, a)
swap (x, y) = (y, x)
curry :: ((a, b) -> c) -> a -> b -> c
curry f x y = f (x, y)
uncurry :: (a -> b -> c) -> (a, b) -> c
uncurry f (x, y) = f x y
Recursive ADT processing
data Tree a = Leaf | Node (Tree a) a (Tree a)
depth :: Tree a -> Int
depth Leaf = 0
depth (Node l _ r) = 1 + max (depth l) (depth r)
flatten :: Tree a -> [a]
flatten Leaf = []
flatten (Node l x r) = flatten l ++ [x] ++ flatten r
Guards with patterns
absolute :: Int -> Int
absolute n
| n < 0 = -n
| otherwise = n
classify :: (Int, Int) -> String
classify (x, y)
| x == 0 && y == 0 = "origin"
| x == 0 = "y axis"
| y == 0 = "x axis"
| otherwise = "elsewhere"
The combination of constructor patterns and guards admits substantial discrimination in one expression.
Case with constructor patterns
import Data.Maybe (fromMaybe)
renderError :: Either String Int -> String
renderError (Right n) = show n
renderError (Left msg) = "error: " ++ msg
main = do
line <- getLine
case reads line :: [(Int, String)] of
[(n, "")] -> print n
_ -> putStrLn "could not parse"
Compact match with \case
{-# LANGUAGE LambdaCase #-}
import Data.List (find)
findPositive :: [Int] -> Maybe Int
findPositive = find (\case
n | n > 0 -> True
_ -> False)
The \case admits a compact lambda over discriminating cases.
A note on the limits of pattern matching
Haskell’s pattern matching is the principal discrimination mechanism but has limits:
- Exhaustiveness checking is conservative: GADTs and refinement types may produce false-positive non-exhaustive warnings.
- Patterns cannot match against arbitrary expressions: only structural patterns (constructors, literals) and view patterns admit dynamic matching.
- No relational patterns: there is no
case n of < 0 -> "neg"; _ -> "non-neg"; use guards instead. - No or-patterns: the syntax
case x of A | B -> ...is not admitted; either repeat the body or use a guard.
For the cases pattern matching cannot express directly, guards and the case/if combination are the conventional fallback. The combination covers the substantial majority of discrimination needs; the cases that remain are typically also cases where the language’s type system would benefit from refinement-type extensions.