Avoiding Null Obsession with Maybe in C#
In the previous blog post I wrote about using a Result class. In this post I want to explore a companion called Maybe.
The Over-use of Null
C# is obsessed with null
. No doubt about it. A lot of attention is made throughout the code we write to handle null values. If an instance contains null and we don’t handle this case, the code throws the dreaded NullReferenceException
. So you end up with a lot of code that looks like this:
var input = Console.ReadLine();
if (input != null)
{
var dayOfWeek = Enum.Parse<DayOfWeek>(input);
}
Or this:
public InsertCustomer(Customer data)
{
if (data == null)
{
throw ArgumentNullException(nameof(data))
}
...
}
It would be nice if we could deal with nulls a little more elegantly.
Introducing Maybe
Maybe is a structure in C# that is a wrapper around a value that may contain a value, or may not. In the book “Functional Programming in C#” by Enrico Buonanno, this concept is called Option
. But I prefer Maybe
because it doesn’t conflict with other uses of Option
in .NET Core.
The interface of the Maybe
structure is:
public struct Maybe<T> where T : class
{
private T _value;
public T Value => _value;
public bool HasValue => _value != null;
public Maybe(T value)
{
_value = value;
}
}
And here is an example of the Maybe
concept in use:
Maybe<Customer> customer = handler.GetCustomer(id);
if (!customer.HasValue)
{
return NotFound()
}
return Ok(customer.Value);
But is this really superior to using null
? Perhaps not. The real benefits of Maybe
appear with some additional operations and helper classes.
The first modification is to add the negative check for a value:
public bool HasValue => _value != null;
public bool HasNoValue => !HasValue;
Now you can write this instead:
Maybe<Customer> customer = handler.GetCustomer(id);
if (customer.HasNoValue)
{
return NotFound()
}
return Ok(customer.Value);
Much cleaner in my opinion.
Adding Operators
In it’s current form, here is how I can use the Maybe
concept in a method:
public Maybe<Customer> GetCustomer(int id)
{
if (id == 0)
{
return new Maybe<Customer>(null);
}
Customer data = ...
return new Maybe<Customer>(data);
}
It would be nice if we could do this:
public Maybe<Customer> GetCustomer(int id)
{
if (id == 0)
{
return null;
}
Customer data = ...
return data;
}
This reduces the syntax changes when using the Maybe
structure. This can be accomplished by introducing operators:
public static implicit operator Maybe<T>(T value)
{
return new Maybe<T>(value);
}
public static bool operator ==(Maybe<T> maybe, T value)
{
if (maybe.HasNoValue)
return false;
return maybe.Value.Equals(value);
}
public static bool operator !=(Maybe<T> maybe, T value)
{
return !(maybe == value);
}
Now we can move onto more elegant changes.
A Step Towards Functional Programming
As demonstrated with the Result
class, you can add a ‘Match’ method to run functions against the value of Maybe
:
public R Match<R>(Func<T, R> hasValue, Func<R> hasNoValue)
{
return HasValue
? hasValue(_value)
: hasNoValue();
}
This can reduce a controller action to this:
public IActionResult Get(int id)
{
return handler.GetCustomer(1)
.Match(
value => Ok(value) as IActionResult,
() => NotFound()
);
}
You can also introduce a mapping function, similar to the Select
in LINQ:
public static class MaybeExtensions
{
public static Maybe<R> Map<T, R>(this Maybe<T> optT, Func<T, R> f)
where T : class
where R : class
=> optT.Match(
t => f(t),
() => default);
}
...
public IActionResult Get(int id)
{
return handler.GetCustomer(1)
.Map(x => new CustomerViewModel(x))
.Match(
value => Ok(value) as IActionResult,
() => NotFound()
);
}
Notice now that we don’t have to throw exceptions if a value is null
. The code is capable of handling this case in a clear and concise manner.
Summary
Littering code with a lot of null
checks can significantly reduce your code’s readability. Finding alternatives like Maybe
can make things better.