How does the Maybe monad act as a short circuit?
I have an implementation of the maybe monad in c# that differs a little from yours, first of all it's not tied to null checks, I believe my implementation more closesly resembles what happens in a standard maybe implementation in for example Haskel.
My implementation:
public abstract class Maybe<T>
{
public static readonly Maybe<T> Nothing = new NothingMaybe();
public static Maybe<T> Just(T value)
{
return new JustMaybe(value);
}
public abstract Maybe<T2> Bind<T2>(Func<T, Maybe<T2>> binder);
private class JustMaybe
: Maybe<T>
{
readonly T value;
public JustMaybe(T value)
{
this.value = value;
}
public override Maybe<T2> Bind<T2>(Func<T, Maybe<T2>> binder)
{
return binder(this.value);
}
}
private class NothingMaybe
: Maybe<T>
{
public override Maybe<T2> Bind<T2>(Func<T, Maybe<T2>> binder)
{
return Maybe<T2>.Nothing;
}
}
}
As you see here the bind function of the NothingMaybe just returns a new nothing so passed in binder expression is never evaluated. It's short circuiting in the sense that no more binder expressions will be evaluated once you got into the "nothing state", however the Bind-function itself will be invoked for each monad in the chain.
This implementation of maybe could be used for any type of "uncertain operation", for example a null check or checking for an empty string, this way all those different types of operations can be chained together:
public static class Maybe
{
public static Maybe<T> NotNull<T>(T value) where T : class
{
return value != null ? Maybe<T>.Just(value) : Maybe<T>.Nothing;
}
public static Maybe<string> NotEmpty(string value)
{
return value.Length != 0 ? Maybe<string>.Just(value) : Maybe<string>.Nothing;
}
}
string foo = "whatever";
Maybe.NotNull(foo).Bind(x => Maybe.NotEmpty(x)).Bind(x => { Console.WriteLine(x); return Maybe<string>.Just(x); });
This would print "whatever" to the console, however if the value was null or empty it would do nothing.
As I understand it, all Bind
methods will be invoked, but the provided expressions will be evaluated only if the previous one returns a value. This means that Bind
methods that are invoked after one that returns null
(or more correctly: default(T)
) will be very cheap.