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)));
//}
}
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\$