3 reasons why the C# type system is broken, and how F# improves the situation

1) null

The most basic thing a type system does is ensure that any operation I do with an identifier is valid for that particular identifier. For example, if I write:

a.Length

the type system ensures that “a” has a type and that “Length” is an accessible member of that type. “a” could be a String, or an Array, for instance. If “a” was an integer, “a.Length” would simply not compile. The type system protects me from writing nonsense and that blowing up in my face at runtime (*cough* dynamic languages *cough*).

That seems simple enough, except C# (and Java, and most other OOP languages) doesn’t even get it right. C# cannot actually guarantee that any member access is valid because any variable can be null at any time, making any member access potentially invalid. “null” doesn’t have a “Length” member or, indeed, any member; it doesn’t support any operation. Attempting to do something with null at least has well-defined behavior (NullReferenceException) but still is a run-time error, i.e. something that made it past the compiler.

How does F# improve the situation? F# largely gets rid of this problem. F# types cannot be null (unless you ask for it really hard, i.e. Unchecked.defaultof<‘T>). If you want to represent nullability, F# makes you encode that notion in the type system through Option types; using an Option as if it was the wrapped type is a compile-time error, and exhaustive pattern matching ensures you have to deal with the possibility that the value is absent. The fun counterpart is that everything else is guaranteed non-null, so you get nullability where you actually need it and don’t need to worry about it in general – unless of course you’re interacting with C# APIs.

2) void

In C#, all methods return something, except for those that are void, which must be treated completely differently. As Eric Lippert explains:

The effect of a call to a void method is fundamentally different than the effect of a call to a non-void method; a non-void method always puts something on the stack, which might need to be popped off. A void method never puts something on the stack. (…) A void returning method can never, ever be treated polymorphically with a non-void-returning method because doing so corrupts the stack!

This is why, for example, it was impossible in .NET 3.5 to have a single generic delegate for all return types; Func of void would blow up the runtime! Hence the silly Action delegate, and resulting duplication of code everywhere.
In addition, void methods are generally unusable with LINQ extension methods (Select, Aggregate, etc.) because you can’t have IEnumerable of void (what the hell is a sequence of absolutely nothing anyway?). They also screw up async/await in major ways; an async void method is indisguishable from the caller’s perspective from a non-async method, cannot be awaited and cannot be composed. This has become one of C#’s major gotchas.

How does F# improve the situation? In F#, void doesn’t exist whatsoever! All void methods are turned by the compiler into methods returning the singleton unit, a type that carries no useful information but is a real, ordinary type requiring no special treatment, unlike void. So in F# you have unified delegates and no async void nonsense. This makes life simpler and eliminates much code duplication and potential mistakes.

3) Exceptions

In C#, the type system says “this method evaluates to type X”; what it fails to say (aside from the fact that X might be null) is that this method may also not evaluate to anything and throw an exception instead. Exceptions got popular in OOP languages in the 1990s when the only alternative was checking for return codes, C-style. Of course the latter is maintenance and code clutter hell, and exceptions do allow us to write just the happy path in general and centralize error handling. It’s just something the type system totally ignores; the fact that your code compiles says nothing of how it accounts for exceptions; unless you are extremely disciplined, it probably blows up in various situations! Java is one of the few languages to have tried integrating exceptions in the type system: it’s a complete failure and it’s unclear how it could be improved. The designers of C# decided not to bother and just let you write nonsense.

Exceptions also assume a single-threaded, single-stack model; they just don’t mesh well with asynchronous code. Just think for yourself, what is supposed to happen if an exception is thrown by asynchronous code executing in some arbitrary thread? How is the caller notified? (Bonus points: what happens if the asynchronous method is void?) C# 5 does some intricate magic under the hood to allow you to still write something like:

try {
    await AsyncMethodThatMightThrow();
}
catch (InvalidOperationException e) {
}

and have that do what you’d expect; in fact the exception has to be handled by the execution context, stored temporarily, and re-thrown when execution resumes in the caller’s context. If the exception was contained in the return value of the method instead, you could simply inspect it and decide what to do, and you wouldn’t need cryptic (hopefully compiler-generated) boilerplate code to re-route the exception across stacks.

How does F# improve the situation? F# supports and embraces exceptions; one might argue that they’re still the default error mechanism; it’s hard to do otherwise when every .NET API throws exceptions. However, F# also has a functional background, which prefers encoding everything in return values. Indeed, it’s possible to write elegant exception-free code in F#, using the technique detailed by Scott Wlaschin in his article railway-oriented programming. I suggest reading the whole thing, but the basic idea is this: instead of returning T and perhaps throwing an exception, functions should return something like an Option type representing Success or Failure; Success contains the useful data (the T), and Failure contains some information regarding the error (like an exception does).

Doesn’t this throw us back to C-style return value hell? Not at all, because F# makes it easy to write very general adapter functions (i.e. monads) that simply pipeline errors through regular non-error-checking functions, until they hit whichever function actually wants to deal with the errors. This gives us both the happy-path convenience of exceptions and type safety, at the expense of minimal syntactic noise. This is technically also possible in C#, but it would look unimaginably horrible due to lack of discriminated unions, pattern matching, custom operators, aforementioned void and lousy higher-order function syntax.

In conclusion, when F# code compiles, it is 100% void-free, generally null-free, and as exception-free as you can make it; this means code that is consistent, does what it says and reserves fewer surprises at run-time. While C# 6 and particularly C# 7 may include more functional constructs like pattern matching or something more or less like discriminated unions, the 3 issues discussed here are fundamental to the design of the language and no amount of tacked-on features will fix them.

Advertisements

4 thoughts on “3 reasons why the C# type system is broken, and how F# improves the situation

  1. Pingback: F# Weekly #6, 2015 | Sergey Tihon's Blog

    1. Zeckul Post author

      C# will probably not get non-nullable reference types (see Eric Lippert), C# won’t get rid of void and I don’t see partial application, custom operators or anything that would make monadic error forwarding viable. You can bring all this via libraries but it’ll be clumsy and entirely optional. For example access to an option type does comparatively little against nullref vs the many ways in which F# prevents null values in the first place, and absence of exhaustive pattern matching at the language level make its use more error prone.

      Thanks for the interesting link BTW!

      Reply
    2. Isaac Abraham

      langage-ext is a set of library-level features; F# bakes these sorts of features in at the language level. Whilst it’s admirable that people are trying to push these features into C# through libraries, there’s no substitution for deep integration into the compiler for some of these features.

      In addition, I would suggest that were you to go to 100 different C# shops are start using langage-ext in any of them, you would be lucky if any of them would know what those features were – they’re simply not typical, idiomatic C# features or design patterns.

      Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s