Daniel Earwicker Chief Software Architect
FISCAL Technologies Ltd

Unfortunate Bifurcations


C# 2019-11-24

Although this is going to seem like a series of picky complaints about C#, really it's about how any language has to evolve, and is a compromise between past and future, and the whole thing is quite difficult.

Also some speculation on what the future of language interoperability will be.

The kind of problem I'm going to pick on is where languages separate two concepts and treat them differently, making a virtue of the differences, but then it becomes a pain dealing with them generically. The language designers seem to be saying "You shouldn't need to treat these two things the same; they're fundamentally different. You're doing it all wrong!" And yet…

Java did this with its separation between:

It seems at first that I can use either as the type of a variable, or a parameter, or a returned value, so they're there are a lot of places where they are interchangeable, but these ultimately fall apart in various annoying ways. I can define my own types for objects that can be aliased, but not if I want to create an object that can't be aliased. There is further pain when dealing with generics. Only class types can be used as type parameters. Primitives have to be boxed (stored inside a wrapper object of class type), so every primitive has a corresponding box type. This damages performance enough that most general purpose libraries have to provide primitive-specific specialisations of their classes. The infection even spreads into other languages that target the JVM.

The language automatically coerces primitives to their box type where it can, but this can lead to strange problems due to the different meaning of equality for class and primitive types, and some unfortunate details of how auto-boxing works.

C# improved on this situation a lot. With its struct keyword it lets you define your own compound types that work just like primitives, and collectively they are all known as value types. Auto-boxing is a lot more seamless. Also you can define what the == operation means on your types, which can be used to hide the differences. Finally, it did a much better job with generics, eliminating most needs for boxing and hand-maintained specialisations.

Generics provide us with a way to be abstract about certain details. Suppose we want to capture the pattern of retrying operations. A basic example:

var answer = Util.Retry(() => GetTheAnswer());

That Retry method calls the operation passed into it until it succeeds without throwing, and passed back the result. What type is the parameter to Retry? It needs to return a value, so it's going to be Func<T>. C# will infer the T from my usage, so answer ends up having the same type as is returned by GetTheAnswer. Neat.

The efficient way to deal with values of primitive types is likely to be different to that for handling reference types, but this detail is hidden from us in C# - the JIT automatically produces one instance of directly executable machine code to be used for all reference types in place of T, and then one further instance for every value type we use. This expansion isn't done by the C# compiler, which just has to produce a single generic version of the CIL bytecode.

How about:

Util.Retry(() => CauseTheSideEffect());

Same idea, but now I don't need a return value, because CauseTheSideEffect "returns" void. Is this going to work? Retry is going to be something like this:

T Retry<T>(Func<T> operation)
{
    for (int n = 4; n >= 0; n--)
    {
        try
        { 
            return operation();
        }
        catch (x) when (x > 0)
        {
            // log x?
        }
    }
}

So we'd like T to be able to be void. It doesn't seem to be asking much, because we're never doing anything with T as a value; we just return it. It seems like the language could be lax about this and let a return statement precede a call to a void method inside another void method. This is what C++ does..

But C++ can get away with this because it instantiates its version of generics (templates) by pretty much replaying your source code like a macro, so if void caused trouble somewhere inside the template's code this would produce an error message, often quite confusingly. C# doesn't work like that - it produces one version of your code in CIL, and CIL uses a different instruction for calling a void method. This unfortunate bifurcation runs deep.

We can mask the problem by providing an overload like this:

void Retry(Action operation) 
     => Retry(() => 
        {
            operation();
            return 0; 
        });

The return value is arbitrary. And so in theory the C# compiler could allow us to use the underlying Func<T> version of Retry by noticing we aren't returning anything and therefore filling in the return 0; for us at the point of use. But obviously it shouldn't always do this, because it would weaken the compiler's ability to spot bugs, due to it silently passing dummy values into our code.

There are other bifurcations that are even more problematic. The worst is probably async/await. Extending our example:

var answer = await Util.Retry(() => GetTheAnswerAsync());

Now we have to re-write retry to accept a Func<Task<T>> and use async/await internally, and then restore our original synchronous version via a wrapper overload:

T Retry(Func<T> operation) 
     => Retry(() => Task.FromResult(operation())).Result;

So we have to wrap the result of the inner operation in a Task<T> and then extract the Result on the outside. Thanks to the way await works this won't actually involve any hidden asynchrony: the inner Task<T> is already completed, so await doesn't try to yield control, and similarly the Result property doesn't need to Wait.

It's once you have two such bifurcations that things like Retry become tedious: you need four overloads to cover every case.

With the addition of nullable references in C# 8 there is another nasty example, which is actually the old split between reference and value types coming back to bite us. Surprisingly, there is currently no way to express T? where T is any type, value or reference. Support for nullable value types has been in the language for a very long time, but they work very differently because for a value type adding support for an additional null state requires extra storage along side the value itself (and to get at the value requires you to look in the Value property). Reference types by contrast have always supported the special null value; what's being added now is the ability to constrain them so they (mostly) don't allow null, which is essentially a compile-time concept. So although the language seems to have a general concept of a nullable "thing", it really doesn't. It just uses the same ? suffix syntax to denote the nullable variants of two entirely different things.

As a deliberately simple example consider the good old Maybe monadic bind operator, popularly defined as IsNotNull:

public static TResult? IsNotNull<TArg, TResult>(
    this TArg? arg, 
    Func<TArg, TResult> operation)
        where TArg : class
        where TResult : class
            => arg != null ? operation(arg) : null;

Note that I've had to constrain both TArg and TResult as being reference types (whereclass). So to cover every possible combination of struct and class for the input and result, I need four overloads! But even worse, this time I can't cheat by making three of them into simple wrappers that call into a single implementation. A nullable reference type is really just a plain reference type in a compile-time disguise, where as a nullable value type is entirely different from its underlying value type at runtime. We have no choice but to copy and paste the code of our method four times, and make slight modifications to each case:

public static TResult? IsNotNull<TArg, TResult>(
    this TArg? arg,
    Func<TArg, TResult?> operation)
        where TArg : class
        where TResult : class
            => arg != null ? operation(arg) : null;

public static TResult? IsNotNull<TArg, TResult>(
    this TArg? arg, 
    Func<TArg, TResult?> operation)
        where TArg : struct
        where TResult : class
            => arg != null ? operation(arg.Value) : null;

public static TResult? IsNotNull<TArg, TResult>(
    this TArg? arg, 
    Func<TArg, TResult?> operation)
        where TArg : class
        where TResult : struct
            => arg != null ? operation(arg) : default;

public static TResult? IsNotNull<TArg, TResult>(
    this TArg? arg,
    Func<TArg, TResult?> operation)
        where TArg : struct
        where TResult : struct
            => arg != null ? operation(arg.Value) : default;    

When we say arg != null, in half the cases (where arg is a value type) that's just sugar for arg.HasValue, but such sugar is non-existent when we need to get the value: we have to say arg.Value. Also when we want to substitute null, in half the cases (where the result is a value type) we have to use the default keyword, which is the 7.x abbreviation of default(TResult?) and means "Nullable<TResult> with no value".

If this was a less trivial example, it would be a genuine pain to maintain those four version. If you had to add another generic nullable parameter, it would double again the number of hand-maintained not-quite-the-same overloads required.

Now combine that with async versions of everything and you double the overloads again. See how these bifurcations get out hand - before you know it you're in the second half of the chessboard. Okay, that's a slight exaggeration.

Anyway, this kind of evolutionary pain is why people start again with new languages. But I think the way forward is already indicated. The CLR is a runtime that is too opinionated and richly featured. This was intended to create a way forward so that a wide range of languages could share libraries with each other. When that happens, it will be utopia compared with today.

But the CLR isn't going to be the platform for that utopia. It was intended to be general enough to support all languages, but now even its flagship showcase language, C#, is showing the strain of supporting its real life users while constrained by the CLR's underlying model. Yes, it's better than the JVM, but that's a very low bar.

Meanwhile the last decade has seen runtimes for Javascript become so ingeniously self-optimising that they can compete with native code even for raw number crunching. There are 3D game engines written in JS. There are emulators for mainstream processors written in plain JS that can boot actual operating systems in the browser - nearly a decade ago this was done for a minimal Linux, but Windows 2000 is now there too.

WebAssembly in a sense grew out of such efforts, but it has only just started. At the moment it provides a sandbox within which an old-school native C/C++ codebase can freely scribble over its own patch of memory without causing wider damage. It does not yet define how a hosted language may expose fine grained objects that will be automatically garbage collected, and can have named members inside them, some of which may be callable. Hopefully when that step is taken, it will initially be as minimal and vague as possible, instead of (as the CLR did) trying to cover every possible approach with fine-grained features.

And then we will have a real breakthrough, because a wide range of languages will be able to move on to the super-fast JS runtimes and bring their libraries with them. We will be able to create data structures that lace together objects written in C#, JavaScript and Python, all to be collected by the same GC.

TypeScript has shown that a type system can be organically fitted over a very dynamic object model, and it can grow to meet user needs in ways that long ago left C# in the dust. I wonder if the future of C# lies in switching its home runtime from the CLR to JS.

Time reversible events 2023-04-07
Language Smackdown: Java vs. C# 2023-03-07
Domesday '86 Reloaded (Reloaded) 2021-02-07
The Blob Lottery 2020-09-27
Abstraction is a Thing 2020-03-07
Unfortunate Bifurcations 2019-11-24
Two Cheers for SQL 2019-08-26
Factory Injection in C# 2019-07-02
Hangfire - A Tale of Several Queues 2019-05-24
How Does Auth work? 2018-11-24
From Ember to React, Part 2: Baby, Bathwater, Routing, etc. 2018-03-18
From Ember to React, Part 1: Why Not Ember? 2017-11-07
json-mobx - Like React, but for Data (Part 2) 2017-02-15
Redux in Pieces 2017-01-28
Box 'em! - Property references for TypeScript 2017-01-11
TypeScript - What's up with this? 2017-01-01
MobX - Like React, but for Data 2016-12-28
Eventless - XAML Flavoured 2016-12-24
Immuto - Epilogue 2016-12-20
Immuto - Radical Unification 2016-09-22
Immuto - Working with React (An Example) 2016-09-16
Immuto - Strongly Typed Redux Composition 2016-09-11
TypeScript - What is a class? 2016-09-11
TypeScript and runtime typing - EPISODE II 2016-09-10
TypeScript and runtime typing 2016-09-04
What's good about Redux 2016-07-24
TypeScript multicast functions 2016-03-13
Introducing doop 2016-03-08
TypeScript is not really a superset of JavaScript and that is a Good Thing 2015-07-11
A new kind of managed lvalue pointer 2014-04-27
Using pointer syntax as a shorthand for IEnumerable 2014-04-26
Adding crazily powerful operator overloading to C# 6 2014-04-23
Introducing Carota 2013-11-04
Want to comment on anything? Create an issue!