The record type is a core feature in F#, but some of its details are not well-known. Let's delve into them.
Type Inference
Type inference in F# may sometimes deduce a record type differently than expected. For instance, in the following code, r
is inferred as the B
type, even though it match the A
type, resulting in compiler errors:
type A = { X: int; Y: string }
type B = { Y: int; X: string }
let r = { X = 1; Y = "a" }
The compiler checks only the field names in the record type and selects the last record definition that matches.
To resolve these errors, we can use explicit type annotations:
let b: A = { X = 1; Y = "a" }
or
let b = { X = 1; Y = "a" } : A
Another option is to use the type in the field name:
let b = { A.X = 1; Y = "a" }
Pattern Matching
Pattern matching allows us to deconstruct records:
type User = { Name: string; Age: int }
let user = { Name = "John"; Age = 20 }
let { Name = name; Age = age } = user
We can deconstruct only specific fields and use other patterns inside, especially in combination with the as
keyword:
type User = { Name: string; Age: int option; Email: string option; Address: string option }
let user = { Name = "John"; Age = None; Email = Some "someEmail"; Address = None }
match user with
| { Email = Some email } as u -> printfn "Sending email to %s: %s" u.Name email
| { Address = Some address } as u -> printfn "Sending a postcard to %s: %s" u.Name address
| u -> printfn "No contact info for %s" u.Name
This way, we can handle cases based on certain fields while still having access to the entire record.
Patterns can also be nested:
type Address = { Street: string; City: string }
type User = { Name: string; Age: int option; Email: string option; Address: Address option }
let { Address = { City = city } } = user
Default Values
Often, we need to create a record specifying only some fields and use default values for the rest. Although F# don't have direct support for this, it is common to use the Default
static member:
type Config = {
ServerAddress: string
Port: int
UseSSL: bool
Timeout: System.TimeSpan option
} with
static member Default = { ServerAddress = "localhost"; Port = 80; UseSSL = false; Timeout = None }
let config = { Config.Default with ServerAddress = "my-great-site.com" }
Updating Nested Records
Starting from F# 8, we can use shorthand syntax for updating nested records:
type PersonalInfo = { Age: int; Email: string }
type User = { Name: string; Info: PersonalInfo }
let user = { Name = "John"; Info = { Age = 20; Email = "john@email" }}
let user2 = { user with Info.Age = user.Info.Age + 1 }