Foundations Key idea: Extensions aren't exotic — GHC2021 already bundles dozens of them by default, and the rest are one pragma away

A Field Guide to GHC Extensions

A practical reference for the GHC language extensions this book has used along the way, plus the common ones it hasn't needed yet — grouped by what problem each one actually solves, with a note on when reaching for it is worth it.

What an extension actually is

Every {-# LANGUAGE X #-} pragma this book has used — BangPatterns, GADTs, RankNTypes, TypeFamilies — turns on one specific, optional relaxation or addition to Haskell’s syntax or type system, scoped to the file that declares it (or, via default-extensions in a .cabal file, to an entire package). None of this is exotic or unstable: GHC has shipped almost all of these for well over a decade, and GHC2021 — the current default language edition — already bundles dozens of the most uncontroversial ones (including several from this list) on by default, with no pragma needed at all.

★Cool Fact

Extensions are opt-in specifically because Haskell’s designers have generally preferred a small, stable core language with optional, individually-justified additions, rather than accumulating features into the base language the way some other languages do. GHC2021 (and its successor editions) exist precisely to periodically promote extensions that have proven themselves safe and uncontroversial into the default set — a slow, deliberate process rather than a one-time decision.

Extensions this book has already used

A quick recap, since these already have full explanations elsewhere:

ExtensionWhat it doesCovered in
BangPatternsForces a value to WHNF at a pattern match, with !Laziness
GADTsLets each constructor specify its own return typeType Families
DataKindsPromotes ordinary values to the type levelType Families
TypeFamiliesFunctions that compute types, not valuesType Families
KindSignaturesLets a kind be written explicitly, like (u :: Unit)Type Families
RankNTypesAllows a forall nested inside an argument’s typeEffects Beyond IO
StrictData / StrictFlips the strictness default for fields / bindingsPractical Haskell
TemplateHaskellCompile-time code generation (makeLenses)Optics

New ground: syntax convenience

These don’t change what’s expressible in Haskell — everything here has an equivalent without the extension — they change how much ceremony writing it takes. Each entry below shows the same function written both ways, so the difference is concrete rather than abstract.

LambdaCase

Pattern-matching directly on a lambda’s one argument, without ever naming it:

-- without: the argument needs a name, purely to immediately match on it
describe = \x -> case x of
  Just n  -> show n
  Nothing -> "nothing"

-- with LambdaCase: the case IS the lambda
{-# LANGUAGE LambdaCase #-}
describe = \case
  Just n  -> show n
  Nothing -> "nothing"

Reach for this the moment a function’s entire body is “immediately pattern-match my one argument” — extremely common after map, foldr, or any higher-order function that wants a short, throwaway matcher. Gotcha: it only helps for a single argument matched immediately; a function taking two arguments where only the second needs matching still needs an explicit \x -> case y of ....

MultiWayIf

An if/else if/else if/… chain, usable as a plain expression rather than only as a function’s guards:

-- without: nested if/else, or guards (which need their own equation)
classify n = if n < 0 then "negative"
             else if n == 0 then "zero"
             else "positive"

-- with MultiWayIf: reads like guards, but works anywhere an expression can go
{-# LANGUAGE MultiWayIf #-}
classify n = if | n < 0     -> "negative"
                 | n == 0    -> "zero"
                 | otherwise -> "positive"

The real payoff shows up inside a let or do-block, where ordinary guards aren’t syntactically available at all but a multi-branch decision is still needed. Gotcha: for a top-level function definition, plain guards on the equation are almost always clearer — MultiWayIf earns its keep specifically for branching that guards can’t reach, not as a guard replacement.

TupleSections

Partially applying a tuple constructor, without writing out a lambda:

-- without
map (\x -> (x, 0)) [1,2,3]

-- with TupleSections
{-# LANGUAGE TupleSections #-}
map (, 0) [1,2,3]

Common when pairing every element of a collection with a constant (an initial counter, a default flag) before folding or processing further. Gotcha: (, 0) is genuinely harder to read at a glance than \x -> (x, 0) for anyone who hasn’t seen the syntax before — worth a comment, or just the lambda, in code meant for newcomers.

RecordWildCards

Bringing every field of a record into scope at once:

data Config = Config { host :: String, port :: Int, debug :: Bool }

-- without
startServer cfg = listen (host cfg) (port cfg) (debug cfg)

-- with RecordWildCards
{-# LANGUAGE RecordWildCards #-}
startServer Config{..} = listen host port debug

Earns its keep in short, field-heavy functions that genuinely need most of a record’s contents.

⚠Common Pitfall

RecordWildCards is the extension most likely to be reached for too eagerly — bringing every field into scope silently is convenient right up until a typo shadows an unrelated variable, or a refactor adds a field that accidentally collides with something already in scope. It earns its keep in short, field-heavy functions; it’s worth a second look in anything longer than a few lines.

OverloadedStrings

Letting a string literal’s type be inferred from context, rather than fixed to String:

-- without
greeting :: T.Text
greeting = T.pack "Hello, world!"

-- with OverloadedStrings
{-# LANGUAGE OverloadedStrings #-}
greeting :: T.Text
greeting = "Hello, world!"   -- the literal itself becomes Text, no T.pack needed

Essential the moment Text or ByteString (Practical Haskell) are in real use — without it, every literal needs an explicit pack call. Gotcha: an ambiguous literal (one GHC can’t pin down a single target type for, often in ghci or a polymorphic context) can trigger a genuinely confusing “ambiguous type variable” error; an explicit type annotation ("hi" :: String) resolves it.

ViewPatterns

Pattern-matching on the result of applying a function to the argument, rather than the argument itself:

-- without
firstWord s = case words s of
  (w:_) -> w
  []    -> ""

-- with ViewPatterns
{-# LANGUAGE ViewPatterns #-}
firstWord (words -> (w:_)) = w
firstWord _                = ""

Useful when the natural thing to pattern-match on isn’t the argument’s own shape, but some transformation of it. Gotcha: shares TupleSections’ readability cost — a view pattern hides a function call inside what looks like a plain pattern, which can genuinely surprise a reader skimming quickly.

New ground: type system

These genuinely extend what can be expressed in a type signature, not just how it’s written.

ScopedTypeVariables

Letting a type variable from a function’s signature refer to the same variable inside the function’s own body:

-- without: the 'a' in go's signature is a brand-new, unrelated variable to GHC
process :: [a] -> [a]
process xs = go xs
  where go :: [a] -> [a]   -- this 'a' is NOT the same 'a' as above!
        go = reverse

-- with ScopedTypeVariables: an explicit forall makes the outer 'a' nameable inside
{-# LANGUAGE ScopedTypeVariables #-}
process :: forall a. [a] -> [a]
process xs = go xs
  where go :: [a] -> [a]   -- now correctly the SAME 'a'
        go = reverse

Needed constantly once local helper functions get their own type signatures that need to agree with the outer one — routine alongside RankNTypes and GADTs (Type Families). Gotcha: the outer signature needs an explicit forall a. for the scoping to take effect; forgetting it produces a confusing “couldn’t match type a with a1” error that gives no hint the fix is a missing forall.

FlexibleInstances

Writing an instance for a specific, concrete application of a type constructor, rather than a fully polymorphic one:

-- without: Haskell 2010 only allows instance heads of the shape (Class (T a1 a2 ... an))
-- instance Show (Maybe Int) where ...   -- REJECTED: Int isn't a bare type variable

-- with FlexibleInstances
{-# LANGUAGE FlexibleInstances #-}
instance Show (Maybe Int) where
  show (Just n) = "got " ++ show n
  show Nothing  = "nothing"

Needed the instant an instance targets one specific type application rather than every possible one. Gotcha: easy to accidentally write two instances that overlap for some type (a general instance Show (Maybe a) and a specific instance Show (Maybe Int) both matching Maybe Int) — GHC will refuse to pick one without OverlappingInstances or careful design.

FlexibleContexts

Allowing constraints more specific than a bare type variable:

-- without: constraints must be of the form (Class a), not (Class (f a))
-- printAll :: Show (f a) => f a -> IO ()   -- REJECTED without the extension

-- with FlexibleContexts
{-# LANGUAGE FlexibleContexts #-}
printAll :: (Show (f a), Functor f) => f a -> IO ()
printAll = print . fmap show

Needed constantly once you write functions generic over a higher-kinded type (f a) with a constraint on the applied type rather than f alone.

MultiParamTypeClasses

A typeclass genuinely relating two or more types, rather than describing one type in isolation:

-- without: a typeclass can only ever be parameterized by one type
-- class Convert a where convert :: a -> ???   -- there's no second type to name the target

-- with MultiParamTypeClasses
{-# LANGUAGE MultiParamTypeClasses #-}
class Convert a b where
  convert :: a -> b

instance Convert Int String where
  convert = show

Reach for this when the relationship you’re modeling is genuinely between two types, not a property of one. Gotcha: GHC often can’t tell which b you meant if more than one Convert Int b instance exists — FunctionalDependencies (next) or TypeFamilies (Type Families) are the usual fixes.

FunctionalDependencies

Telling GHC that one type parameter of a multi-parameter class determines another, resolving the ambiguity MultiParamTypeClasses alone can introduce:

{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}
class Convert a b | a -> b where   -- "a determines b": at most one b per a
  convert :: a -> b

instance Convert Int String where
  convert = show

The | a -> b reads as “for any given a, there’s only one valid b” — once declared, GHC uses it to resolve instance selection instead of demanding an explicit type annotation at every call site.

TypeApplications

Specifying a type argument directly at the call site, instead of relying on inference or a separate annotation:

-- without: an annotation on the whole expression
n = read "5" :: Int

-- with TypeApplications
{-# LANGUAGE TypeApplications #-}
n = read @Int "5"

Especially handy for functions like read or maxBound, where the type argument is really what you’re choosing, and an annotation on the outside feels like it’s in the wrong place. Gotcha: which position @Int fills depends on the order of forall-bound type variables in the function’s own signature, which isn’t always obvious from how the function is normally called — checking the signature (or ghci’s :type) before reaching for @ avoids a wrong-position mistake.

New ground: deriving, safely and conveniently

DeriveFunctor, DeriveFoldable, DeriveTraversable

Auto-generating instances for straightforward “container-shaped” types:

-- without: every method, by hand
data Tree a = Leaf | Node (Tree a) a (Tree a)
instance Functor Tree where
  fmap _ Leaf         = Leaf
  fmap f (Node l x r) = Node (fmap f l) (f x) (fmap f r)

-- with the extensions: GHC writes the above for you
{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}
data Tree a = Leaf | Node (Tree a) a (Tree a)
  deriving (Functor, Foldable, Traversable)

Gotcha: the derived instance follows a fixed structural traversal order (generally left-to-right through the constructor’s fields) — if your type needs a different semantic order (in-order vs. pre-order for a tree, say), the derived instance won’t match, and you’ll need to write it by hand instead.

GeneralizedNewtypeDeriving

Letting a newtype automatically reuse its wrapped type’s instances, instead of re-deriving them one by one:

-- without: every instance, by hand, even though the logic is identical to Double's
newtype Meters = Meters Double
instance Num Meters where
  (Meters a) + (Meters b) = Meters (a + b)
  -- ... every other Num method, by hand ...

-- with GeneralizedNewtypeDeriving
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype Meters = Meters Double deriving (Num, Show, Eq)

Gotcha: this is the exact opposite instinct from Base Camp’s newtype safety trick — automatically inheriting Num means Meters values can now be added and multiplied directly, which may quietly undo the whole reason a newtype wrapper existed in the first place. Reach for it only when inheriting the underlying type’s arithmetic is genuinely wanted, not by default.

DerivingStrategies

Disambiguating which deriving mechanism applies, when more than one is available for the same class:

-- without: ambiguous once GeneralizedNewtypeDeriving is also active --
-- does `deriving Show` mean the usual structural Show, or Double's Show reused?
newtype Meters = Meters Double
  deriving (Show)

-- with DerivingStrategies: say exactly which mechanism you mean
{-# LANGUAGE DerivingStrategies, GeneralizedNewtypeDeriving #-}
newtype Meters = Meters Double
  deriving stock (Eq)
  deriving newtype (Num, Show)

Needed the moment a module has both ordinary (“stock”) deriving and GeneralizedNewtypeDeriving (or DeriveAnyClass) active at once, since GHC then has more than one valid way to satisfy the same deriving clause.

StandaloneDeriving

Writing a deriving declaration separately from the data declaration:

-- without: deriving attached directly often can't supply the context a GADT needs
data Expr a where
  IntLit :: Int -> Expr Int
  -- deriving Show   -- doesn't work cleanly here

-- with StandaloneDeriving
{-# LANGUAGE StandaloneDeriving #-}
deriving instance Show (Expr a)

GADTs (Type Families) often can’t derive directly at the declaration site, since the constraint needed varies per-constructor; standalone deriving, written separately, lets that context be supplied explicitly instead.

In the Wild

Real-world .cabal files very commonly set a long list of default-extensions once, at the package level, rather than repeating {-# LANGUAGE ... #-} pragmas in every file — OverloadedStrings, LambdaCase, RecordWildCards, and ScopedTypeVariables are among the most frequently pre-enabled this way, precisely because they’re low-risk and used constantly once a codebase reaches any real size.

New ground: metaprogramming

TemplateHaskell

Every extension so far changes what a single piece of code can express. TemplateHaskell changes when code gets written — letting one piece of Haskell generate another piece of Haskell, at compile time, before the real compilation ever happens:

{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH

powerQ :: Int -> Q Exp
powerQ 0 = [| \n -> 1 |]
powerQ k = [| \n -> n * $(powerQ (k - 1)) n |]

cube :: Int -> Int
cube = $(powerQ 3)

[| ... |] is a quotation bracket — it doesn’t run the code inside, it builds an ordinary Haskell value (of type Q Exp) representing that code’s abstract syntax tree. $( ... ) is a splice — it runs a Q Exp-producing computation at compile time and inserts the resulting syntax tree directly into the program being compiled. cube = $(powerQ 3) doesn’t call powerQ while the program runs; GHC calls it while compiling, and the compiled cube ends up containing the fully unrolled \n -> n * (n * (n * 1)), with powerQ itself never appearing in the final binary at all.

⚠Common Pitfall

This is, functionally, the same idea LISP has called a macro since the 1960s — code that writes code, expanded before the “real” program runs. The difference is in the guardrails: a classical LISP macro receives raw, untyped syntax as data and can rewrite it with arbitrary LISP code, hygiene included only if the macro author is careful (Scheme’s syntax-rules, one of Haskell Among Peaks’ own family of languages, is the hygienic exception). Template Haskell’s brackets and splices are still ordinary, type-checked Haskell manipulating a structured Exp/Dec syntax-tree type through the Q monad — less free-form than a LISP macro, but with GHC checking the shape of the generated code for you before it’s ever spliced in.

In practice, most Haskell code that uses Template Haskell doesn’t write brackets and splices by hand the way powerQ does above — it calls a library’s pre-written TH functions, which generate the boilerplate on your behalf. Optics’ own makeLenses is exactly this: makeLenses ''Point is a splice that inspects Point’s fields at compile time and generates an entire family of lens definitions, code a human would otherwise have to write by hand, once per field, every time a record’s shape changes.

★Cool Fact

Deriving mechanisms (DeriveFunctor, GeneralizedNewtypeDeriving, and the rest of this chapter’s “New ground: deriving” section) are themselves a narrower, safer sibling of what Template Haskell does in full generality — both generate code at compile time rather than making a programmer write it by hand, but deriving clauses are restricted to a small, GHC-blessed set of patterns, while Template Haskell can generate, in principle, arbitrary Haskell from arbitrary compile-time computation.

Where to look things up

This table is deliberately a starting point, not the full list — GHC ships well over a hundred extensions, most narrow enough that most codebases never need them. The authoritative, always-current reference is the GHC User’s Guide’s Language Extensions chapter, which documents every extension GHC supports, including exact semantics and interactions between extensions used together — worth bookmarking the first time an unfamiliar {-# LANGUAGE #-} pragma shows up in someone else’s code.