Heresy in C#: Nullable Reference Extensions
I've been eagerly anticipating the release of C# 8 and its nullable reference types. With it, we'll have the ability to treat null
as if it were its own type, similar to how Kotlin handles it. And with that ability, do some things that are sure to give both object-oriented and functional purists nightmares - nullable reference extension methods!
The launch of Visual Studio 2019 gave me the chance to attempt this heresy, and extend nullable types with two of my best friends - map
and flatMap
. In the Linq
world, these are Select
and SelectMany
. I find these two methods make working with any sort nullable union type a little bit cleaner, and hoped they would do the same in the C# world.
Also, to make sure I had as much feature bloat as possible, I implemented them using another awesome feature of C# 8, switch expressions!
Here's the gist:
namespace Heresy {
public static class NullableReferenceExtensions {
public static U? Map<T, U>(this T? x, Func<T, U> fn)
where T : class
where U : class {
return x switch {
null => null,
T it => fn(it)
};
}
public static U? FlatMap<T, U>(this T? x, Func<T, U?> fn)
where T : class
where U : class {
return x switch {
null => null,
T it => fn(it)
};
}
}
}
To create an extension method, we create a static class, and add it as a static method within the class. Then we add the type we are extending as a method parameter with a this
modifier. Since we want Map
and FlatMap
to apply to any nullable type, we use generics types, and indicate that they're nullable by adding a ?
after the type. Here, the type we are extending is T?
. For now, we're only handling nullable reference types, so we add the : class
generic constraint to our types.
The new switch expression is kind of like combining the regular switch with C#'s lambda syntax, were each case is it's own lambda function. We match on x
and if it's null we pass it on, if not we apply the function argument to it.
Tests
We'll use xUnit and the incredibly clever and well thought out Person
and Car
types below to test our extensions.
class Person {
public string FirstName { get; }
public string LastName { get; }
public Person(string first, string last) {
FirstName = first;
LastName = last;
}
}
class Car {
public Person? Driver { get; set; }
public Car() {}
public Car(Person driver) {
Driver = driver;
}
public Car? Wreck(bool isTotaled) =>
isTotaled switch {
true => null,
false => new Car()
};
}
Map
For Map
we just need to make sure that if the value Map
is called on is null, the result of the Map
method is also null. If the value isn't null, the result of Map
is the result of the lambda passed to it.
[Fact]
public void MapTest() {
Person? person = null;
Car? car = person.Map(p => new Car(p));
Assert.Null(car);
Person? person2 = new Person("Garrett", "Van");
Car? car2 = person2.Map(p => new Car(p));
Assert.Equal("Garrett", car2?.Driver?.FirstName);
Assert.Equal("Van", car2?.Driver?.LastName);
}
FlatMap
In our FlatMap
test, if the value we are flat mapping is null, we pass it along, just as we did in Map
. Unlike Map
, the result of our lambda in FlatMap
can return null itself. To account for it, we need to make sure that if the flat mapped value is not null, but the result of the lambda is null, the overall result is null. Only if neither the flat mapped value, nor the result of the lambda is null do we get a non-null result.
[Fact]
public void FlatMapTest() {
Person? person = null;
Car? car = person.FlatMap(p => {
return new Car(p).Wreck(false);
});
Assert.Null(car);
Car? car2 = person.FlatMap(p => {
return new Car(p).Wreck(true);
});
Assert.Null(car2);
Person? person2 = new Person("Garrett", "Van");
Car? car3 = person2.FlatMap(p => {
return new Car(p).Wreck(true);
});
Assert.Null(car3);
Car? car4 = person2.FlatMap(p => {
return new Car(p).Wreck(false);
});
Assert.NotNull(car4);
}
With or without these lovely extension methods, I'm really excited about these new features. My biggest beef with null has always been that it seemed like a hack in a type system. Nullable reference types are a step in the right direction.
Reference: Visual Studio 2019, nullable reference types, switch expressions, Dealing with the Absence of Value, xUnit