Skip to content
Records and Field Access
step 1/7

Reading — step 1 of 7

Learn

~3 min readPractical Haskell

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 data with 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 name in two records in the same module without DuplicateRecordFields or qualifying.
  • Treating accessors like methodsname p not p.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…