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.
Projection ¶
Selecting a subset of available properties.
C#
¶
var names = from p in persons
select new { Name = p.FirstName };
F#
¶
let names = query {
for p in persons do
select {|Name = p.FirstName|}
}
Deserialization ¶
Deserializing a JSON object into an anonymous type/record object.
JSON input ¶
{
"success": true,
"message" : "Processed!",
"code" : 0,
"id": "89e8f9a1-fedb-440e-a596-e4277283fbcf"
}
C#
¶
// 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");
F#
¶
// 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.
Copy and update ¶
C#
¶
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 });
F#
¶
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.
Structural equality ¶
C#
¶
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);
F#
¶
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 ¶
Struct anonymous records ¶
C# anonymous types are always classes. In F# structs are supported too:
let p = struct {| Id= 10; Value = "Test" |}
Limitations (F#) ¶
Lack of pattern matching support ¶
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.