Skip to content
AgileLogics
6 min read

Records

A record is a named group of named values — the F# way to describe a piece of data:

type Person = { Name: string; Age: int; Email: string }

let ada = { Name = "Ada"; Age = 36; Email = "ada@example.com" }

printfn "%s is %d" ada.Name ada.Age
printfn "%A" ada
Ada is 36
{ Name = "Ada"
  Age = 36
  Email = "ada@example.com" }
  • Field names start with a capital letter. Fields can also go on separate lines, without semicolons.
  • You don't write the type when creating a record: F# infers Person from the field names.
  • %A prints the whole record in F# syntax — very handy for debugging.

Copy and update

Records are immutable. To "change" one, create a copy with some fields replaced:

type Person = { Name: string; Age: int }

let ada = { Name = "Ada"; Age = 36 }
let older = { ada with Age = ada.Age + 1 }

printfn "%A" ada
printfn "%A" older
{ Name = "Ada"
  Age = 36 }
{ Name = "Ada"
  Age = 37 }

Structural equality

Two records are equal when all their fields are equal — no need to write an Equals method:

type Point = { X: int; Y: int }

let a = { X = 1; Y = 2 }
let b = { X = 1; Y = 2 }

printfn "%b" (a = b)
printfn "%b" (a = { b with Y = 3 })
printfn "%A" (List.sort [ { X = 2; Y = 0 }; { X = 1; Y = 5 } ] |> List.map (fun p -> p.X, p.Y))
printfn "%A" (Set.ofList [a; b] |> Set.count)
true
false
[(1, 5); (2, 0)]
1

Records also get comparison (field by field, in order) and hashing, so they work as Map keys and in Sets.

Pattern matching on records

type Order = { Id: int; Total: decimal; Express: bool }

let shippingCost order =
    match order with
    | { Express = true } -> 15m
    | { Total = t } when t >= 100m -> 0m
    | _ -> 5m

let orders =
    [ { Id = 1; Total = 40m; Express = true }
      { Id = 2; Total = 150m; Express = false }
      { Id = 3; Total = 30m; Express = false } ]

for o in orders do
    printfn "Order %d ships for %M" o.Id (shippingCost o)
Order 1 ships for 15
Order 2 ships for 0
Order 3 ships for 5

Records with members

A record can have members — properties and methods computed from its fields:

type Rectangle =
    { Width: float
      Height: float }

    member this.Area = this.Width * this.Height
    member this.Scale factor = { Width = this.Width * factor; Height = this.Height * factor }

let r = { Width = 3.0; Height = 2.0 }
printfn "%.1f" r.Area
printfn "%.1f" (r.Scale(2.0)).Area
6.0
24.0

Anonymous records

For quick, one-off shapes, anonymous records need no type definition:

let summary = {| Count = 3; Label = "items" |}
printfn "%d %s" summary.Count summary.Label
printfn "%A" summary
3 items
{| Count = 3
   Label = "items" |}

Common mistakes

  • Two record types with the same field names: F# infers the one defined last. Annotate (let p: Person = ...) or qualify a field ({ Person.Name = ...; Age = ... }) when it's ambiguous.
  • Forgetting a field when creating a record — every field is required.

Practice

Define a Book record with Title, Author, and Year. Create two books, then create a corrected copy of one with a different year, and print the titles of books published before 2000.

Show solution
type Book = { Title: string; Author: string; Year: int }

let books =
    [ { Title = "SICP"; Author = "Abelson & Sussman"; Year = 1985 }
      { Title = "Expert F#"; Author = "Syme et al."; Year = 2007 } ]

let fixedBook = { books[0] with Year = 1996 }
printfn "%A" fixedBook

books
|> List.filter (fun b -> b.Year < 2000)
|> List.iter (fun b -> printfn "%s" b.Title)
{ Title = "SICP"
  Author = "Abelson & Sussman"
  Year = 1996 }
SICP