Empty for a reason
November 7, 2014 Leave a comment
So far I have found Option<T> for reference types to be the best way of avoiding null reference exceptions while keeping my code very readable. I use it when parsing strings, retrieving values from dictionaries, getting properties from object via reflection and more. Option<T> allows one to write very clean code without any risk of null reference exceptions. To illustrate this point, consider the following code:
string r = null; if (someObject != null) { var value = someObject.GetPropertyValue<string>("P"); int i; if (value != null && int.TryParse(value, out i)) { dict.TryGetValue(i, out r); } }
which, using Option<T> and associated LINQ support can be rewritten as
var r = from o in someObject.AsOption() from s in o.GetPropertyValue<string>("P") from i in s.ParseAsInt() from v in i.Lookup(dict) select v;
on the first line we have an empty Option if someObject is null. The first from extracts the actual object if the Option is not empty, the second one retrieves a property value if the property exists and if it is of type string and if the value is not null, then we try to parse the string and get an integer value if the parsing is successful, finally we look up a dictionary and again get a value only if the dictionary holds the key. So all in all we have six different reasons to end up with an empty result. The main goal of Option is to avoid null reference exceptions, but quite often, in particular while debugging, one is actually interested in why the result is empty, is it because the dictionary didn’t contain the key? because the string didn’t represent an integer? etc. As the code stands there is no way of knowing the actual reason without redoing the whole calculation one step at a time, typically by stepping through the code one step at a time.
An empty result is empty for one reason and only one reason. So the idea is that whenever a step returns an empty option the reason (a string) for returning empty is stored in the option. Here is the implementation of the methods used in the above example, you will see that each time an empty Option is returned, the reason for doing so is specified:
public static Option<T> AsOption<T>(this T value) { if (value is ValueType) return new Option<T>(value); if ((object)value == null) return Option.Empty<T>("value is null"); return new Option<T>(value); } public static Option<T> AsOption<T>(this T value, string reason) { if (value is ValueType) return new Option<T>(value); if ((object)value == null) return Option.Empty<T>(reason); return new Option<T>(value); } public static Option<R> GetPropertyValue<R>(this object value, string propertyName) { return from v in value.AsOption() from pi in v.GetPropertyInfo(propertyName) from cast in pi.GetValue(v).AsOption().As<R>() select cast; } public static Option<PropertyInfo> GetPropertyInfo<T>(this T value, string propertyName) { return from v in value.AsOption() from p in v.GetType() .GetProperty(propertyName) .AsOption("Property " + propertyName + " not found in " + v.GetType().Name) select p; } public Option<R> As<R>() { return this.HasValue && value is R ? ((R)(object)value).AsOption() : Option.Empty<R>("Cannot cast " + typeof(T).Name + " to " + typeof(R).Name); } public static Option<int> ParseAsInt(this string source) { int value; return int.TryParse(source, out value) ? value.AsOption() : Option.Empty<int>("Input string cannot be parsed:" + source); } public static IOption<V> Lookup<K, V>(this K key, Dictionary<K,V> dict) { V value; return dict.TryGetValue(key, out value) ? new Option<V>(value) : Option.Empty<V>("Key not found:" + key.ToString()); }
So now, if the result of our query is empty, we can retrieve the reason for it being empty from the Option instance in the form of a string. Note that this was achieved without making a single modification to our code.
Let’s now have a look at how the other version of the code would have to be modified to achieve the same thing:
string reason = "null someObject"; string r = null; if (someObject != null){ reason = "Unable to retrieve property P in " + someObject.GetType().Name; var value = someObject.GetPropertyValue<string>("P").Value; int i; if (value != null) { reason = "Unable to parse " + value + " to an int"; if (int.TryParse(value, out i)){ reason = "Key not found:" + i.ToString(); dict.TryGetValue(i, out r); } } }
where I have avoided using else branches to keep the code compact and slightly more readable, but clearly there is no contest between the two versions.
So far I have found this useful mostly for logging and debugging purposes. But I wonder whether a strongly typed version of this could be used for other purposes…