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.
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.
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.
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.
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
.
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.
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.
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
.
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.
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
.
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.
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#.