पाठ 22 / 25

Type-Level Features and GHC Extensions

Explore GADTs, phantom types, type families and language editions.

Going further with types

GHC extends Haskell 2010 with many language extensions, enabled per file with {-# LANGUAGE ... #-} or per project. Language editions bundle stable ones: GHC2021 (and the newer GHC2024) enable widely used extensions such as ScopedTypeVariables, TypeApplications, DeriveFunctor and LambdaCase. Powerful type-level features include phantom types, type parameters that do not appear in values but distinguish them (Money 'INR vs Money 'USD with DataKinds); GADTs (generalised algebraic data types), whose constructors can refine the result type, enabling type-safe interpreters; type families, functions on types; RankNTypes for functions that take polymorphic arguments; DerivingVia for reusing instances; and type-level literals (Symbol, Nat). Libraries such as Servant and many database libraries rely on them. These tools encode more invariants in types, but they increase complexity and error-message difficulty. Use them where the guarantee is worth it, and keep most application code in plain, simple Haskell (sometimes called "simple Haskell" or "boring Haskell").

Types that carry more information

Phantom and indexed types let the compiler reject invalid combinations.

Two coins with different currency tags trying to enter an addition box; one pair with matching tags passes, the mismatched pair is blocked.
Figure 8.1 — Phantom types prevent mixing currencies.

Phantom currency types and a GADT interpreter

DataKinds for currencies, GADTs for a well-typed expression language.

{-# LANGUAGE DataKinds, KindSignatures, GADTs #-}

-- Phantom types: the currency exists only at the type level
data Currency = INR | USD

newtype Money (c :: Currency) = Money Int
  deriving Show

addMoney :: Money c -> Money c -> Money c
addMoney (Money a) (Money b) = Money (a + b)

price :: Money 'INR
price = Money 49900

shipping :: Money 'INR
shipping = Money 4900

-- addMoney price (Money 10 :: Money 'USD)   -- compile error: INR vs USD

-- GADT: each constructor states the type of value it produces
data Expr a where
  IntE  :: Int -> Expr Int
  BoolE :: Bool -> Expr Bool
  Add   :: Expr Int -> Expr Int -> Expr Int
  Equal :: Expr Int -> Expr Int -> Expr Bool
  If    :: Expr Bool -> Expr a -> Expr a -> Expr a

eval :: Expr a -> a
eval (IntE n)    = n
eval (BoolE b)   = b
eval (Add x y)   = eval x + eval y
eval (Equal x y) = eval x == eval y
eval (If c t e)  = if eval c then eval t else eval e

main :: IO ()
main = do
  print (addMoney price shipping)                       -- Money 54800
  print (eval (If (Equal (IntE 2) (Add (IntE 1) (IntE 1))) (IntE 10) (IntE 20)))   -- 10
  -- Add (BoolE True) (IntE 1) would not type-check

Complexity budget

Each advanced type feature adds a cost for every reader of the code. Teams often agree on a set of allowed extensions and reserve type-level tricks for libraries and the most critical invariants.

त्वरित जाँच: What does a phantom type parameter such as c in Money c provide?

  • Runtime currency conversion
  • A mutable field
  • A compile-time tag that prevents mixing values with different tags, with no runtime cost
  • A JSON encoding
Answer

A compile-time tag that prevents mixing values with different tags, with no runtime cost — The parameter exists only in types, so mismatches are caught by the compiler.