Polyglot
Languages Haskell pattern matching
Haskell § pattern-matching

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:

PatternExampleMatches
Literal0, 'a', "hello", TrueThe exact value
Variablex, nameAnything; binds to the variable
Wildcard_Anything; binds nothing
ConstructorJust 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-patternall@(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.