Hi my blog readers!
First, personal announcement related to this blog:
If you follow my blog, you'll see that this blog is not quite often updated. Because many things have changed: Open Live Writer doesn't support Blogger.com anymore, and Blogger.com itself won't accept OAuth/OpenIDConnect anymore. Therefore I'm still searching for new place to blog, hopefully a new at least affordable, less than 80$ annually.
Ok, back to current blog!
I'm humbly joining the tradition of F# Advent! This 2021, I focus on what's new on F# 6.0, and some additional notes on some of its new features.
New features of F# 6.0
I'm so excited to try F# 6.0! It is also at the same time of the release of Visual Studio 2022 and .NET 6.0!
Here are the interesting and important new features:
- The task { .. } computation expression to support general .NET Task (that usually done in C#/VB.NET)
- Simpler collection indexing syntax using expr[idx] instead of expr.[idx]
- Struct representations for partial active patterns (new attribute to have marking of partial active pattern as struct)
And others new features
- Overloaded custom operations in computation expressions
- "as" patterns
- Increased consistency of indentation and undentation in code
- Additional implicit conversions
- New number format for binary
and many more! For more complete list, visit Microsoft F# Docs:
https://docs.microsoft.com/en-us/dotnet/fsharp/whats-new/fsharp-6
Now let's visit F# task computation expression.
F# task computation expression
NOTE: I might be biased, but this new task computation expression is the most important, because it brings closer compatibility with async-task based programming in C# and VB.
We all know that F# already has async computation expression. This existing async computation expression also has convenient functions to interop with Task, such as F#'s Async.StartAsTask:
https://fsharp.github.io/fsharp-core-docs/reference/fsharp-control-fsharpasync.html#StartAsTask
The task computation exprerssion is better than the existing F# async computation when interop with Task not just the faster performance and easier debugging, but the interop is easier.
The term easier is actually translated as closer compatibility with Task. Why? Let's see the sample code in the Docs:
let readFilesTask (path1, path2) = task { let! bytes1 = File.ReadAllBytesAsync(path1) let! bytes2 = File.ReadAllBytesAsync(path2) return Array.append bytes1 bytes2 }
We now can call those async API like File.ReadAllBytesAsync(path1) with implicit await by having let! on the returning result.
To see what really happened, the task computation expression comes as TaskBuilder. This builder will generate the necessary IL within the task expression.
Let's see the generated C# decompiler: (I use free JetBrains DotPeek 2021.3)
We could see the similar pattern of C# async in that readFilesTask method by observing the similar pattern of async state machine.
Then the IL goes further to return task, as in this generated IL method of readFilesTask that returns the Task:
.method public static class [System.Runtime]System.Threading.Tasks.Task`1readFilesTask( string path1, string path2 ) cil managed { .maxstack 4 .locals init ( [0] valuetype FSharp6NewFeatures.Say/readFilesTask@11 readFilesTask11, [1] valuetype FSharp6NewFeatures.Say/readFilesTask@11& local ) // [23 7 - 23 82] IL_0000: ldloca.s readFilesTask11 IL_0002: initobj FSharp6NewFeatures.Say/readFilesTask@11 // [24 7 - 24 64] IL_0008: ldloca.s readFilesTask11 IL_000a: stloc.1 // local // [26 7 - 26 26] IL_000b: ldloc.1 // local IL_000c: ldarg.1 // path2 IL_000d: stfld string FSharp6NewFeatures.Say/readFilesTask@11::path2 // [28 7 - 28 26] IL_0012: ldloc.1 // local IL_0013: ldarg.0 // path1 IL_0014: stfld string FSharp6NewFeatures.Say/readFilesTask@11::path1 // [30 7 - 30 73] IL_0019: ldloc.1 // local IL_001a: ldflda valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1 FSharp6NewFeatures.Say/readFilesTask@11::Data IL_001f: call valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 ::Create() IL_0024: stfld valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1 ::MethodBuilder // [32 7 - 32 75] IL_0029: ldloc.1 // local IL_002a: ldflda valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1 FSharp6NewFeatures.Say/readFilesTask@11::Data IL_002f: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1 ::MethodBuilder IL_0034: ldloc.1 // local IL_0035: call instance void valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 ::Start (!!0/*valuetype FSharp6NewFeatures.Say/readFilesTask@11*/&) // [34 7 - 34 44] IL_003a: ldloc.1 // local IL_003b: ldflda valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1 FSharp6NewFeatures.Say/readFilesTask@11::Data IL_0040: ldflda valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 valuetype [FSharp.Core]Microsoft.FSharp.Control.TaskStateMachineData`1 ::MethodBuilder IL_0045: call instance class [System.Runtime]System.Threading.Tasks.Task`1 valuetype [System.Runtime]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1 ::get_Task() IL_004a: ret } // end of method Say::readFilesTask
Now that we can observe the generated IL after compiling the F# sample, that F# code sample is roughly (semantically) equivalent to this C#:
public static async TaskReadFilesTask(string path1, string path2) { var bytes1 = await File.ReadAllBytesAsync(path1); var bytes2 = await File.ReadAllBytesAsync(path2); var bytes3 = new Byte[bytes1.Length + bytes2.Length]; // Equivalent logic for F# Array.append for (int i = 0; i < bytes1.Length; i++) bytes3[i] = bytes1[i]; for (int i = 0; i < bytes2.Length; i++) bytes3[bytes1.Length + i] = bytes2[i]; return bytes3; }
As we see now, it is closer to what C# async has, and this feature also remove barrier to have close compatibility between F# and C# async.
NOTE: thanks to vibrant F# users, in this first release of F# 6, there is still a bug: any call to Array.map will have undesired exception. The GitHub issue for this bug is available and the merged PR to fix this is also available!
Based on the current progress of that fix, this fix will be available in the upcoming release of .NET 6.0.200 at the same time with VS 2022 17.1.0 release.
But what if you want to use it now? We could just use Don Syme's temporary workaround available on that GitHub issue of that bug.
What are you waiting for? Let's start to code with F# 6.0 now, F# folks! And Merry Christmas and happy holiday! ❤️