I have encountered a scenario where the VB compiler outsmarts the C# compiler. I am stunned by this. C# is the far superior programming language. But I’ve done a lot of testing and am convinced it’s true.
Month ago, while working on C# code for my Leaderless Replication post, I ran into a compiler error I did not understand. My mental compiler- as opposed to Microsoft’s C# compiler- concluded all the types were correct and the code should compile. I could not figure out what was incorrect about the code. I accepted my code was wrong- I’m not arrogant enough to suspect a compiler bug. I just didn’t understand why it was wrong.
I ran into the same compiler error a couple weeks ago while writing code at work. I was writing highly asynchronous code that processes numerous HTTP requests made to a partner web service. To improve performance I make these HTTP requests concurrently (many requests are “in flight” at any given moment) and leverage the .NET Framework’s
Task.WhenAny method to process responses “as they arrive”. The throughput of this integration code is constrained by dependencies between HTTP requests.
I designed a solution I call an async producer / consumer pipeline.
To retrieve the required data, I must call four different web service methods in sequence. The output of each request becomes the input of the next request. I designed a solution I call an async producer / consumer pipeline. The technical details of my solution will make an interesting topic for a future blog post. However, let’s focus on the compiler error I encountered.
Update 2020 Sep 07: I’ve written a blog post describing my pipeline solution. See Async Producer / Consumer Pipeline.
A C# Compiler Error
In the async pipeline code, rather than rewrite low-level networking code, I call previously written methods that issue HTTP requests, parse the JSON response, and return domain class instances, typed as interfaces. I realized one of the four method calls was problematic. I needed to use the method result to add an entry to a
Dictionary. However, the data I intended to use as the dictionary key was not included in the method’s return value. It was passed as a method parameter but was not available when I awaited the
Task.WhenAny. I did not want to alter the existing, clean method signature. Contorting it to resolve an issue in my client code seemed kludgy. My dilemma was how to make an input value, known to the producer, available to consumer code further down the pipeline? I thought about adding state to the
Task at the time of its creation, but it would be untyped (
Object) and besides, it was unclear how to accomplish this. In a moment of inspiration I thought to write a lambda method that captures the input value (a “closure“) and includes it in a Tuple return value. Yes, that’s an elegant solution! So I wrote the following code. Well, I’ll show you code I wrote for my Leaderless Replication personal project rather than code I wrote for my employer. It demonstrates the same issue. The async lambda technique is in lines 24 – 30.
This code fails to compile.
PS C:\Users\Erik\...\Leaderless Replication> dotnet build -c release Microsoft (R) Build Engine version 16.6.0+5ff7b0c9e for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Determining projects to restore... All projects are up-to-date for restore. QuorumNode.cs(46,33): error CS1660: Cannot convert lambda expression to type 'Task<(string Value, int ToNodeId)>' because it is not a delegate type Build FAILED.
A Clumsy Solution
At the time I wrote this code for my Leaderless Replication post, I did not understand why it fails to compile. I do now. However, back in December I was anxious to complete the replication algorithm and didn’t want to spin my wheels on an esoteric compiler error. I rewrote the code as a local function (lines 24 – 30) and moved on. However, I did not like the local function solution. It felt clumsy to me.
A VB.NET Compiler Error
Fast-forward seven months to the present. While working on the async producer / consumer pipeline code I described above I ran into the same compiler error. Except this time I was writing VB.NET code. Yes, one of our 25 projects is written in VB.NET. We’ve converted all our projects to C# (or they originated as C# projects) except one: The largest, most complex project; The project I was working in while adding web service integration using my async pipeline technique.
I did not like the local function solution. It felt clumsy to me.
I hate VB. Prior to joining my present employer one year ago, the last time I wrote a line of VB was in 2002. When .NET Framework 1.0 was released in January of 2002, I immediately taught myself C# and migrated my code from VB6 to C#. At the time, I was a one-man team working on a much smaller codebase than I am now, so I was able to learn a new programming language in, I don’t know, a month, and migrate all my code in a week. Migrating my present employer’s VB code to C# is a much more daunting task that will involve five developers, three QA engineers, and many weeks or months of effort.
An Elegant VB.NET Solution
Having run into this compiler error a second time, I was determined to find an elegant solution. I did. I realized my mistake. I was attempting to assign an async lambda method to a variable typed as a
Task(Of (Value As String, ToNodeId As Integer)). The lambda method is an anonymous function. So the VB.NET compiler interprets it as a
Function() As Task(Of (Value As String, ToNodeId As Integer)). See Bill Wagner’s Do Async Lambdas Return Tasks? blog post.
This is known as
Func<Task<(string Value, int ToNodeId)>> in C#.
I can’t assign a
Function to a
Task. The compiler told me so when it complained, “Cannot convert lambda expression… to Task … because it is not a delegate type.” What is a delegate type? A
Function. And what does an asynchronous function return when invoked? A
Task. So invoke the function, you dummy!
The solution is simple: add parenthesis at the end of the lambda method so it’s invoked immediately.
The result of invoking the function is a
Task(Of (Value As String, ToNodeId As Integer)), which is assignable to the
regionTasks collection. The solution is simple: add parenthesis at the end of the lambda method (line 22) so it’s invoked immediately.
Elegant Solution Unsupported in C#
This code compiles in VB.NET. Amazingly, it does not compile in C#. Immediately invoking the lambda method (in line 30) causes a compiler error.
PS C:\Users\Erik\...\Leaderless Replication> dotnet build -c release Microsoft (R) Build Engine version 16.6.0+5ff7b0c9e for .NET Core Copyright (C) Microsoft Corporation. All rights reserved. Determining projects to restore... All projects are up-to-date for restore. QuorumNode.cs(57,33): error CS0149: Method name expected Build FAILED.
Why doesn’t it compile in C#? Because the C# compiler cannot infer the lambda method’s return type. The VB.NET compiler has no difficulty determining the return type. That’s what I mean when I say in this post’s title, “VB Outsmarts C#”. In order to use an async lambda method in C#, I must cast its return value as a
Func, which produces horribly illegible code.
Too many parentheses! The point of using lambda methods is to minimize the amount of “ceremony” required to accomplish a programming objective. Casting violates that principle. It expands a terse but clear statement into a verbose and incomprehensible statement. Terrible.
An Okay Solution in C#. Meh.
The best I can do in C# is explicitly declare a
Func, then invoke it immediately (in line 8).
This isn’t too bad, I guess. I wish I could use an async lambda method in C#, but it appears that is not possible today in C# 8.0.
If you have found a more elegant solution, let me know. Actually, please answer the question I posted on Stack Overflow so other programmers benefit from your suggestion.
Update 2020 Jul 06
The Stack Overflow community was very helpful. Shortly after posting my question I received a couple good suggestions. User “Guru Stron” suggested a LINQ solution and a static helper method. User “Theodor Zoulias” suggested a less didactic name for the helper method. I decided to use a helper method because, in my opinion, it doesn’t impede the flow of code. I find it easier to read than other solutions. I added the helper method to my Utilities NuGet package and integrated it into my Leaderless Replication code like so (line 24):
I hope you enjoyed this squabble with the C# compiler. I feel I settled on a reasonable solution. Though I recognize this really comes down to an individual programmer’s preferred style.