C# : The null object pattern and the poor man's Maybe Monad
Lots of people have re-invented the Maybe monad in C#, as a simple search will show, usually as a way of bypassing null checking if
statements and to be able to write code like
return Music.GetCompany("4ad.com") | |
.IfNotNull(company => company.GetBand("Pixies")) | |
.IfNotNull(band => band.GetMember("David")) | |
.IfNotNull(member => member.Role); |
But we have the tools given to us already if we observe that null
has no type -- as we can see when
var x = new string[1]; // default null value | |
var n = x.OfType<string>().Count(); | |
Console.WriteLine("Length = {0}", n); |
reports a length of zero!
So our Some<T>
is just a length-1 IEnumerable<T>
; and None<T>
an empty one -- the null object pattern, in fact.
For the price of having to specify types at each stage -- as we would anyway have had to in the past for declaring intermediate the values to test against null -- we can use existing methods to finesse the null check. Returning to the borrowed example, it would be
return new[] { Music.GetCompany("4ad.com") } | |
.OfType<RecordCompany>().Select(company => company.GetBand("Pixies")) | |
.OfType<Band>().Select(band => band.GetMember("David")) | |
.OfType<Member>().Select(member => member.Role); |
The monadic return monad extraction operation is just FirstOrDefault()
; which is fine for reference types. Value types are different in any case as we wouldn't be null checking those -- as the later steps in a chain though, stopping at the filtering to the value type and making an Any
test may be preferred.
Looking at value types, switching to this example, the code looks like:
public static IEnumerable<int> Div(this int numerator, int denominator) | |
{ | |
//return denominator == 0 | |
// ? Enumerable.Empty<int>() | |
// : new[] { numerator/denominator }; | |
if (denominator != 0) yield return numerator/denominator; | |
} | |
public static IEnumerable<int> DoSomeDivision(int denominator) | |
{ | |
return from a in 12.Div(denominator) | |
from b in a.Div(2) | |
select b; | |
} | |
... | |
var result = | |
from a in new[] { "Hello World!" } | |
from b in DoSomeDivision(2) | |
from c in new[] { new DateTime(2010, 1, 14) } | |
select (a + " " + b.ToString() + " " + c.ToShortDateString()); | |
Console.WriteLine(result.Any() ? result.First() : "Nothing"); |
which leaks a little bit in that we have to do the monadic return monad extraction by hand at the end, but otherwise behaves exactly as the explicit monad implementation.
1 comment :
Interestingly, I posted exactly about this just around a week ago: http://bugsquash.blogspot.com/2011/10/poor-man-option-type-in-c.html
Just a couple of minor corrections: monadic return is the lifting of pure values into the monad, so it's actually the array constructor, not FirstOrDefault() (i.e a function T -> M instead of M -> T).
First() is better here than FirstOrDefault(), as it makes more patent the partiality of the function... just like Haskell's fromJust and F#/OCaml's Option.get , it fails when there is no value.
Post a Comment