Monads explained in C#

The newer and much longer version of this article is now available: Monads explained in C# (again)

It looks like there is a mandatory post that every blogger who learns functional programming should write: what a Monad is. Monads have the reputation of being something very abstract and very confusing for every developer who is not a hipster Haskell programmer. They say that once you understand what a monad is, you loose the ability to explain it in simple language. Doug Crockford was the first one to lay this rule down, but it becomes kind of obvious once you read 3 or 5 explanations on the web. Here is my attempt, probably doomed to fail :)

Monads are container types

Monads represent a class of types which behave in the common way.

Monads are containers which encapsulate some kind of functionality. On top of that, they provide a way to combine two containers into one. And that’s about it.

The goals of monads are similar to generic goals of any encapsulation in software development practices: hide the implementation details from the client, but provide a proper way to use the hidden functionality.

It’s not because we want to be able to change the implementation, it’s because we want to make the client as simple as possible and to enforce the best way of code structure. Quite often monads provide the way to avoid imperative code in favor of functional style.

Monads are flexible, so in C# we could try to represent a monadic type as a generic class:

public class Monad<T>
{
}

Monad instances can be created

Quite an obvious statement, isn’t it. Having a class Monad<T>, there should be a way to create an object of this class out of an instance of type T. In functional world this operation is known as Return function. In C# it can be as simple as a constructor:

public class Monad<T>
{
    public Monad(T instance)
    {
    }
}

But usually it makes sense to define an extension method to enable fluent syntax of monad creation:

public static class MonadExtensions
{
    public static Monad<T> Return<T>(this T instance) => new Monad<T>(instance);
}

Monads can be chained to create new monads

This is the property which makes monads so useful, but also a bit confusing. In functional world this operation is expressed with the Bind function (or >>= operator). Here is the signature of Bind method in C#:

public class Monad<T>
{
    public Monad<TO> Bind<TO>(Func<T, Monad<TO>> func)
    {
    }
}

As you can see, the func argument is a complicated thing. It accepts an argument of type T (not a monad) and returns an instance of Monad<TO> where TO is another type. Now, our first instance of Monad<T> knows how to bind itself to this function to produce another instance of monad of the new type. The full power of monads comes when we compose several of them in one chain:

initialValue
    .Return()
    .Bind(v1 => produceV2OutOfV1(v1))
    .Bind(v2 => produceV3OutOfV2(v2))
    .Bind(v3 => produceV4OutOfV3(v3))
    //...

Let’s have a look at some examples.

Example: Maybe (Option) type

Maybe is the 101 monad which is used everywhere. Maybe is another approach to dealing with ‘no value’ value, alternative to the concept of null. Basically your object should never be null, but it can either have Some value or be None. F# has a maybe implementation built into the language: it’s called option type. Here is a sample implementation in C#:

public class Maybe<T> where T : class
{
    private readonly T value;

    public Maybe(T someValue)
    {
        if (someValue == null)
            throw new ArgumentNullException(nameof(someValue));
        this.value = someValue;
    }

    private Maybe()
    {
    }

    public Maybe<TO> Bind<TO>(Func<T, Maybe<TO>> func) where TO : class
    {
        return value != null ? func(value) : Maybe<TO>.None();
    }

    public static Maybe<T> None() => new Maybe<T>();
}
public static class MaybeExtensions
{
    public static Maybe<T> Return<T>(this T value) where T : class
    {
        return value != null ? new Maybe<T>(value) : Maybe<T>.None();
    }
}

Return function is implemented with a combination of a public constructor which accepts Some value (notice that null is not allowed) and a static None method returning an object of ‘no value’. Return extension method combines both of them in one call.

Bind function is implemented explicitly.

Let’s have a look at a use case. Imagine we have a traditional repository which loads data from an external storage (no monads yet):

public interface ITraditionalRepository
{
    Customer GetCustomer(int id);
    Address GetAddress(int id);
    Order GetOrder(int id);
}

Now, we write a client class which loads data one by one and tries to find a shipper:

Shipper shipperOfLastOrderOnCurrentAddress = null;
var customer = repo.GetCustomer(customerId);
if (customer?.Address != null)
{
    var address = repo.GetAddress(customer.Address.Id);
    if (address?.LastOrder != null)
    {
        var order = repo.GetOrder(address.LastOrder.Id);
        shipperOfLastOrderOnCurrentAddress = order?.Shipper;
    }
}
return shipperOfLastOrderOnCurrentAddress;

Note, that the code assumes that repository returns null if some entity is not found, although nothing in the type system shows that. Then, there is a number of null checks (facilitated with elvis operator). The code gets a bit cluttered and less linear.

Here is an alternative repository which returns Maybe type:

public interface IMonadicRepository
{
    Maybe<Customer> GetCustomer(int id);
    Maybe<Address> GetAddress(int id);
    Maybe<Order> GetOrder(int id);
}

The contract is more explicit: you see that Maybe type is used, so you will be forced to handle the case of absent value.

And here is how the above example can be rewritten with Bind method composition:

Maybe<Shipper> shipperOfLastOrderOnCurrentAddress =
    repo.GetCustomer(customerId)
        .Bind(c => c.Address)
        .Bind(a => repo.GetAddress(a.Id))
        .Bind(a => a.LastOrder)
        .Bind(lo => repo.GetOrder(lo.Id))
        .Bind(o => o.Shipper);

There’s no branching anymore, the code is fluent and linear.

If you think that the syntax looks very much like a LINQ query with a bunch of Select statements, you are not the only one ;) One of the common implementations of Maybe implements IEnumerable interface which allows a more C#-idiomatic binding composition. Actually:

IEnumerable + SelectMany is a monad

IEnumerable is an interface for enumerable containers.

Enumerable containers can be created - thus the Return monadic operation.

The Bind operation is defined by the standard LINQ extension method, here is its signature:

public static IEnumerable<B> SelectMany<A, B>(
    this IEnumerable<A> first,
    Func<A, IEnumerable<B>> selector)

And here is an example of composition:

IEnumerable<Shipper> someWeirdListOfShippers =
    customers
        .SelectMany(c => c.Addresses)
        .SelectMany(a => a.Orders)
        .SelectMany(o => o.Shippers);

The query has no idea about how the collections are stored (encapsulated in containers). We use functions A -> IEnumerable<B> to produce new enumerables (Bind operation).

Monad laws

There are a couple of laws that Return and Bind need to adhere to, so that they produce a proper monad.

Identity law says that that Return is a neutral operation: you can safely run it before Bind, and it won’t change the result of the function call:

// Given
T value;
Func<T, M<U>> f;

// == means both parts are equivalent
value.Return().Bind(f) == f(value)

Associativity law means that the order in which Bind operations are composed does not matter:

// Given
M<T> m;
Func<T, M<U>> f;
Func<U, M<V>> g;

// == means both parts are equivalent
m.Bind(f).Bind(g) == m.Bind(a => f(a).Bind(g))

The laws may look complicated, but in fact they are very natural expectations that any developer has when working with monads, so don’t spend too much mental effort on memorizing them.

Conclusion

You should not be afraid of the “M-word” just because you are a C# programmer. C# does not have a notion of monads as predefined language constructs, but it doesn’t mean we can’t borrow some ideas from the functional world. Having said that, it’s also true that C# is lacking some powerful ways to combine and generalize monads which are possible in Haskell and other functional languages.


Cloud developer and researcher.
Software engineer at Pulumi. Microsoft Azure MVP.

comments powered by Disqus