Records

Record pitfalls and extensions: field names are top-level functions, so two records in one module can't share a field name without DuplicateRecordFields. Use OverloadedRecordDot (ana.name, GHC 9.2+) and NoFieldSelectors for more conventional record syntax. Accessors on sum types with different fields are partial; avoid that design.

data Person = Person
  { name :: String
  , age :: Int
  , email :: Maybe String
  }
  deriving (Show, Eq)

ana :: Person
ana = Person {name = "Ana", age = 30, email = Just "ana@example.com"}

bo :: Person
bo = Person "Bo" 25 Nothing           -- positional construction also works

-- Field accessors are functions
-- name ana == "Ana"

-- Record update syntax (creates a new value)
olderAna :: Person
olderAna = ana {age = 31}

-- Pattern matching on records
greet :: Person -> String
greet Person {name = n, age = a} = n ++ " is " ++ show a

{-# LANGUAGE RecordWildCards, NamedFieldPuns #-}
greet2 :: Person -> String
greet2 Person {..} = name ++ " (" ++ show age ++ ")"        -- RecordWildCards

greet3 :: Person -> String
greet3 Person {name, age} = name ++ " " ++ show age         -- NamedFieldPuns