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
Personfrom the field names. %Aprints 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