Monad Transformers in C#

1.8k views Asked by At

I am working on using monad transformers in C#.
I would like to know if the following code I present, shows that I have understood this.
I am fairly new to this so any feedback / comments are really welcome.
This example is just for wrapping a maybe monad in a validation monad.

using System;
using NUnit.Framework;

namespace Monads
{
    public static class MaybeExtensions
    {
        public static IMaybe<T> ToMaybe<T>(this T value)
        {
            if (value == null)
                return new None<T>();

            return new Just<T>(value);
        }
    }

    public interface IMaybe<T>
    {
        IMaybe<U> Select<U>(Func<T, U> f);

        IMaybe<U> SelectMany<U>(Func<T, IMaybe<U>> f);

        U Fold<U>(Func<U> error, Func<T, U> success);
    }

    public class Just<T> : IMaybe<T>
    {
        public Just(T value)
        {
            this.value = value;

        }

        public IMaybe<U> Select<U>(Func<T, U> f)
        {
            return f(value).ToMaybe();
        }

        public IMaybe<U> SelectMany<U>(Func<T, IMaybe<U>> f)
        {
            return f(value);
        }

        public U Fold<U>(Func<U> error, Func<T, U> success)
        {
            return success(value);
        }

        public IValidation<U, T> ToValidationT<U>()
        {
            return new ValidationMaybeT<U, T>(this, default(U));
        }

        private readonly T value;
    }

    public class None<T> : IMaybe<T>
    {
        public IMaybe<U> Select<U>(Func<T, U> f)
        {
            return new None<U>();
        }

        public IMaybe<U> SelectMany<U>(Func<T, IMaybe<U>> f)
        {
            return new None<U>();
        }

        public U Fold<U>(Func<U> error, Func<T, U> success)
        {
            return error();
        }

        public IValidation<U, T> ToValidationT<U>(U exceptionalValue)
        {
            return new ValidationMaybeT<U, T>(this, exceptionalValue);
        }
    }

    public class Customer
    {
        public Customer(string name)
        {
            Name = name;
        }

        public string Name { get; set; }
    }

    public interface IValidation<T, U>
    {
        IValidation<T, V> Select<V>(Func<U, V> f);

        IValidation<T, V> SelectMany<V>(Func<U, IValidation<T, V>> f);
    }

    public class ValidationError<T, U> : IValidation<T, U>
    {
        public ValidationError(T error)
        {
            Error = error;
        }

        public IValidation<T, V> Select<V>(Func<U, V> f)
        {
            return new ValidationError<T, V>(Error);
        }

        public IValidation<T, V> SelectMany<V>(Func<U, IValidation<T, V>> f)
        {
            return new ValidationError<T, V>(Error);
        }

        public T Error { get; private set; }
    }

    public class ValidationSuccess<T, U> : IValidation<T, U>
    {
        public ValidationSuccess(U value)
        {
            Result = value;
        }

        public IValidation<T, V> Select<V>(Func<U, V> f)
        {
            return new ValidationSuccess<T, V>(f(Result));
        }

        public IValidation<T, V> SelectMany<V>(Func<U, IValidation<T, V>> f)
        {
            return f(Result);
        }

        public U Result { get; private set; }
    }

    public class ValidationMaybeT<T, U> : IValidation<T, U>
    {
        public ValidationMaybeT(IMaybe<U> value, T error)
        {
            Value = value;
            Error = error;
        }

        public IValidation<T, V> Select<V>(Func<U, V> f)
        {
            return Value.Fold<IValidation<T, V>>(() => new ValidationError<T, V>(Error), s => new ValidationSuccess<T, V>(f(s)));
        }

        ValidationError<T, V> SelectManyError<V>()
        {
            return new ValidationError<T, V>(Error);
        }

        public IValidation<T, V> SelectMany<V>(Func<U, IValidation<T, V>> f)
        {
            return Value.Fold(() => SelectManyError<V>(), s => f(s));
        }

        public IMaybe<U> Value { get; private set; }

        public T Error { get; private set; }
    }

    public interface ICustomerRepository
    {
        IValidation<Exception, Customer> GetById(int id);
    }

    public class CustomerRepository : ICustomerRepository
    {
        public IValidation<Exception, Customer> GetById(int id)
        {

            if (id < 0)
                return new None<Customer>().ToValidationT<Exception>(new Exception("Customer Id less than zero"));

            return new Just<Customer>(new Customer("Structerre")).ToValidationT<Exception>();
        }
    }

    public interface ICustomerService
    {
        void Delete(int id);
    }

    public class CustomerService : ICustomerService
    {
        public CustomerService(ICustomerRepository customerRepository)
        {
            this.customerRepository = customerRepository;

        }

        public void Delete(int id)
        {
            customerRepository.GetById(id)
                .SelectMany(x => SendEmail(x).SelectMany(y => LogResult(y)));


        }

        public IValidation<Exception, Customer> LogResult(Customer c)
        {
            Console.WriteLine("Deleting: " + c.Name);
            return new ValidationSuccess<Exception, Customer>(c);
            //return new ValidationError<Exception, Customer>(new Exception("Unable write log"));
        }

        private IValidation<Exception, Customer> SendEmail(Customer c)
        {
            Console.WriteLine("Emailing: " + c.Name);
            return new ValidationSuccess<Exception, Customer>(c);
        }

        ICustomerRepository customerRepository;
    }

    [TestFixture]
    public class MonadTests
    {
        [Test]
        public void Testing_With_Maybe_Monad()
        {
            new CustomerService(new CustomerRepository()).Delete(-1);
        }
    }
}

Another smaller sub question is if C# had higher kinded types could I just implement this class once (ValidationT) and it work for all other wrapped monads or is this incorrect?

1

There are 1 answers

0
louthster On

Almost, is the quickest answer. Your ValidationMaybeT is storing the value of the Maybe, whereas a true monad transformer would have the behaviour of the Maybe and the Validation monad, and could modify the default behaviour of the wrapped monad if required.

This is a very manual way of doing it, which I wouldn't necessarily recommend, it gets very messy, very quickly. C#'s lack of higher-kinded polymorphism will trip you up at every opportunity.

The closest I managed (even then it's not a proper monad transformer system) is with my library: Language-Ext

There are 13 monads in the project (Option, Map, Lst, Either, Try, Reader, etc.), and I implement a standard set of functions for all of them:

Sum      
Count    
Bind     
Exists   
Filter   
Fold     
ForAll   
Iter     
Map      
Select
SeletMany
Where
Lift

These functions are the most useful in functional programming, and will pretty much allow you to do any operation needed.

So with all monads implementing these standard functions, they become a higher-kinded type. Not that the compiler knows this, they are all just part of the same 'set'.

Then I wrote a T4 template to generate transformer functions as extension methods (they have a T suffix), for every combination of monad and function in the 'higher-kinded type'.

So for example:

var list = List(Some(1),None,Some(2),None,Some(3));
var total = list.SumT();

The code above results in 6. The definition for SumT is:

int SumT(Lst<Option<int>> self) => 
    self.Map( s => s.Sum() ).Sum();

FilterT for example will also work on the inner monad:

var list = List(Some(1),None,Some(2),None,Some(3));
list = list.FilterT(x => x > 2);

So the extension method route is a very good one. Instead of creating a new type, use:

IValidation<IMaybe<T>>

Then provide the Maybe extension methods for IValidation<IMaybe<T>>

You can either do what I did and auto-generate from a standard set, or write them manually. It then keeps your Maybe and Validation implementations clean and the bespoke transformer functionality separate.

If you're interested, this is the T4 template I used to generate the transformer methods (it's pretty ramshackle to be honest): LanguageExt.Core/HKT.tt

And this is the generated code: LanguageExt.Core/HKT.cs

Before I did the HKT stuff above I did a similar method to what you're attempting, I have a monad called TryOption<T> which is a Try and an Option. But with the new HKT stuff I can now write Try<Option<T>>. The original implementation is here:

Anyway, I hope that helps!