5 min read
On this page

Advanced Pattern Matching

Once you have basic patterns and guards, Haskell offers several power tools: view patterns for matching on computed values, pattern synonyms for naming common patterns, lazy and strict patterns for fine-grained evaluation control, and GADT pattern matching for type-level reasoning. You will not reach for these every day, but knowing they exist saves you from awkward workarounds when the basics are not enough.

View Patterns

A view pattern lets you apply a function before matching. Enable the extension and use the (f -> p) syntax:

{-# LANGUAGE ViewPatterns #-}

import Data.List (uncons)

firstWord :: String -> Maybe String
firstWord (words -> []) = Nothing
firstWord (words -> (w:_)) = Just w

Without view patterns, you would write:

firstWord :: String -> Maybe String
firstWord s = case words s of
  []    -> Nothing
  (w:_) -> Just w

View patterns shine when you want to match on a transformed view of the input without naming the transformation step. Common uses include matching on Map.lookup results, parseInt, or Data.Text.uncons:

import qualified Data.Map.Strict as Map

processConfig :: Map.Map String Int -> String
processConfig (Map.lookup "timeout" -> Just t) = "Timeout is " ++ show t
processConfig _ = "No timeout configured"

The tradeoff: view patterns can hide complexity. Reading them requires understanding both the pattern and the function. Use them when the gain in clarity is real.

Pattern Synonyms

Pattern synonyms let you give names to patterns. They are useful when a pattern is structurally complex but conceptually simple:

{-# LANGUAGE PatternSynonyms #-}

data Point = Point Int Int

-- Bidirectional synonym - works in patterns and as a constructor
pattern Origin :: Point
pattern Origin = Point 0 0

-- Now you can write
isOrigin :: Point -> Bool
isOrigin Origin = True
isOrigin _      = False

Pattern synonyms can also be unidirectional, matching but not constructing:

{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ViewPatterns #-}

import Data.Text (Text)
import qualified Data.Text as T

pattern Empty :: Text
pattern Empty <- (T.null -> True)

pattern NonEmpty :: Char -> Text -> Text
pattern NonEmpty c rest <- (T.uncons -> Just (c, rest))

describeText :: Text -> String
describeText Empty = "empty"
describeText (NonEmpty 'A' _) = "starts with A"
describeText _ = "other"

Production codebases use pattern synonyms sparingly. They are most valuable when wrapping libraries whose internal representation differs from how you want to think about them.

Lazy Patterns

A lazy pattern (with ~) does not force evaluation when matching. The match always succeeds without inspecting the value, but accessing a binding from the pattern forces it lazily:

firstAndSecond :: [Int] -> (Int, Int)
firstAndSecond ~(x:y:_) = (x, y)

Without ~, the pattern would fail on a list shorter than two elements. With ~, the function returns immediately, but accessing x or y on a too-short list throws a runtime error.

Lazy patterns are useful when you want to delay forcing the input until the result is actually used. The classic example is corecursive definitions:

fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

-- A pattern that needs to match before fibs has progressed enough
-- would deadlock without laziness

In practice, you rarely write ~ explicitly. You will encounter it when a tutorial demonstrates the corecursive pattern, but day-to-day Haskell code does not use it heavily.

Bang Patterns

A bang pattern (with !) forces strict evaluation. The pattern fails to match if the value is bottom (an error or non-termination):

{-# LANGUAGE BangPatterns #-}

sumStrict :: [Int] -> Int
sumStrict = go 0
  where
    go !acc [] = acc
    go !acc (x:xs) = go (acc + x) xs

Without the bang, acc would build up a chain of unevaluated thunks (0 + 1, 0 + 1 + 2, ...) before the final result is forced. With the bang, each iteration evaluates the accumulator immediately, keeping memory usage constant.

This is the canonical fix for space leaks in left folds. foldl' (with the strict tick) is just foldl with a bang on the accumulator under the hood.

Bang patterns also force record fields:

{-# LANGUAGE BangPatterns #-}

data Counter = Counter { count :: !Int, label :: String }

-- count is always evaluated when the Counter is built

You can mark fields strict with ! directly in the data declaration, or use the StrictData language extension to make every field strict by default:

{-# LANGUAGE StrictData #-}

data Point = Point Int Int  -- Both fields are now strict

Many production Haskell codebases turn on StrictData per-module. It eliminates a whole category of space leaks in domain types where laziness brings no benefit.

As-Patterns Revisited

as-patterns (@) bind a name to the entire matched value while also pattern matching its structure:

firstTwo :: [a] -> [a]
firstTwo all@(x:y:_) = [x, y]  -- 'all' is bound to the whole list
firstTwo xs = xs

This avoids reconstructing the list when you need both the whole value and its parts. It is more efficient and more readable than:

firstTwo :: [a] -> [a]
firstTwo (x:y:_) = [x, y]  -- Lost reference to original list
firstTwo xs = xs

GADTs and Pattern Matching (Briefly)

Generalized Algebraic Data Types let constructors carry type information that pattern matching can use. They are an advanced feature, but worth knowing about:

{-# LANGUAGE GADTs #-}

data Expr a where
  IntLit  :: Int -> Expr Int
  BoolLit :: Bool -> Expr Bool
  Add     :: Expr Int -> Expr Int -> Expr Int
  And     :: Expr Bool -> Expr Bool -> Expr Bool
  If      :: Expr Bool -> Expr a -> Expr a -> Expr a

eval :: Expr a -> a
eval (IntLit n) = n
eval (BoolLit b) = b
eval (Add a b) = eval a + eval b
eval (And a b) = eval a && eval b
eval (If c t e) = if eval c then eval t else eval e

The pattern match on IntLit n tells the type checker that the result type is Int, which is why eval can return both Int and Bool from the same function. This eliminates entire classes of "type-incorrect" expression trees at compile time.

You will not write GADTs your first year of Haskell, but you will see them in libraries like singletons, dependent-sum, and parsers like megaparsec (for state machines).

Exhaustiveness Checking

GHC checks that pattern matches cover all constructors. Compile with -Wincomplete-patterns (or -Wall) to see warnings:

data Color = Red | Green | Blue

-- Compiles, but GHC warns: "Pattern match(es) are non-exhaustive"
isWarm :: Color -> Bool
isWarm Red   = True
isWarm Green = False
-- forgot Blue!

In production code, -Wall -Werror is the standard. Missing patterns become compile errors, not runtime crashes. This is one of Haskell's strongest safety nets.

Common Pitfalls

Forgetting that view patterns are evaluated in order. Multiple view patterns in the same case re-evaluate the function for each branch. Cache the result with a let if performance matters.

Using lazy patterns where you want strictness. ~(x:xs) always succeeds, even on []. If you actually want to handle the empty case, do not use ~.

Bang patterns confused with strict data fields. A bang on a function argument forces it during pattern matching, but the field can still hold a thunk afterward. To make a field always strict, declare it strict in the data type or use StrictData.

Pattern synonyms that obscure rather than clarify. If a synonym is only used once or twice, just write the pattern inline. Synonyms pay off when the pattern is reused in many places and has a clear conceptual name.

Relying on undocumented exhaustiveness behavior. GADT pattern matching can have surprising exhaustiveness rules. Always enable -Wall and trust the warnings.

Key Takeaways

  • View patterns ((f -> p)) match against the result of applying a function. Useful when paired with Map.lookup, Data.Text.uncons, etc.
  • Pattern synonyms give names to complex patterns. Use them when a pattern is reused and benefits from a clear name.
  • Lazy patterns (~) defer matching; bang patterns (!) force evaluation immediately.
  • StrictData is a simple lever for making domain types strict by default — recommended for most production modules.
  • As-patterns (@) capture the whole value while matching its structure. They are both efficient and readable.
  • GADTs let pattern matching reveal type-level information. Advanced, but increasingly common in libraries.
  • Always compile with -Wincomplete-patterns (or -Wall -Werror) to catch missing branches at compile time.