New C# 9 features and their F# counterparts

I recently watched Mads Torgersen’s video presentation of C# 9 from Microsoft Build and also read his presentation on the same topic titled Welcome to C# 9.0. It is obvious that the C# team is heavily inspired by functional programming and many of the new features have equivalents in F# (where they have existed for a long time). I thought it would be interesting to do a comparison of the new features and their F# counterparts. Since C# is primarily an object oriented language it might be difficult to add functional concepts in a way that fits well into the existing language. Let’s see what it looks like.

Init-only properties

Several of the new C# 9 features focuses on immutability. For those of you familiar with functional programming it is no news that immutability is one of the ground pillars of the functional paradigm. Mutating data is something we associate with imperative and object oriented programming.

C# 9 will introduce a feature called init-only properties which limits the possibility to set property values to only when a new instance of an object is created. Let’s look at some code.

// Prior to C# 9
public class Person 
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
}
var person = new Person { FirstName = "Eric", LastName = "Backhage" };
// It is possible to change the value of the properties whenever, this
// might not be something you want.
person.FirstName = "Carl";

// With C# 9
public class Person
{
  // Notice that 'set' has been changed to 'init'
  public string FirstName { get; init; }
  public string LastName { get; init; }
}
var person = new Person { FirstName = "Eric", LastName = "Backhage" };
// With 'set' changed to 'init' the following line will not compile.
person.FirstName = "Carl";
// However, it is still possible to set the 'person' variable to
// a different value.
person = new Person { FirstName = "Carl", LastName = "Backhage" }; 

F# – Record type assigned to mutable variable

If I wanted to have similar behavior in F# I would use a record type and assign it to a variable that is declared as mutable. The code would look like this:

type Person = { FirstName: string; LastName: string }
let mutable person = { FirstName = "Eric"; LastName = "Backhage" }
// As in the C# version the following line would not compile
person.FirstName <- "Carl"
// But it is still possible to set the 'person' variable to a
// different value
person <- { FirstName = "Carl"; LastName = "Backhage" }

The only reason I would use ’mutable’ however is for variables that are updated thousands of times in a tight loop. In other cases I would use the default behavior of immutable variables. We’ll see this next.

Records

Records in C# has been planned for a long time and it feels really nice that they finally seem to make it into the language. For those of you unfamiliar with Records they can be described as immutable data containers without any logic (methods) associated with them. To define a Record type in C# 9 you add the keyword data to the class definition. Like this:

public data class Person
{
  public string FirstName { get; init; }
  public string LastName { get; init; }
}
/* 
  There is also a shorthand that removes most of the 
   boiler plate. The above code can be written like
   this instead:
   public data class Person { string FirstName; string LastName }
*/
// Creating an instance of Person is no different
// than before
var person = new Person { FirstName = "Eric", LastName = "Backhage" };
// Properties are still init-only so the following line
// will not compile
person.FirstName = "Carl";
// However, since Person is now a Record the
// following line will also not compile
person = new Person { FirstName = "Carl", LastName = "Backhage" };
// Instead you must introduce a new variable
var anotherPerson = person with { FirstName = "Carl" };
// The 'with' keyword lets you clone properties from another
// instance. anotherPerson.LastName is equal to "Backhage"

F# – Records

In F# Records is one of the core language types and has been around forever. The above C# 9 code looks like this in F#:

type Person = { FirstName: string; LastName: string }
let person = { FirstName = "Eric"; LastName = "Backhage" }
// As in C# 9 the following does not compile
person.FirstName <- "Carl"
person <- { FirstName = "Carl"; LastName = "Backhage" }
// Instead you need to introduce a new variable
let anotherPerson = { person with FirstName = "Carl" }

C# 9 Records – value based equality

Unlike regular classes, that uses reference based equality unless you override the Equals method, C# 9 Records will use value based equality. That means that if two records are of the same type and their properties have the same values, they will be considered equal. Let’s look at some example code.

public data class Person { string FirstName; string LastName; }
var p1 = new Person { FirstName = "Eric", LastName = "Backhage" };
var p2 = new Person { FirstName = "Eric", LastName = "Backhage" };
var p3 = new Person { FirstName = "Carl", LastName = "Karlsson" };

p1.Equals(p2); // => true
p2.Equals(p3); // => false

In F#, Records works exactly in the same way, you just use ’=’ for equality checking instead:

type Person { FirstName: string; LastName: string }
let p1 = { FirstName = "Eric"; LastName = "Backhage" }
let p2 = { FirstName = "Eric"; LastName = "Backhage" }
let p3 = { FirstName = "Carl"; LastName = "Karlsson" }

p1 = p2 // -> true
p2 = p3 // -> false

C# 9 Records – Positional Records

Positional Records is a nice shorthand that reduces the amount of boiler plate code needed when defining record types with constructors and deconstructors. The code example below shows how it will work.

public data class Person(string FirstName, string LastName);

/* The code above is actually equal to all of this:
public data class Person
{
  string FirstName;
  string LastName;
  public Person(string firstName, string lastName)
    => (FirstName, LastName) = (firstName, lastName);
  public void Deconstruct(out string firstName, out string lastName)
    => (firstName, lastName) = (FirstName, LastName);
}
*/
// Instantiating
var person = new Person("Eric", "Backhage");
// Deconstructing
var (firstName, lastName) = person;

Now we are getting on par with F# in terms of being succinct. Let’s compare the same operations line by line:

// C# 9
public data class Person(string FirstName, string LastName);
// F#
type Person { FirstName: string; LastName: string }
// C# 9
var person = new Person("Eric", "Backhage");
// F#
let person = { FirstName = "Eric"; LastName = "Backhage" };
// C# 9
var (firstName, lastName) = person;
// F#
let { FirstName = firstName; LastName = lastName } = person

Nice work of the C# team!

C# 9 Records – Inheritance

True to the object oriented paradigm, C# Records will support inheritance. You will be able to create base Records and create derivatives that inherit from them. It will look like this:

public data class Person { string FirstName; string LastName; }
public data class Student : Person { int ID; }

var student = new Student { FirstName = "Eric", LastName = "Backhage", ID = 1 };

Since inheritance is an object oriented concept and F# is a functional-first language F# Records can not be inherited. The solution is to use composition instead. One way of accomplishing similar behavior in F# is shown in the code below:

type PersonalInfo = { FirstName: string; LastName: string }
type Student = { ID: int; PersonalInfo: PersonalInfo }

let student = { ID = 1; PersonalInfo = { FirstName = "Eric"; LastName = "Backhage" } }

My personal preference here is composition. I believe that the C# team have had a really tough time getting value based equality and instantiation using the ’with’ keyword working well with inheritance, hope it will be worth it.

Conclusion and further reading

I think the new C# 9 features will be great additions to the C# language and it is my opinion that the C# language design team are doing a great job adding nice new features to the language. Immutable records are not something you would normally expect to find in an object oriented language, and they feel a lot more natural in F#, but I am confident they will make a great addition to C# now that they are finally coming.

For more information on upcoming C# 9 features that I haven’t mentioned here I recommend reading Mads Torgersens post on (from where I copied most C# 9 examples in the code in this post) https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/.

Now, stop reading and go write some code!

Lämna en kommentar

E-postadressen publiceras inte. Obligatoriska fält är märkta *