Reading — step 1 of 7
Learn
Record syntax in Haskell auto-generates accessor functions and provides convenient update syntax. Real World Haskell chapter 3 introduces records as the standard way to define data with named fields.
Basic record syntax
data Person = Person
{ name :: String
, age :: Int
, email :: String
} deriving (Show)
ada :: Person
ada = Person { name = "Ada", age = 36, email = "[email protected]" }
The field names BECOME functions:
name ada -- "Ada"
age ada -- 36
email ada -- "[email protected]"
Each field name is a top-level function Field :: Record -> FieldType.
Record update syntax
older :: Person
older = ada { age = 37 } -- new Person, age changed
renamed :: Person
renamed = ada { name = "Linus", age = 53 } -- multiple fields
The r { field = value } syntax produces a NEW record. Originals are immutable.
Pattern matching on records
greet :: Person -> String
greet (Person { name = n, age = a }) =
"Hi, " ++ n ++ ", age " ++ show a
-- Or skip fields you don't care about:
describe :: Person -> String
describe Person { name = n } = n ++ " exists"
Use positional matching too:
greet (Person n a _) = "Hi, " ++ n ++ ", age " ++ show a
But named matching is more refactor-safe — adding fields doesn't break existing patterns.
The accessor name conflict problem
data Person = Person { name :: String, age :: Int }
data Company = Company { name :: String, founded :: Int }
ERROR — name defined twice. Two solutions:
1. Module-level qualifying — different modules:
-- in Person.hs
data Person = Person { name :: String, age :: Int }
-- in Company.hs
data Company = Company { name :: String, founded :: Int }
-- Use as Person.name p, Company.name c
2. DuplicateRecordFields extension (GHC 8.0+):
{-# LANGUAGE DuplicateRecordFields #-}
-- now both can have `name`; uses are sometimes ambiguous, GHC asks for type annotations
3. RecordWildCards extension for shorthand:
{-# LANGUAGE RecordWildCards #-}
foo :: Person -> String
foo Person{..} = name ++ show age -- name and age in scope
Records vs newtypes vs data
- Plain
datawith positional fields — small types where field meaning is clear - Records — multiple fields, especially when meaning isn't obvious from type
newtype— single-field wrappers for type safety
Update is shallow — and that's painful for nested records
data Address = Address { city :: String, zip :: String }
data User = User { uname :: String, address :: Address }
-- Updating nested address.city is verbose:
u' = u { address = (address u) { city = "NYC" } }
This is why Haskell has lenses (a separate library) for ergonomic deep updates. Lenses are covered in the Advanced course.
Strictness and records
data Point = Point { x :: !Double, y :: !Double }
The ! makes the field STRICT — evaluated when the record is constructed, not lazily later. For numeric fields in hot paths, almost always use strict fields.
With {-# LANGUAGE StrictData #-}, all data fields default to strict.
Common mistakes
- Field name conflicts — don't define
namein two records in the same module without DuplicateRecordFields or qualifying. - Treating accessors like methods —
name pnotp.name. The accessor is just a regular function. - Missing
deriving Show— can't print custom records. - Lazy fields in numeric records — leaks memory in tight loops. Use
!or StrictData. - Reaching for plain data when records would document the fields — for 3+ fields, records read better.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…