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