ZetCode

F# record

last modified February 16, 2023

In this article, we show how to work with records in F#.

A record is an aggregate of named values. It is immutable by default. Records can contain members. It is possible to create mutable fields with the mutable keyword.

F# record simple example

A record is defined with the type keyword. The values are specified between the { } brackets.

simple.fsx
type User =
    { FirstName: string; LastName: string; Occupation: string; Salary: int }

let users =
    [ { FirstName = "Robert"; LastName = "Novak"; Occupation = "teacher"; Salary = 1770 }
      { FirstName = "John"; LastName = "Doe"; Occupation = "gardener"; Salary = 1230 }
      { FirstName = "Lucy"; LastName = "Novak"; Occupation = "accountant"; Salary = 670 } ]

users |> List.iter (printfn "%A")

The program defines a User record. We create a list of three users from the record type. The list is then iterated.

type User =
  { FirstName: string; LastName: string; Occupation: string; Salary: int }

The record type defines three fields. The fields are separated with semicolons. These semicolons are optional. The field name and its type are separated with a colon.

let users =
    [ { FirstName = "Robert"; LastName = "Novak"; Occupation = "teacher"; Salary = 1770 }
      { FirstName = "John"; LastName = "Doe"; Occupation = "gardener"; Salary = 1230 }
      { FirstName = "Lucy"; LastName = "Novak"; Occupation = "accountant"; Salary = 670 } ]

We have a list of three users. The field names are separated from the values with the equals character.

λ dotnet fsi simple.fsx
{ FirstName = "Robert"
  LastName = "Novak"
  Occupation = "teacher"
  Salary = 1770 }
{ FirstName = "John"
  LastName = "Doe"
  Occupation = "gardener"
  Salary = 1230 }
{ FirstName = "Lucy"
  LastName = "Novak"
  Occupation = "accountant"
  Salary = 670 }

When we place each field on a separate line, we can omit the semicolon.

simple2.fsx
type User =
    { FirstName: string
      LastName: string
      Occupation: string
      Salary: int }

let users =
    [ { FirstName = "Robert"
        LastName = "Novak"
        Occupation = "teacher"
        Salary = 1770 }
      { FirstName = "John"
        LastName = "Doe"
        Occupation = "gardener"
        Salary = 1230 }
      { FirstName = "Lucy"
        LastName = "Novak"
        Occupation = "accountant"
        Salary = 670 } ]

users |> List.iter (printfn "%A")

The program defines and creates records without semicolons.

F# record access fields

The fields of a record are access via the dot character.

access.fsx
type User = { Name: string; Occupation: string }

let u =
    { Name = "John Doe"
      Occupation = "gardener" }

printfn "%s" u.Name
printfn "%s" u.Occupation

We create a user record with two fields and then print the values of those fields. The field names are accessed via the dot character.

λ dotnet fsi access.fsx
John Doe
gardener

F# record fields order

F# determines a records type by the name and type of its fields, not the order that fields are used.

order.fsx
type User = { Name: string; Occupation: string }

let u1 =
    { Name = "John Doe"
      Occupation = "gardener" }

let u2 =
    { Occupation = "driver"
      Name = "Roger Roe" }

printfn "%A" u1
printfn "%A" u2

We define two record objects. The order in which the Name and Occupation orders are defined is not relevant.

λ dotnet fsi order.fsx
{ Name = "John Doe"
  Occupation = "gardener" }
{ Name = "Roger Roe"
  Occupation = "driver" }

F# clone record

New records can be derived from existing records using with.

clone.fsx
type User = { Name: string; Occupation: string }

let u1 =
    { Name = "John Doe"
      Occupation = "gardener" }

printfn "%A" u1

let u2 = { u1 with Name = "Peter Smith"}
printfn "%A" u2

In the example, we clone a new user base on an existing user.

let u2 = { u1 with Name = "Peter Smith"}

We derive user2 from user1; we keep the occupation and change the name.

λ dotnet fsi clone.fsx
{ Name = "John Doe"
  Occupation = "gardener" }
{ Name = "Peter Smith"
  Occupation = "gardener" }

F# record output

The %A specifier is used for pretty-printing tuples, records and union types. The %O is used for other objects, using ToString.

output.fsx
type User =
  { Name: string
    Occupation: string }
  override this.ToString() =
      sprintf "%s %s" this.Name this.Occupation

let u1 =
  { Name = "John Doe"
    Occupation = "gardener" }

let u2 =
  { Name = "Roger Roe"
    Occupation = "driver" }

printfn "%A" u1
printfn "%O" u2

We define a record type where we override the ToString method. We output the records with the %A and %O specifiers.

λ dotnet fsi output.fsx
{ Name = "John Doe"
  Occupation = "gardener" }
Roger Roe driver

F# record deconstructing

Deconstructing is unpacking types into single pieces.

decons.fsx
type User = { Name: string; Occupation: string }

let u1 =
    { Name = "John Doe"
      Occupation = "gardener" }

let { Name = n1; Occupation = o1 } = u1
printfn "%s %s" n1 o1

let { Name = _; Occupation = o2 } = u1
printfn "%s" o2

let { Name = n2 } = u1
printfn "%s" n2

The program deconstructs a user record. Fields can be omitted.

λ dotnet fsi decons.fsx
John Doe gardener
gardener
John Doe

F# nesting records

We can nest a record inside another record with and.

nest.fsx
type User =
    { Name: string
      Occupation: string
      Address: Address }

and Address = { Line1: string; Line2: string }


let u1 =
    { Name = "John Doe"
      Occupation = "gardener"
      Address =
        { Line1 = "Address 1"
          Line2 = "Address 2" } }

printfn "%A" u1

let u2 =
    { Name = "Roger Doe"
      Occupation = "driver"
      Address =
        { Line1 = "Address 1"
          Line2 = "Address 2" } }

printfn "%A" u2

We have a User record where we nest an Address type.

λ dotnet fsi nest.fsx
{ Name = "John Doe"
  Occupation = "gardener"
  Address = { Line1 = "Address 1"
              Line2 = "Address 2" }
  Colours = { Col1 = "red"
              Col2 = "blue" } }
{ Name = "Roger Doe"
  Occupation = "driver"
  Address = { Line1 = "Address 1"
              Line2 = "Address 2" }
  Colours = { Col1 = "red"
              Col2 = "green" } }

F# record equality

Records have structural equality. Structural equality is when two objects contain the same values.

equality.fsx
type User =
    { Name: string
      Occupation: string }

let u1 =
    { Name = "John Doe"
      Occupation = "gardener" }

let u2 =
    { Name = "Roger Roe"
      Occupation = "driver" }

printfn "%A" (u1 = u2)

In the example we compare two user records.

F# record members

Members in a record can be defined witih member.

member.fsx
type User =
  { Name: string
    Occupation: string }

    member this.Info() =
        $"{this.Name} is a {this.Occupation}"


let u1 = { Name= "John Doe"; Occupation="gardener" }
let u2 = { Name= "Roger Roe"; Occupation="driver" }

printfn "%s" (u1.Info())
printfn "%s" (u2.Info())

In the example, we define the Info member.

λ dotnet fsi member.fsx
John Doe is a gardener
Roger Roe is a driver

F# record pattern match

Records can be used with pattern matching.

pattern_match.fsx
type User =
    { FirstName: string
      LastName: string
      Occupation: string }

let users =
    [ { FirstName = "John"
        LastName = "Doe"
        Occupation = "gardener" }
      { FirstName = "Jane"
        LastName = "Doe"
        Occupation = "teacher" }
      { FirstName = "Roger"
        LastName = "Roe"
        Occupation = "driver" } ]

for user in users do
    match user with
    | { LastName = "Doe" } -> printfn "%A" user
    | _ -> ()

The example prints all Does.

| { LastName = "Doe" } -> printfn "%A" user

In this branch we check for all records whose LastName equals to "Doe".

In this article we have worked with record type in F#.

Author

My name is Jan Bodnar and I am a passionate programmer with many years of programming experience. I have been writing programming articles since 2007. So far, I have written over 1400 articles and 8 e-books. I have over eight years of experience in teaching programming.