My pleasure. I guess to help a bit more (because the list monad tends to be easier to comprehend) is to see how other types are implemented. So here's a very basic implementation of the Option/Maybe monad:
public class Option<T>
{
public readonly bool HasValue;
public readonly T Value;
internal Option(bool hasValue, T value)
{
HasValue = hasValue;
Value = value;
}
public Option<U> Select<U>(Func<T, U> map) =>
HasValue
? Option.Some<U>(map(Value))
: Option.None<U>();
public Option<V> SelectMany<U,V>(Func<T, Option<U>> bind, Func<T,U,V> project) =>
HasValue
? bind(Value).Select(u => project(Value,u))
: Option.None<V>();
}
public static class Option
{
public static Option<T> Some<T>(T value) =>
new Option<T>(true, value);
public static Option<T> None<T>() =>
new Option<T>(false, default(T));
}
The SelectMany implementation is slightly more complicated than I showed before. This is an optimisation that C# does to group the bind and map together. So it may look slightly scary as a function. But hopefully you can see that if the Option<T> has a value then it first invokes bind, then uses the result of the bind (an Option<U>) to project the final result. Here's a more imperative version of it:
public Option<V> SelectMany<U, V>(Func<T, Option<U>> bind, Func<T, U, V> project)
{
if (HasValue)
{
var u = bind(Value);
if (u.HasValue)
{
return Option.Some(project(Value, u.Value));
}
else
{
return Option.None<V>();
}
}
else
{
return Option.None<V>();
}
}
You can see that with the IEnumerable<T> version of Select and SelectMany it encapsulates list iteration. With the Option monad it doesn't do that. It instead checks the HasValue field, and if it's false then it doesn't run the map or bind functions.
The second static class: Option, contains the 'return' functions: Some or None. These wrap a value of type T in an Option<T>.
Now if we use Option<T> in a LINQ expression:
var option1 = Option.Some(10);
var option2 = Option.Some(10);
var none = Option.None<int>();
var res1 = from x in option1
from y in option2
select x + y;
// res1.HasValue == true res1.Value == 20
var res2 = from x in option1
from y in none
select x + y;
// res2.HasValue == false
var res3 = from x in none
from y in option2
select x + y;
// res3.HasValue == false
This is the same as using do notation in Haskell:
do x <- option1
y <- option2
return (x + y)
If we were to do that imperatively it would look like this:
var res = Option.None<int>();
if( option1.HasValue )
{
if( option2.HasValue )
{
res = Option.Some(option1.Value + option2.Value);
}
}
Clearly more cluttered and error prone and importantly, not composable. This is where the notion of 'programmable semi-colons' comes from. It appears that the monad is running behaviour 'between the lines', and it is.
Hopefully that clears the fog. I'll keep an eye on this thread for a few days, so feel free to drop any questions in here or on my project page.
The second static class: Option, contains the 'return' functions: Some or None. These wrap a value of type T in an Option<T>.
Now if we use Option<T> in a LINQ expression:
This is the same as using do notation in Haskell: If we were to do that imperatively it would look like this: Clearly more cluttered and error prone and importantly, not composable. This is where the notion of 'programmable semi-colons' comes from. It appears that the monad is running behaviour 'between the lines', and it is.Hopefully that clears the fog. I'll keep an eye on this thread for a few days, so feel free to drop any questions in here or on my project page.