Monday, December 26, 2022

ADVENT 2022: What's new in F# 7 and some tips (with BenchmarkDotnet)

 Hi my blog audiences! First of all, Merry Christmas for you in this December 2022!

As always, this blog entry in December is part of F# Advent 2022 edition. Thanks Sergey Tihon for tirelessly organizing this F# Advent annually!

As most of you know I often discussing what's new in F# and with the related annual release of .NET. 

NOTES: Now .NET Core is just called .NET, so when I mention .NET this means not .NET Framework, it is .NET Core without Core. See the original Microsoft's official announcement on why.

Before I continue to discuss this F# 7, all of the code/features mentioned here requires Visual Studio 2022 17.4.0 as it is the starting version that provide support for .NET 7.0 SDK. But I strongly recommend to use 17.4.3 and later, because 17.4.3 has many bug fixes and important security fix of CVE-2022-41089 Remote Code Execution .

So what's new in F# 7? Again, there is an official explanation available on Microsoft blog: https://devblogs.microsoft.com/dotnet/announcing-fsharp-7/ and I suggest you to visit that page first.

These are the noticeable new features in F# 7:

  1. Simplified F# SRTP syntax
  2. Static virtual members 
  3. Ability to consume (interop) C# required and init members

And many more as mentioned in the official announcement above.

For this blog entry, I describe static virtual members and consuming C# required and init members.

Static virtual members in F#

The ideas behind this is simple: having static virtual members in interface. As in the blog mentioned, this is needed to consume and also to define static virtual members as those in C#.

    public interface IGetNext where T : IGetNext
    {
        static abstract T operator ++(T other);
    }

For more context, please visit the official doc of this C# 11 feature: https://learn.microsoft.com/dotnet/csharp/whats-new/tutorials/static-virtual-interface-members

That sample is using complex sample of implementing C# ++ operator, and this is not easily consumed in F#, as F# doesn't have increment operators. 


NOTE: there's a question about this C#'s increment implementation in F# as mentioned in Stackoverflow, but this is not semantically the same as in C#.

Therefore, let's try a simpler one: 

    public interface IFinancialMath
    {
        static abstract double CalculateSavingInterest(double saving, double annualInterestRate, double totalYears);
    }

and then we can consume that in F#:


What about we create in F#? To start quickly, we can use the sample in the blog.

But if we are looking at the F# example in the blog, compiling it will yield warning of FS3535:


But we can suppress this warning in the F# project file (the fsproj) instead of using #nowarn, because it is easier to reason about that this fsproj has disabled warning that we are consciously aware.

NOTE: having nowarn as embedded in the code using #nowarn is still useful if you need granular control on the code instead of project scope in fsproj.

For example:

  
  <PropertyGroup>
	<NoWarn>FS3535</NoWarn>
  </PropertyGroup>

And then it is also simpler, and MSBUILD will recognize that NoWarn to be passed to compiler successfully.

Consuming C# required and init members in F#

Consuming C# required and init members in F# is quite easy, as F# can recognize C# classes/records that has required and init.

NOTE: F# doesn't have the same idiomatic feature of C# required semantics, therefore it is not built in F#. It can only consume C# required semantics, not having its own required semantics.

Large part of this feature has been explained by the blog, but I will add my own additional detail here.

The number of C# members that have required mark will always be checked by F# compiler, but this check will always check each one of the required members. The good thing about this F# support for this feature is the fact that the F# compiler will tell you what are the missing required members to be included as part of the constructor call.

For example: if you have a Person class with 3 required property members, then the compiler will check the required members at the time of calling the constructor. Any missing member will be mentioned by the compiler's error message:

The C# init sample is already described nicely in the blog. This may look like a new feature, as it now recognizes init. But it is not just that: it is actually a matter of change of behavior: previous init was handled as mutating the value of the property and now F# 7 compiler will yield an error if there's an init on the getter and others when the setter and getter is used outside the scope of the init.

This will therefore ensure consistent behavior when these C# codes are used in F#, with the correct behavior as intended.

Comparing LINQ improvements in .NET 7 with equal F# using BenchmarkDotnet

Now that we have .NET 7.0 and F# 7, we also have performance improvements in .NET 7.0. One of the nice improvements is some of LINQ methods are now significantly faster!

In this case, we are going to use very useful and also popular benchmark tool in .NET, using BenchmarkDotnet

Contrary to some of some saying that BenchmarkDotnet is only available (supported) by C#, it isn't that case at all. BenchmarkDotnet can be used by F# (and other managed language such as VB.NET) successfully. It is also open source, therefore it is easy to learn and to contribute back to BenchmarkDotnet.

A good case of this in F# is we are checking the perf improvements in .NET 7.0 against equal function in F#, such as LINQ's Max and F# Array.max.

According to the official announcement of Performance improvements in .NET 7.0, one of the improved LINQ method is Max method. 

Now let's compare this with F# Array.max and see the winner!

Before that, I have to prepare a sample benchmark code, then add BenchmarkDotnet nuget package toat leverage the BenchmarkDotnet. This the simplified code:

And this is the result:


So clearly the winner is LINQ's Max()!

This is interesting, because LINQ Max and Min in .NET 7 is now optimized further to use vectorized API of AVX extension, which is very efficient and also faster.

This means there's still some improvement, particularly on F# implementation of Array.max as we have seen in that benchmark result.

NOTE to myself: I think I will propose this to F# official repo as improvement, therefore it should be on a par with ,NET  LINQ's Max and Min implementations. 😊

That's all folks! Merry Christmas and happy New Year of 2023!