F# vs C#: Anonymous Records

Intro ¶

Anonymous types have been in C# since version 3.0 which makes it quite a mature and classic feature. On the contrary, anonymous records are in F# only since version 4.6 (which has only been released earlier this year). Both features serve a similar purpose. They enable developer to bundle a bunch of read-only properties into an ad-hoc inline-declared type.

Usage examples ¶

Full examples can be found here.

Selecting a subset of available properties.

var names = from p in persons
            select new { Name = p.FirstName };
let names = query { 
        for p in persons do
        select {|Name = p.FirstName|}
    }

Deserializing a JSON object into an anonymous type/record object.

{
    "success": true,
    "message" : "Processed!",
    "code" : 0,
    "id": "89e8f9a1-fedb-440e-a596-e4277283fbcf"
}
// please note: System.Text.Json serializer does not support classes 
// without parameterless constructors out of the box
// a custom converter would be needed (like in the F# example)

T Deserialize<T>(T template) => JsonSerializer.Deserialize<T>(input);

var result = Deserialize(template: new { success = false, id = Guid.Empty });

if (result.success) Console.WriteLine(result.id);
else throw new Exception("Error");
// using a custom converter from FSharp.SystemTextJson package
let opts = JsonSerializerOptions()
opts.Converters.Add(JsonFSharpConverter())   

let result = JsonSerializer.Deserialize<{|success:bool; id:Guid|}>(input, opts)
if result.success then printfn "%A" (result.id)
else failwith "Error"

I am using the new System.Text.Json serializer. It does not support read-only types out of the box so the C# example does not work. In the F# example I use a third-party package: FSharp.SystemTextJson. As far as language usage is concerned you can see in C# I need an additional generic method and a template object. On the other hand, in F# I can satisfy the type parameter by providing type definition in-line.

var dob = new DateTime(2000, 12, 12);
var data = new { FirstName = "Alice", LastName = "Smith", DateOfBirth = dob };
Console.WriteLine(new { data.FirstName, LastName = "Jones", data.DateOfBirth });
let dob = DateTime(2000, 12, 12)
let data = {| FirstName = "Alice"; LastName = "Smith"; DateOfBirth = dob |}
printfn "%A" {| data with LastName = "Jones" |}

Thanks to F#’s built-in copy and update expressions, i.e. the with syntax, copy and update operation is far simpler with anonymous types. In C# all the properties must be manually copied. On the plus side, property names are automatically inferred.

var dob = new DateTime(2000, 12, 12);
var r1 = new { FirstName = "Alice", LastName = "Smith", DateOfBirth = dob };
var r2 = new { FirstName = "Alice", LastName = "Smith", DateOfBirth = dob };
// false
var referentialEq = r1 == r2;
// true
var structuralEq = r1.Equals(r2);
let dob = DateTime(2000, 12, 12)
let r1 = {| FirstName = "Alice"; LastName = "Smith"; DateOfBirth = dob |}
let r2 = {| FirstName = "Alice"; LastName = "Smith"; DateOfBirth = dob |}
// false
let referentialEq = obj.ReferenceEquals(r1, r2) 
// true
let structuralEq = r1 = r2 

Structural equality is supported both in C# and F#. However, be careful with the way comparison is performed. In F# structural equality is verified via the equality operator (=). This is not true for C# though. The equality operator in C# (==) for reference types by default performs a referential equality check. To get structural comparison the Equals method must be used as explained in the last paragraph of the Remarks section.

F#-only features ¶

C# anonymous types are always classes. In F# structs are supported too:

let p = struct {| Id= 10; Value = "Test" |}

Limitations (F#) ¶

Pattern matching is the bread and butter of F# so it hurts quite a lot. More on this limitation here.

Outro ¶

Anonymous records in F# serve similar purpose but are more powerful than anonymous types in C#. I find them particularly useful in prototyping scenarios where you can iterate quickly without defining named types/records. However, lack of pattern matching support slightly impairs their usefulness.