ZetCode

F# record

last modified May 1, 2025

In this article, we explore how to effectively work with records in F# and understand their role in data structuring.

A record is a collection of named values grouped together in a structured format. By default, records are immutable, meaning their values cannot be changed after initialization. However, they can include members such as functions or computed properties, enhancing their functionality. If mutability is required, fields can be explicitly marked as mutable using 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 extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.