6
\$\begingroup\$

I am not sure if there are good ROP implementations already existing in the open because I couldn't find any, actually I found one by odytrice (https://github.com/odytrice/Operation), but I think the use of Operation class to represent Func, Success result and Failure result at the same time is unfortunate.

My implementation Railway.NET (which I hacked in two days) tries to solve these particular problems so that you only have to work with clr types.

public class TestClass
{
    [Test]
    public void LinqTest()
    {
        var organisationId = 1;
        // partial application of GetPerson (although not as elegant as F#)
        // Here we use organisationId as some dependency of the method but 
        // in fact we can 'embed' any or multiple other dependencies, think  
        // of DbContext, configuration, etc... 
        // What ever dependencies we choose to have, it does not
        // affect how we write the ageFunc below.
        var getPerson = new Func<string, Person>(name => GetPerson(organisationId, name));


        var ageFunc =
            from x in getPerson
            from y in Validate(x)
            select GetAge(y);

        // ageFunc is of type Func<string, IMonad<int>>
        // in this case we expect a failure result
        ageFunc("ibrahim").Should().BeAssignableTo<IFailure>()
            .Which.Message.Should().Be("Validation error");
    }

    public ValidationError<Person> Validate(Person p) => new ValidationError<Person>("Validation error");

    public int GetAge(Person p) => p.Age;

    // Current implementation of GetPerson will always return a value
    // In that case we declare this to return Person type and it will be
    // automatically wrapped in a SuccessResult<Person>.
    // In real world scenario, we would change the return type to 
    // Result<Person> which could be a SuccessResult<Person>
    // or a FailureResult<Person>.
    public Person GetPerson(int organisationId, string firstName) => new Person { FirstName = firstName, Age = 36, OrganisationId = organisationId };

}

public class ValidationError<T>: IMonad<T>, IFailure
{
    public ValidationError(string message)
    {
        this.Message = message;
    }

    public IMonad<U> Map<U>(Func<T, IMonad<U>> func)
    {
        return new ValidationError<U>(this.Message);
    }

    public string Message { get; }
    public Exception Exception { get; }
}
public class Person
{
    public string FirstName { get; set; }
    public int Age { get; set; }
    public int OrganisationId { get; set; }
}

The getPerson and Validate methods are wrapped with SuccessResult, or in case of and exception, with FailureResult.

public interface IMonad<T>
{
    // I Probably should have called this Bind
    IMonad<U> Map<U>(Func<T, IMonad<U>> func);
}
public interface IFailure
{
    string Message { get; }
    Exception Exception { get; }
}
public interface ISuccessResult<out T>
{
    T Value { get; }
}

/// <summary>
/// Result monad
/// </summary>
/// <typeparam name="TSuccess"></typeparam>
public abstract class Result<TSuccess>: IMonad<TSuccess>
{
    public class SuccessResult : Result<TSuccess>, ISuccessResult<TSuccess>
    {
        public TSuccess Value { get; }

        public SuccessResult(TSuccess value)
        {
            this.Value = value;
        }

        public override IMonad<U> Map<U>(Func<TSuccess, IMonad<U>> next)
        {
            return next(Value);
        }

        public override IMonad<U> Map<U>(Func<TSuccess, U> next)
        {
            return Result.Success(next(Value));
        }
    }

    public class Failure : Result<TSuccess>, IFailure
    {
        public string Message { get; }

        public Exception Exception { get; }

        public Failure(string message)
        {
            Message = message;
        }

        public Failure(Exception exception)
            : this(exception.Message)
        {
            Exception = exception;
        }

        public override IMonad<U> Map<U>(Func<TSuccess, IMonad<U>> next)
        {
            return Result.Failure<U>(this.Message);
        }

        public override IMonad<U> Map<U>(Func<TSuccess, U> next)
        {
            return Result.Failure<U>(this.Message);
        }
    }

    public abstract IMonad<U> Map<U>(Func<TSuccess, IMonad<U>> next);

    public abstract IMonad<U> Map<U>(Func<TSuccess, U> next);
}

public static class Result
{
    public static Result<T>.SuccessResult Success<T>(T value)
    {
        return new Result<T>.SuccessResult(value);
    }

    public static Result<T>.Failure Failure<T>(string message)
    {
        return new Result<T>.Failure(message);
    }
}

Also, you can change e.g the getPerson method to return some custom Result types, if it only implements IMonad<T>.

To be able to write the LINQ syntax, I v added the following code snippet which is pretty much like the the monadic bind operator of Haskell

public static class MonadExtensions
{
    public static Func<T, IMonad<V>> Bind<T, R, V>(this Func<T, IMonad<R>> operation, Func<R, IMonad<V>> nextResult)
    {
        return t => operation(t).Map(nextResult);
    }

    public static Func<T, IMonad<S>> SelectMany<T, R, U, S>(this Func<T, IMonad<R>> operation, Func<R, IMonad<U>> monadSelector, Func<R, U, S> resultSelector)
    {
        return t => operation(t).Map(r => monadSelector(r).Map(u => Result.Success(resultSelector(r, u))));
    }

    public static Func<T, IMonad<S>> SelectMany<T, R, U, S>(this Func<T, IMonad<R>> operation, Func<R, U> nextSelector, Func<R, U, S> resultSelector)
    {
        return t => operation(t).Map(r => Result.Success(resultSelector(r, nextSelector(r))));
    }

    public static Func<T, IMonad<S>> SelectMany<T, R, U, S>(this Func<T, IMonad<R>> operation, Func<R, IMonad<U>> monadSelector, Func<R, U, IMonad<S>> resultSelector)
    {
        return t => operation(t).Map(r => monadSelector(r).Map(u => resultSelector(r, u)));
    }

    public static Func<T, IMonad<S>> SelectMany<T, R, U, S>(this Func<T, R> operation, Func<R, IMonad<U>> monadSelector, Func<R, U, S> resultSelector)
    {
        return SelectMany<T, R, U, S>(t => Result.Success(operation(t)), monadSelector, resultSelector);
    }

    public static Func<T, IMonad<S>> SelectMany<T, R, U, S>(this Func<T, R> operation, Func<R, U> nextSelector, Func<R, U, S> resultSelector)
    {
        return SelectMany<T, R, U, S>(t => Result.Success(operation(t)), nextSelector, resultSelector);
    }

    public static Func<T, IMonad<S>> SelectMany<T, R, U, S>(this Func<T, R> operation, Func<R, IMonad<U>> monadSelector, Func<R, U, IMonad<S>> resultSelector)
    {
        return SelectMany<T, R, U, S>(t => Result.Success(operation(t)), monadSelector, resultSelector);
    }

    public static Func<T, IMonad<U>> Select<T, R, U>(this Func<T, IMonad<R>> operation, Func<R, IMonad<U>> selector)
    {
        return operation.Bind(selector);
    }

    //public static Operation<T, U> Select<T, R, U>(this Operation<T, R> operation, Func<R, U> selector)
    //{
    //    return operation.Bind(t => Result.Success(selector(t)));
    //}
}

Next is to fully support LINQ syntax.

So check out this link Railway.NET, I 'll appreciate if you like it, and even more if you have suggestions and improvements.

Complete code for LINQPad

object Main()
{
var organisationId = 1;
// partial application of GetPerson (although not as elegant as F#)
// Here we use organisationId as some dependency of the method but 
// in fact we can 'embed' any or multiple other dependencies, think  
// of DbContext, configuration, etc... 
// What ever dependencies we choose to have, it does not
// affect how we write the ageFunc below.
var getPerson = new Func<string, Person>(name => GetPerson(organisationId, name));



var ageFunc =
from x in getPerson
from y in Validate(x)
select GetAge(y);

// ageFunc is of type Func<string, IMonad<int>>
// in this case we expect a failure result
return ageFunc("ibrahim");
}

// Define other methods and classes here

public static ValidationError<Person> Validate(Person p) => new ValidationError<Person>("Validation error");

public static int GetAge(Person p) => p.Age;

// Current implementation of GetPerson will always return a value
// In that case we declare this to return Person type and it will be
// automatically wrapped in a SuccessResult<Person>.
// In real world scenario, we would change the return type to 
// Result<Person> which could be a SuccessResult<Person>
// or a FailureResult<Person>.
public static Person GetPerson(int organisationId, string firstName) => new Person { FirstName = firstName, Age = 36, OrganisationId = organisationId };

public interface IMonad<T>
{
// I Probably should have called this Bind
IMonad<U> Map<U>(Func<T, IMonad<U>> func);
}
public interface IFailure
{
string Message { get; }
Exception Exception { get; }
}
public interface ISuccessResult<out T>
{
T Value { get; }
}
/// <summary>
/// Result monad
/// </summary>
/// <typeparam name="TSuccess"></typeparam>
public abstract class Result<TSuccess> : IMonad<TSuccess>
{
public class SuccessResult : Result<TSuccess>, ISuccessResult<TSuccess>
{
public TSuccess Value { get; }

public SuccessResult(TSuccess value)
{
this.Value = value;
}

public override IMonad<U> Map<U>(Func<TSuccess, IMonad<U>> next)
{
return next(Value);
}

public override IMonad<U> Map<U>(Func<TSuccess, U> next)
{
return Result.Success(next(Value));
}
}

public class Failure : Result<TSuccess>, IFailure
{
public string Message { get; }

public Exception Exception { get; }

public Failure(string message)
{
Message = message;
}

public Failure(Exception exception)
: this(exception.Message)
{
Exception = exception;
}

public override IMonad<U> Map<U>(Func<TSuccess, IMonad<U>> next)
{
return Result.Failure<U>(this.Message);
}

public override IMonad<U> Map<U>(Func<TSuccess, U> next)
{
return Result.Failure<U>(this.Message);
}
}

public abstract IMonad<U> Map<U>(Func<TSuccess, IMonad<U>> next);

public abstract IMonad<U> Map<U>(Func<TSuccess, U> next);
}

public static class Result
{
public static Result<T>.SuccessResult Success<T>(T value)
{
return new Result<T>.SuccessResult(value);
}

public static Result<T>.Failure Failure<T>(string message)
{
return new Result<T>.Failure(message);
}
}

public class ValidationError<T> : IMonad<T>, IFailure
{
public ValidationError(string message)
{
this.Message = message;
}

public IMonad<U> Map<U>(Func<T, IMonad<U>> func)
{
return new ValidationError<U>(this.Message);
}

public string Message { get; }
public Exception Exception { get; }
}
public class Person
{
public string FirstName { get; set; }
public int Age { get; set; }
public int OrganisationId { get; set; }
}

public static class MonadExtensions
{
public static Func<T, IMonad<V>> Bind<T, R, V>(this Func<T, IMonad<R>> operation, Func<R, IMonad<V>> nextResult)
{
return t => operation(t).Map(nextResult);
}

public static Func<T, IMonad<S>> SelectMany<T, R, U, S>(this Func<T, IMonad<R>> operation, Func<R, IMonad<U>> monadSelector, Func<R, U, S> resultSelector)
{
return t => operation(t).Map(r => monadSelector(r).Map(u => Result.Success(resultSelector(r, u))));
}

public static Func<T, IMonad<S>> SelectMany<T, R, U, S>(this Func<T, IMonad<R>> operation, Func<R, U> nextSelector, Func<R, U, S> resultSelector)
{
return t => operation(t).Map(r => Result.Success(resultSelector(r, nextSelector(r))));
}

public static Func<T, IMonad<S>> SelectMany<T, R, U, S>(this Func<T, IMonad<R>> operation, Func<R, IMonad<U>> monadSelector, Func<R, U, IMonad<S>> resultSelector)
{
return t => operation(t).Map(r => monadSelector(r).Map(u => resultSelector(r, u)));
}

public static Func<T, IMonad<S>> SelectMany<T, R, U, S>(this Func<T, R> operation, Func<R, IMonad<U>> monadSelector, Func<R, U, S> resultSelector)
{
return SelectMany<T, R, U, S>(t => Result.Success(operation(t)), monadSelector, resultSelector);
}

public static Func<T, IMonad<S>> SelectMany<T, R, U, S>(this Func<T, R> operation, Func<R, U> nextSelector, Func<R, U, S> resultSelector)
{
return SelectMany<T, R, U, S>(t => Result.Success(operation(t)), nextSelector, resultSelector);
}

public static Func<T, IMonad<S>> SelectMany<T, R, U, S>(this Func<T, R> operation, Func<R, IMonad<U>> monadSelector, Func<R, U, IMonad<S>> resultSelector)
{
return SelectMany<T, R, U, S>(t => Result.Success(operation(t)), monadSelector, resultSelector);
}

public static Func<T, IMonad<U>> Select<T, R, U>(this Func<T, IMonad<R>> operation, Func<R, IMonad<U>> selector)
{
return operation.Bind(selector);
}

//public static Operation<T, U> Select<T, R, U>(this Operation<T, R> operation, Func<R, U> selector)
//{
//    return operation.Bind(t => Result.Success(selector(t)));
//}
}
\$\endgroup\$
  • \$\begingroup\$ Funny. I was just hacking on the same idea last weekend. I became unhappy with my implementation because it really needed Discriminated unions for the new pattern matching features to work cleanly. \$\endgroup\$ – RubberDuck Aug 13 '17 at 21:43
  • \$\begingroup\$ distriminated unions are basically, in terms of OOP, some abstract base class and a bunch of one level subtypes. That is exactly what I did in Railway.NET, however, we would have to wait for pattern matching based on 'structure' of the subtype untill C# 8+, at least to my knowlegde, there are no plan for this in C# 7 \$\endgroup\$ – Ibrahim ben Salah Aug 14 '17 at 9:04
  • \$\begingroup\$ The first example does not make any sense. If getPerson expects a string how can you use it in from x in getPerson without passing any parameters? Then why do you even need this variable? It would be much easier to write from x in GetPerson(x) then creating a Func for it that does exactly the same as the original method. Also how can you loop over a Person? Is it a collection? So many questions so few lines of code. \$\endgroup\$ – t3chb0t Aug 14 '17 at 10:14
  • \$\begingroup\$ @t3chb0t good point, however there is a huge benefit in this as this demonstrates partial application of the method getPerson, compose it first with other methods and only after we construct the whole logic of the program we apply the argument(s) to it \$\endgroup\$ – Ibrahim ben Salah Aug 14 '17 at 10:20
  • \$\begingroup\$ This sounds interesting but I cannot see this benefit yet. Could you improve the example so that one can see how it is being constructed etc? \$\endgroup\$ – t3chb0t Aug 14 '17 at 10:23
3
\$\begingroup\$
var ageFunc =
    from x in getPerson
    from y in Validate(x)
    select GetAge(y);

While making this work by implementing SelectMany extensions for the Func and Select for the second from is a nice trick the usage is very confusing. I don't know, maybe it's the example or maybe I don't get it but wrapping a Person with a Func only to be able to use it with a query doesn't sound very useful.

The expression from x in aCollectionOfXes means that I evaluate each element of a collection but what does from x in getPerson mean? What am I evaluating? Its properties? Its fields? Its constructors? No idea.

The second line is even more confusing. What does from y in Validate(x) mean? Am I looping over validation results? No! I'm looping over a person again! This is not just confusing, this is super confusing. No the biggest surprise, GetAge does not retrieve any age but another Func. It's a func-ception :-]

Nice work making it work but based on this example I don't see any practival uses of this pattern.

\$\endgroup\$
  • \$\begingroup\$ In short I think you re right, in fact I already changed my code in the github repo because I came to the same conclusion. In the resulting code there contains only IMonad of T and supporting extensions methods. Working with Func like I showed before might be still beneficial tho for composing functions but that has nothing to do with Monads. \$\endgroup\$ – Ibrahim ben Salah Aug 16 '17 at 11:55
  • \$\begingroup\$ The confusion about Validate is caused by the result type ValidationError<T> but what I intended to show here is that you can return any custom type. In real scenario's the result type should be IMonad<T> or any type that represents wether the operation had succeeded or failed. The whole benefit of ROP is to hide errors and error handling and only show the happy path in the query. So that means you would only pass Person objects around or it's Age. It also means that error handling can be implemented in one location, as I showed with ValidationError<T> \$\endgroup\$ – Ibrahim ben Salah Aug 16 '17 at 12:21
  • \$\begingroup\$ Implementing SelectMany and using query syntax seems to be a common way to implement monads in C#. I don't like it either, but it's a nightmare to call SelectMany with lambda syntax. \$\endgroup\$ – RubberDuck Sep 2 '17 at 0:40
  • \$\begingroup\$ It is because people think Linq is only for IEnumerable, but Linq can be used with any type. In this case, Option/Maybe types works like a single element enumerable. You can implement idioms with linq. \$\endgroup\$ – Luis Jan 19 '18 at 1:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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