10
\$\begingroup\$

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?

\$\endgroup\$
6
  • \$\begingroup\$ +1 for the effort, but I wonder if maybe the non-standard names will be confusing to people familiar with monads? For instance, it looks like your Select is often called map and SelectMany is often called bind. (Not a criticism, just an observation) \$\endgroup\$ Commented Dec 3, 2013 at 14:52
  • 3
    \$\begingroup\$ @MattFenwick: You are completely correct, however much of LINQ is inspired by monads and C# uses the terms Select and SelectMany instead of map and bind. It does this faithfully enough that anybody writing monadic code in C# should be quite comfortable using Select/SelectMany. \$\endgroup\$
    – Phoshi
    Commented Dec 3, 2013 at 15:00
  • 6
    \$\begingroup\$ I question whether monad transformers are a good idea in C#. For one, they already are semi-unpleasant to use in Haskell (Many people want a better system for combining effects) and two, the reason why they don't suck is because of the typeclass-y prolog we do to make it so that operations like get, put, and tell automagically propogate up a monad stack, this isn't possible in C#. You'd end up with the dreaded lift(lift(lift(foo))) \$\endgroup\$ Commented Dec 3, 2013 at 15:06
  • \$\begingroup\$ Just a note, your validation type can be generalized to the Either type, or disjoint union. \$\endgroup\$
    – Matt H
    Commented Dec 3, 2013 at 20:29
  • \$\begingroup\$ @jozefg Are you saying that because you have type classes in Haskell / Scala you do not need to manually convert like how I do with .ToTValidationT()? Secondly I am unsure what you mean by the operations get / put and lift? Can you further explain? \$\endgroup\$
    – Blair Davidson
    Commented Dec 4, 2013 at 0:59

2 Answers 2

1
\$\begingroup\$

There isn't much to review here. However I did find this:

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>();
    }
}

Here I find the condition would be best written like this:

if (id < 0)
{
    var exception = new Exception("Customer Id is less than zero");
    return new None<Customer>().ToValidationT<Exception>(exception);
}
else
{
    return new Just<Customer>(new Customer("Structerre")).ToValidationT<Exception>();
}

And as I typed this I noticed you were instantiating System.Exception. You should be using a more specific exception here, InvalidArgumentException seems appropriate - throwing System.Exception is bad.

Also the CustomerService is tied to the Console, you may prefer injecting some IServiceOutputProvider which could be implemented with console output, but just as well with a MessageBox output - basically CustomerService has too many responsibilities here, writing to the console has nothing to do with logging results or sending an email.

\$\endgroup\$
2
  • \$\begingroup\$ I don't agree with using if-else and bracers and stuff here, but throwing a more specific exception is good advice. +1 \$\endgroup\$ Commented Dec 19, 2013 at 23:38
  • \$\begingroup\$ @SimonAndréForsberg instantiating the exception on the same line as instantiating the None<T> and calling the .ToValidationT<T> is a little too much to put on a single line. It deserves a scope. Thus a proper else block. \$\endgroup\$ Commented Dec 19, 2013 at 23:43
1
\$\begingroup\$

Maybe

If you're writing a maybe monad, it's very informative to think about IEnumerable<T>. Maybe can be thought of as a special type of enumerable, which instead of having 0, 1 or many items, can only have 0 or 1 items.

Thought of that way, you can transform any IEnumerable<T> to a IMaybe<T> simply by calling .Take(1). So to start off, we can create a wrapped enumerable:

public class Maybe<T>
{
    internal IEnumerable<T> _inner;

    internal Maybe<T>(IEnumerable<T> inner)
    {
        _inner = inner.Take(1);
    }

    public T Value => _inner.First();
    public bool HasValue => _inner.Any();        
}

Now we have a Maybe, but we can't really do anything with it. I already decided that the constructor shouldn't be exposed publicly, its behaviour seems too confusing to expose directly to consumers.

LINQ-to-Objects can give us another hint here: static constructors and extension methods are likely to be very powerful. Let's start with:

public static class Maybe
{
    public static Maybe<T> None<T>()
    {
        return new Maybe<T>(Enumerable.Empty<T>());
    }

    public static Maybe<T> Some<T>(T value)
    {
        return new Maybe<T>(Enumerable.Repeat(value,1));
    }

    public static Maybe<U> Select(Maybe<T> source, Func<T,U> selector)
    {
        return new Maybe<T>(source._inner.Select(selector));
    }
}

Now we have a start, we can compare across to yours:

  • I picked "Some" instead of "Just" because I think it's a more conventional name for this
  • By making Select an extension method, we don't have to do a separate implementation for the cases of having and not having a value.
  • We're able to easily make use of LINQ for trivial implementations of most of the methods we want.
  • For free we get LINQ's lazy evaluation, which is idiomatic here

Probably the best way to decide what extension methods to include is to look at LINQ and decide what methods are applicable, and whether they need to be renamed. For example, Where is useful, but might be better named If. Concat is also useful if we think of it as Coalesce (in the same sense as null coalescing).

However, we're sort of cheating here. By making _inner internal, we can write the extension methods we need, but only in this project. By contrast, IEnumerable<T> lets anyone extend it by exposing the underlying properties needed to use it.

We could fix this by just exposing _inner, but this again is potentially confusing to consumers, and could be thought of as an implementation detail. It's somewhat a judgement call, but an alternative would be:

public class Maybe<T>
{
    // ...

    public Maybe<U> Map(Func<IEnumerable<T>,IEnumerable<U>> transform)
    {
        return new Maybe<U>(transform(_inner));
    }
}

Now our Select, for example, becomes:

public static Maybe<U> Select<T,U>(this Maybe<T> source, Func<T,U> selector)
{
    source.Map(_inner => _inner.Select(selector));
}

This leaves us with a minimal interface for Maybe<T>, which shouldn't need to be modified or extended.


Either

As Matt H mentioned, it's probably more useful to think of your "Validation" class as an "Either". This is a useful decoupling of what functionality the class provides and what you happen to be using that functionality for. You should also keep that in mind with your Maybe implementation. For example, your Fold uses the names success and error, but in general there's no reason to think that having a value indicates success and not having a value indicates an error. Perhaps you have a Maybe<Exception>!

In the same way that Maybe can be thought of as "IEnumerable with the restriction that there is at most one value", Either can be thought of as "Tuple of Maybes with the restriction that exactly one has a value". So we can do the same process of encoding this in the class via the constructors:

public class Either<T,U>
{
    Tuple<Maybe<T>,Maybe<U>> _inner;

    internal Either(Tuple<Maybe<T>,Maybe<U>> inner)
    {
        _inner = inner;
    }

    public V Fold(Func<T,V> selectOne, Func<U,V> selectTwo)
    {
        var left = _inner.ItemOne.Select(selectOne);
        var right = _inner.ItemTwo.Select(selectTwo);

        return left.Coalesce(right).Value;
    }
}

We can now go through the same process of static constructors (one for the "left has value" case, one for the "right has value" case), exposing a single method that lets us write any extension methods we need, and then writing relevant extension methods. By comparison to the version in the question, this leaves us with:

  • Only one actual implementing class, with extension methods not caring about whether the left or right has the value
  • A class which is general enough to be used for validation, but also other purposes
  • Easy extensibility
  • A nice general mechanism for getting values (note if we wanted something more tailored to validation, we could write an extension method like: T GetOrThrow<T>(this Either<T,Exception> source);

As a side note, this is a good demonstration of the power of invariants. By carefully protecting state, we've managed to transform IEnumerable<T> into Maybe<T>, and Tuple<Maybe<T>,Maybe<U>> into Either<T,U>, just by adding a straightforward restriction to each one.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.