Tired of Null references? abolish them with Maybe

31 Dec 2011 in software-development | dotnet | patterns |

Are you tired of null checks in your C# code? if (blahdiblah != null) ? There are several ways out

  • Code in a different language
  • Live with it
  • Put some guidance on your code like e.g. NotNull attributes to give devs guidelines when coding against some API of yours
  • Make the idea of not having a value a first-class citizen in your code

This post deals with the 4th point. My inspiration came from a brief look into F# and some coding with it. In there you can modify your type to be something like Option(Of T) instead of T. It says that there may be no value of type T, and your code must deal with that sort of thing:

let att = x.GetCustomAttributes(true) |> Seq.tryFind is<TypeConverterAttribute>
match att with
| Some(att) -> Some(att |> instantiateTypeConverter)
| _ -> None

So, instead of writing a method that may return null, you write a method that returns an Option, or Maybe of some type. Its structure is similar to the Nullable(Of T) in that you can ask a Maybe whether it contains a value and gives you access to the Value.

An implementation of Maybe can be found in my current pet project, DynamicXaml. The first version only worked for class types, but I found this rather limiting. While structs cannot point to nothing (since they don’t point at all), the concept of not having a value is still useful for struct types (that’s why Nullable is in place, D’oh!). Hence, the current implementation also takes structs. The standard constructor will say it has a value when the provided value deviates from default(T)’s response. You can override that by stating yourself whether you have a value or not.

Also note that the current implementation is a struct, whose semantics are a good match to what a Maybe-_value_ represents.

Two extension methods allow quick transposition to the Maybe “type system” which work against class types and Nullable types.

int? x = null;
Maybe<int> maybe = x.ToMaybe();

string y = "";
Maybe<string> maybe2 = y.ToMaybe();

Then comes Get and Do which covers a lot of ground in the Maybe domain, namely assigning and executing something. It allows you to write code similar to the following:

_invokeMemberHandler.MaybeFirst(h => h.CanHandle(this)).Do(h => h.Handle(this));

See the MaybeFirst extension methods which works like FirstOrDefault from the LINQ namespace but returns a Maybe-value. All operations that occur within the Maybe domain essentially perform a side-effect when the Maybe value in question has a value. The side effect is the lambda you specify.

In framework-type code, operations that check whether an object has certain properties seem to be quite common-place. For this, the Cast-extension methods appears to be rather useful. It tries to cast a Maybe(Of T) to a Maybe(Of U). If the input has a value and can be cast to U, the resulting maybe contains a value.

The following code uses yet another extension method that takes in an array of operations in the Maybe-Domain and execute them all until one operation returns a Maybe with a value:

public static Maybe<object[]> NormalizeToBuiltXaml(this InvokeContext ctx, Func<InvokeContext,object> rootObjectSelector)
{
    var value = rootObjectSelector(ctx);

    return value.Maybe(
        v => v.Cast<object[]>(),
        v => v.Cast<Func<XamlBuilder, Xaml>>()
                .Get(func => func(ctx.Builder).Create())
                .Get(obj => new[] { obj }),
        v => v.Cast<Func<XamlBuilder, Xaml[]>>()
                .Get(func => func(ctx.Builder))
                .Get(xamls => xamls.Select(x => x.Create()).ToArray()),
        v => v.Get(obj => new [] { obj })
    );
}

It is a normalization method that accepts a multitude of different input types and processes them to the same output type. See how natural the output is also a Maybe? If nothing matched, subsequent operations (side-effects) will not be executed.

If you want to transpose back from the maybe-domain to the “normal” types, you can use the MustHaveValue extension methods. The standard one will throw an exception if there is no value, while the second takes a value that will be used as return if the maybe contains no value.

As a final example, see the following code:

public Maybe<DataTemplate> FindForType(Type type)
{
    return _interfaceTemplates.Get(type).Or(_root.FindFor(type));
}

// using...
public static Maybe<U> Get<T,U>(this IDictionary<T,U> dictionary, T key)
{
    U value;
    var success = dictionary.TryGetValue(key, out value);
    return new Maybe<U>(value, success);
}

Compare this to

public DataTemplate FindForType(Type type)
{
    DataTemplate dt;
    if (_interfaceTemplates.TryGetValue(type, out dt))
      return dt;
    return _root.FindFor(type); // Assuming this returns a DataTemplate
}

While the standard write-up is also fairly readable, an API consumer still fails to see whether the return value may be null or not.

And this may be the most important conclusion point of this incoherent end-of-year blog post. If you want to, you can make just about any implicit assumption of your code explicit. This one way here shows how the fact that there may not be a value available from an operation can be made explicit.

Chronology

  |  
comments powered by Disqus