Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

Sign up
Here's how it works:
  1. Anybody can ask a question
  2. Anybody can answer
  3. The best answers are voted up and rise to the top

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?

share|improve this question

migrated from programmers.stackexchange.com Dec 5 '13 at 14:41

This question came from our site for professional programmers interested in conceptual questions about software development.

    
+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) – Matt Fenwick Dec 3 '13 at 14:52
3  
@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. – Phoshi Dec 3 '13 at 15:00
6  
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))) – jozefg Dec 3 '13 at 15:06
    
Just a note, your validation type can be generalized to the Either type, or disjoint union. – Matt H Dec 3 '13 at 20:29
    
@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? – Blair Davidson Dec 4 '13 at 0:59

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.

share|improve this answer
    
I don't agree with using if-else and bracers and stuff here, but throwing a more specific exception is good advice. +1 – Simon Forsberg Dec 19 '13 at 23:38
    
@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. – Mat's Mug Dec 19 '13 at 23:43

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.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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