F# tips weekly #4: Record types

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 }
3
Subscribe to my newsletter

Read articles from Jindřich Ivánek directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Jindřich Ivánek
Jindřich Ivánek

Software developer focused mainly on F#, with a passion for functional programming, problem solving, efficient algorithms and programming languages. I have a strong computer science background, focused mainly in graph theory and graph algorithms; also was intrigued by data structures, complexity, and computability theory. Experienced with backend F# programming. I worked with mathematical optimization (mixed integer programming) and use of statistical methods as well. I contributed to several open source F# projects, mostly around developer tooling.