Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ


Choose your language

InfoQ Homepage News F# 4.6 Introduces Anonymous Record Types

F# 4.6 Introduces Anonymous Record Types

The next release of F#, F# 4.6, will most notably bring anonymous record types and structs to the language, along with a few additions to the standard library.

Anonymous record types remove the need to provide a name for a record type and bring a bit of structural typing to an otherwise strictly nominal language.

Although they are unlikely to fundamentally change how you write F# code, they do fill many smaller gaps F# programmers have encountered over time, and can be used for succinct data manipulation that was not previously possible.

The following snippet shows how to define and call a function that takes an anonymous record and how equality works:

let foo (args: {| a: Int; b: Int |}) =

foo {| a=2; b=4 |}

{| a = 1+1 |} = {| a = 2 |} // true
{| a = 1+1 |} > {| a = 1 |} // true

{| a = 1+1 |} = {| a = 2;  b = 1|} // error, record type mismatch

It is worth noting that anonymous records are at their heart still nominal types and thus do not support structural sub-typing. This has the effect that pattern matching cannot be used over them.

As mentioned, you can also define anonymous structs, which differ from records in that they are allocated on the stack, using the struct {| ... |} syntax. Two anonymous records or structs are recognized as belonging to the same type only if the label fields match exactly. For example, a copy-and-update expression will generate a different anonymous record type:

let data = {| X = 1; Y = 2 |}
let expandedData = {| data with Y = 3 |} // {| X:Int; Y:Int |}
let expandedData' = {| data with Z = 3 |} // {| X:Int; Y:Int; Z:Int |}

You can also extend a normal record into an anonymous record:

type R1 = { X: int }
let r1 = { X=1 }

let data1 = {| r1 with Y=1 |} // {| X:Int; Y:Int |}

One useful use of anonymous record types is when serializing/deserializing lightweight JSON data, since an anonymous record type definition can be used as a type specification:

let networkData = "{\"age\":28,\"name\":\"Phillip\"}"
let person = JsonConvert.DeserializeObject<{|name: string; age: int|}>(str)

Another scenario where anonymous record types will simplify F# syntax is with record types that are mutually recursive, as the following example shows:

type FullName = { FirstName: string; LastName: string }
type Employee =
    | Engineer of FullName
    | Manager of {| Name: FullName; Reports: Employee list |}
    | Executive of {| Name: FullName; Reports: Employee list; Assistant: Employee |}

In F# 4.5, you would use the following syntax with named record types:

type FullName = { FirstName: string; LastName: string }
type Employee =
    | Engineer of FullName
    | Manager of Manager
    | Executive of Executive

and Manager = { Name: FullName; Reports: Employee list }

and Executive = { Name: FullName; Reports: Employee list; Assistant: Employee }

On the standard library front, F# 4.6 includes the following improvements:

  • ValueOptions, struct-based Option type introduced in F# 4.5, is now on-a-par with the Option type thanks to support for DebuggerDisplay, IsNone, IsSome, None, Some, op_Implicit, and ToString.

  • List, Array, and Seq support tryExactlyOne method, which works like exactlyOne but returning an option type wrapping the only element of the collection or None if zero or more than one elements are present.

F# 4.6 is available in Visual Studio 2019 Preview 2 and requires the .NET SDK 2.1.6xx or 2.2.6xx preview of the .NET SDK.

Rate this Article