Heresy in C#: Nullable Reference Extensions

C# 8 here I come

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