Nullable reference types compared to the Option monad

I wanted to investigate how Nullable reference types, the new big feature that was introduced in C# 8, compared to the Option type from language-ext. But let’s start with some information to set up the scene.

Nullable reference types

The way to indicate the abscence of a value in C#, as well as in many other languages, is usually to use null. For example, assume that we wish to look up a user in the database by supplying a user id. The method signature might look something like this:

public User GetUserById(int userId);

Now, this signature is a bit problematic since it does not say anything about what will happen when no user with the given userId exists. It might throw an exception, it might return a default User, or it might return null. The only way to really know is to look at the implementation. Also, if null is returned and the calling method doesn’t cover this case, we might end up with the dreaded null reference exception.

In C# 8 these problems have been addressed, by the introduction of Nullable reference types. If you turn on this feature in your project (see https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references for details) the GetUserById method can be declared like this instead:

public User? GetUserById(int userId);

Notice the questionmark after User? That indicates that this method might return null. This is a strong indication to what will happen if no user with the given user id exists. Also, the compiler will now generate warnings if you try to deference the returnvalue without first checking for null. This is nice!

The Option monad

Now, don’t be scared of the M-word. You don’t need to understand what a monad is for the following text to make sense. Later in this post I will, however, show some nice things that comes from this, but you still don’t need to understand all the underlying details (in fact, if you’re a C# developer you probably use monads all the time without even knowing it. Every type that implements IEnumerable is actually a monad).

Anyway, the idea of using an Option type comes from the functional paradigm. In F#, as an example, the option type is part of the language out of the box. In C# you can find a nice implementation of it in language-ext, which is available as a nuget package from the official nuget repository.

Using the option type the GetUserById method would get a signature like this:

public Option<User> GetUserById(int userId);

The Option type is like a container that can be either empty or contain a single value. Just like the nullable reference type, using an option is a clear indication to the caller to what to expect in the case where no user with the given user id exists.

And, just like the nullable reference type, the calling code must handle the possibility that no user is returned. Actually, using option is even more secure since there is no way to get the code to even compile without handling both possible outcomes.

Comparing the two

So, are there any benefits with using nullable reference types over the option monad, or the other way around? Well, the obvious benefit of nullable reference types is that it has built-in language support in C# starting from C# 8. No need for extra nuget packages, only a quick change in the project file and it’s enabled. It is even possible to gradually introduce it by using more fine grained control mechanisms than entire projects.

Also, nullable reference types is probably a lot easier for existing C# programmers to accept than introducing the Option type, which might look and feel a bit strange and takes a while to understand and see the benefits of. It sure feels like nullable reference types is the better option here (no pun intended).

Now, with that said, you might take the blue pill, stop reading here and remain in ignorence.
Or, you may take the red pill and I’ll show you how deep the rabbit-hole goes (did not get the reference? Shame on you! Go see The Matrix and come back here afterwards).

The problem with null is that it has no value, it’s the abscence of a value. An Option on the other hand is always a valid instance. In language-ext the Option type is implemented as a struct, meaning it can’t be null. Also, in case the Option contains a value, that value can also not be null. The implementation of Option stops any attempts to create a new Option containing a null value. This means, if you use Option you eliminate null references from your code, if you use nullable reference types they are still there.

The question that should come to mind here is, what difference does it make if I use null or not? Let me show you by an example. Assume that you have a program that takes a user id, a string to be parsed as int, as input. It looks up the user in your database then, if a user is found, it looks up the user’s e-mail address and finally, if an e-mail address for the user could be found it sends a promotional e-mail to the user. Using nullable reference types you method signatures might look like this:

public Customer? GetCustomerById(int userId);
public Email? GetEmailForCustomer(Customer customer);
public void SendPromotionalEmail(Email email);

and to cover for invalid input and possible null values, the calling code needs to look something like this:

public void SendPromoToCustomer(string id)
{
  if (int.TryParse(id, out var userId)
  {
    var customer = GetCustomerById();
    if (customer == null)
      return;
    var email = GetEmailForCustomer(customer);
    if (email == null)
      return;
    SendPromotionalEmail(email);
  }
}

Now, this is a really simple example but it clearly shows the noise that the handling of null values introduces to the code. In a large codebase, null checks like these are everywhere.

Let me show you how the code would look using Option, Bind and Do from language-ext (here comes the part where the fact that Option is a monad makes all the difference). First, let’s change the null reference type returning methods to return Option instead:

public Option<Customer> GetCustomerById(int userId);
public Option<Email> GetEmailForCustomer(Customer customer);
public void SendPromotionalEmail(Email email);

Now, the calling code can be re-written to this:

public void SendPromoToCustomer(string id)
{
  parseInt(id)
    .Bind(GetCustomerById)
    .Bind(GetEmailForCustomer)
    .Do(SendPromotionalEmail);
}

This code still handles the possibility that the id string might not be possible to parse to an int, that GetCustomerById might not return a user, and that an e-mail address might be missing, but since we always have a valid instance to work with, an Option, we can just push it through the workflow and let the framework (language-ext) handle the error cases.

Bind and Do are extensions methods on Option. They inspect the content of the Option and if it contains a value the function given as parameter will be called with the value of the Option as a parameter. The return value of the function will then be used in the next call to Bind or Do. However, if the Option does not contain any value, the function will not be called. Instead, a new empty Option will be returned directly by Bind or Do.

Conclusion

I truly believe that Option is a better alternative to null for representing the abscence of a value. However, I also understand that using Option and language-ext is a big change which requires many developers to re-think and change the way they think and reason about handling these cases. Hence, I would not introduce this into a large existing codebase where developers are used to handling null. In that case I would recommend introducing nullable reference types, if possible (C# 8 is not supported on older frameworks).

But, starting a new, smaller, project I would opt for using language-ext and trying to get away from null as much as possible.

If you are interested in learning more, I recommend that you take a look at the book review that I wrote on the book ”Functional programming in C#”, https://ericbackhage.net/c/book-review-functional-programming-in-c/

Rulla till toppen