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)));
//}
}
1 Answer 1
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.
-
\$\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– Ibrahim ben Salah2017年08月16日 11:55:42 +00:00Commented Aug 16, 2017 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– Ibrahim ben Salah2017年08月16日 12:21:05 +00:00Commented Aug 16, 2017 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– RubberDuck2017年09月02日 00:40:46 +00:00Commented Sep 2, 2017 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– Luis2018年01月19日 01:52:27 +00:00Commented Jan 19, 2018 at 1:52
getPerson
expects a string how can you use it infrom x in getPerson
without passing any parameters? Then why do you even need this variable? It would be much easier to writefrom x in GetPerson(x)
then creating aFunc
for it that does exactly the same as the original method. Also how can you loop over aPerson
? Is it a collection? So many questions so few lines of code. \$\endgroup\$