During the last couple of weeks I've been very interested in using a Maybe monad with AutoFixture's Kernel code, but although many examples can be found on the internet, they remain samples. Rinat Abdullin and Zack Owens both posted samples, but I particularly like Mike Hadlow's series about Monads in C# because he also explains how to use LINQ with monads such as the Maybe monad.

As I really wanted a Maybe monad for AutoFixture, I first thought about simply implementing it directly in the AutoFixture source. However, I found it too arbitrary to put such a general purpose programming construct into a specific library such as AutoFixture. My next thought was to create a small open source project just for that single purpose, but then I though about the problem a bit more…

The BCL sort of already has a Maybe monad - you just need to recognize it as such.

What is a Maybe monad really? If you really distill it, it's just a type that either contains a value, or doesn't contain a value. In other words, it's a type that represents a particular range: a set with either zero or one items. That's just a special case of a more general range or collection, and we already have LINQ covering those constructs.

Here it is: the Maybe monad from the BLC (encapsulated in a nice extension method):

public static class LightweightMaybe
{
    public static IEnumerable<T> Maybe<T>(this T value)
    {
        return new[] { value };
    }
}

Obviously, this method returns a Maybe with a value, but we can just as easily represent Nothing with an empty array.

With my ‘new' Maybe monad, I can now write code like this (where request is a System.Object instance):

return (from t in request.Maybe().OfType<Type>()
        let typeArguments = t.GetGenericArguments()
        where typeArguments.Length == 1
        && typeof(IList<>)
            == t.GetGenericTypeDefinition()
        select context.Resolve(typeof(List<>)
            .MakeGenericType(typeArguments)))
        .DefaultIfEmpty(new NoSpecimen(request))
        .SingleOrDefault();

You may think that this looks dense, but before that the code looked like this:

var type = request as Type;
if (type == null)
{
    return new NoSpecimen(request);
}
 
var typeArguments = type.GetGenericArguments();
if (typeArguments.Length != 1)
{
    return new NoSpecimen(request);
}
 
if (typeof(IList<>) != 
    type.GetGenericTypeDefinition())
{
    return new NoSpecimen(request);
}
 
return context.Resolve(typeof(List<>)
    .MakeGenericType(typeArguments));

Notice that in this more traditional approach involving Guard Clauses, I have to construct a new NoSpecimen object in three different places, thus violating the DRY principle. I like not having all those if/return blocks in the code.


Comments

That's a very neat idea. Now I think of it, it's a pattern you see used a lot in Haskell too.

In your first code blog, rather than returning:

return new[]{value};

It would be nicer to do this:

return Enumerable.Single(value);

:)
2011-02-04 20:38 UTC
Yes, I believe the concept of a Maybe monad originates from Haskell or a similar language (but I can't remember the specific details).

Using Enumerable.Single(value) will not work because it takes an IEnumerable<T> and returns a T. We want the exact opposite: take a T and return IEnumerable<T>.
2011-02-04 21:33 UTC
Cant say I know it inside out have never used DefaultIfEmpty IRL, but should the .SingleOrDefault be a .Single() or a [0] ? (I'd much favor a .Single() to be honest)

@other commenter: Enumerable.Repeat(value,1) does the trick you want. I sometimes cruft up a .One helper method, but I believe there's a more accepted name for it in the excellent RealWorldFunctionalProgramming in C# and F# book I dont have to hand (The one that makes Mark's head hurt :D)
2011-02-04 23:29 UTC
Yes, you are right - Single() is enough. My mistake :)

It's true that there are many ways to create an IEnumerable with a single element.
2011-02-05 08:06 UTC
I agree that this functional approach is DRY, but I still find it a bit hard to follow. Instead, why not simply refactor the imperative code to a DRY, more intend revealing (but still imperative) version. This is what I propose:

var type = request as Type;

bool requestIsAType = type != null;
bool withOneGenericArgument = requestIsAType && type.GetGenericArguments().Length == 1;
bool isAGenericList = requestIsAType && type.GetGenericTypeDefinition() == typeof(IList<>);

if (requestIsAType && isAGenericList && withOneGenericArgument)
{
return context.Resolve(typeof(List<>).MakeGenericType(type.GetGenericArguments()));
}
else
{
return new NoSpecimen(request);
}

Doesn’t this just read like a functional spec? “When the request is a Type of a generic list with one generic generic argument, than … otherwise …”.

Cheers
2011-02-05 09:07 UTC
Did you test that code? I'm pretty sure it has defects.

If you call GetGenericTypeDefinition() on a type which is not generic, an exception will be thrown. This could happen if, for instance, I were to invoke the method with request = typeof(object).

If you want to play around with this, just pull the AutoFixture source and revert to revision 391 and try it out on the ListRelay class. It has pretty comprehensive test coverage.
2011-02-05 09:31 UTC
#
Did I test that code? Of course not! ;-) Just trying to prove what a bit of refactoring can do :-)
2011-02-05 22:50 UTC
Yes, but the point is that it's those little things that end up making a more procedural refactoring less than readable. In any case, 'readability' of code is highly subjective so obviously YMMV.
2011-02-06 09:35 UTC
3P #
For me the LINQ version is almost unreadable. If it makes me more then few seconds to understand the code I think that code is not finished. Putting sth in one line is not "Clean Code" I think.
2011-04-03 19:03 UTC
I came late to comment due to tweets exchange with author. But I want add that treating null as something that should semantically avoided in code, it's not only a matter of readability but also a symptom of good design. Totally agree with Mark Seemann that kindly supplied also this Maybe monad implementation. Excellent work.
2013-03-06 08:18 UTC


Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Friday, 04 February 2011 13:11:34 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Friday, 04 February 2011 13:11:34 UTC