Lets build a monad library in C# - Maybe

in #programming7 years ago (edited)

Apart from the Identity monad, the Maybe monad is probably the easiest to understand so I will start there. In C# think Nullable but able to wrap reference types as well as reference types. I will also forgo the standard monad return/bind semantics in favor of full LINQ integration.

If you missed it you can find the Introduction Here.

This means the following.

  • ToMaybe() and ToSome() in place of return, construction semantics.
  • SelectMany() in place of bind/flatmap/fmap, it is the same signature.
  • Select() to complete LINQ integration, this is Map.

We start with an interface:

using System;

namespace Woz.Monads
{
    public interface IMaybe<T>
    {
        bool HasValue { get; }

        T Value { get; }

        IMaybe<TResult> Select<TResult>(
            Func<T, TResult> selector);

        IMaybe<TResult> SelectMany<TResult>(
            Func<T, IMaybe<TResult>> selector);

        IMaybe<TResult> SelectMany<T2, TResult>(
            Func<T, IMaybe<T2>> selector, 
            Func<T, T2, TResult> projection);

        IMaybe<T> Where(Func<T, bool> predicate);

        TResult Match<TResult>(
            Func<T, TResult> someSelector,
            Func<TResult> noneSelector);

        IMaybe<T> Recover(T defaultValue);

        IMaybe<T> Recover(Func<T> defaultFactory);

        void Do(Action<T> action);
    }
}

There is a lot going on here so I will detail each part.

  • HasValue - true/false to indicate if the monad has a value. true = Some(value), false = None
  • Value - Returns the wrapped value or throws an exception when no value in place.
  • Select - This is map. Operates the same as IEnumerable.Select() but only runs when there is a value.
  • SelectMany - This is bind. Operates the same as IEnumerable.SelectMany() but only runs when there is a value.
  • Where - This operates like an if statement. If the predicate is true maintains the monad state otherwise returns a None.
  • Match - A way to trigger an operation against the Some(value) or None and return a value.
  • Recover - A way to get back to a Some(value) when the monad is None.
  • Do - Triggers the action if the monad is Some(value).

So lets look at the Some class so you can see what is going on here. Most of this should make sense. We already know we have a value so we can just operate on it with the semantics of the interface.

using System;

namespace Woz.Monads
{
    internal class Some<T> : IMaybe<T>
    {
        internal Some(T value)
        {
            Value = value;
        }

        public bool HasValue => true;

        public T Value { get; }

        public IMaybe<TResult> Select<TResult>(
            Func<T, TResult> selector) 
            => selector(Value).ToMaybe();

        public IMaybe<TResult> SelectMany<TResult>(
            Func<T, IMaybe<TResult>> selector) 
            => selector(Value);

        public IMaybe<TResult> SelectMany<T2, TResult>(
            Func<T, IMaybe<T2>> selector, Func<T, T2, TResult> projection) 
            => selector(Value).SelectMany(value => projection(Value, value).ToMaybe());

        public IMaybe<T> Where(Func<T, bool> predicate) 
            => predicate(Value) ? this : None<T>.Default;

        public TResult Match<TResult>(
            Func<T, TResult> someSelector, Func<TResult> noneSelector) 
            => someSelector(Value);

        public IMaybe<T> Recover(T defaultValue) => this;

        public IMaybe<T> Recover(Func<T> defaultFactory)
            => this;

        public void Do(Action<T> action) 
             => action(Value);
    }
}

Next we explore None. This is the implementation of when we have no value, in all cases it propagates no value reshaping as required.

using System;

namespace Woz.Monads
{
    internal class None<T> : IMaybe<T>
    {
        internal static readonly IMaybe<T> Default = new None<T>();

        private None() {}

        public T Value
        {
            get { throw new InvalidOperationException("No value"); }
        }

        public bool HasValue => false;

        public IMaybe<TResult> Select<TResult>(
            Func<T, TResult> selector)
            => None<TResult>.Default;

        public IMaybe<TResult> SelectMany<TResult>(
            Func<T, IMaybe<TResult>> selector)
            => None<TResult>.Default;

        public IMaybe<TResult> SelectMany<T2, TResult>(
            Func<T, IMaybe<T2>> selector, Func<T, T2, TResult> projection)
            => None<TResult>.Default;

        public IMaybe<T> Where(Func<T, bool> predicate)
            => this;

        public TResult Match<TResult>(Func<T, TResult> someSelector, Func<TResult> noneSelector)
            => noneSelector();

        public IMaybe<T> Recover(T defaultValue)
            => defaultValue.ToMaybe();

        public IMaybe<T> Recover(Func<T> defaultFactory)
            => defaultFactory().ToMaybe();

        public void Do(Action<T> action) {}
    }
}

The final pieces of the puzzle are some statics to construction. This is split into a generic and non generic class this is because I prefer the non-generic class semantics for the construction but the generic class is required to support the None property.

namespace Woz.Monads
{
    public static class Maybe
    {
        public static IMaybe<T> ToSome<T>(this T value)
            => new Some<T>(value);

        public static IMaybe<T> ToMaybe<T>(this T value)
            => value == null ? Maybe<T>.None : new Some<T>(value);
    }

    public static class Maybe<T>
    {
        public static IMaybe<T> None => None<T>.Default;
    }
}

I will show some usage in the next article and also push this to a git repo when I get a chance. I have written this on my Surface which only has VS2015. I will port this to .NET Core in VS2017 on my main dev box when I get a chance.

For now, here is a quick example of usage.

// Wrap some values
var someA = 5.ToSome();
var someB = "Hello".ToSome();
var none = Maybe<int>.None;

// someValue is the string "5 Hello"
var someResult =
    from intValue in someA
    from strValue in someB
    select $"{intValue} {strValue}";

// noneValue is None because one of the monads is a None
var noneValue =
    from intNone in none
    from strValue in someB
    select $"{intNone} {strValue}";

As you can see, no null/none checks that is all abstracted away via LINQ.

Feel free to ask any questions

Happy coding

Woz

Sort:  

My account was hacked, but I have now regained control. I have reset all my keys and am editing out dangerous links: please don't click on any links you see in my comments! Please help me recover by removing this flag if possible when you see this message.