louthy / language-ext

C# functional language extensions - a base class library for functional programming
MIT License
6.41k stars 415 forks source link

Either<TL, TR> cannot be used as a generic constraint #982

Closed mrpmorris closed 2 years ago

mrpmorris commented 2 years ago

I've just started to play with Functional programming today using your library. It's very good, well done and thank you!

I've tried adding a generic constraint like so

public static void MapCommand<TRequest, TResponse>(this WebApplication app, string url)
 where TResponse : Either<ErrorResponse, SuccessResponse>

But the C# won't allow this, presumably because Eiher<,> is a struct.

Could you please add an interface IEither<TL, TR> so that I can add this constraint?

Note that I will be returning descendants of ErrorResponse and SuccessResponse, so the interface would need whichever type of variance that is (co or contra, I have no idea) :)

CK-LinoPro commented 2 years ago

Most functional-first languages don't build on inheritance or don't support it in the first place. Either<L, R> is not marked as sealed, so you are not technically forbidden to create a derived type but it doesn't really make sense to do so. All Either<L, R> does is describe a two-state sum type where its value could either be of type L or type R with R often representing a "success" state under applicative and monadic evaluation, i.e. Bind and from .. in .. select .. will stop any further evaluation in case of L.

In your case, I would advise creating your own sum types ErrorResponse and SuccessResponse (using CodeGen or by hand) and just returning Either<ErrorResponse, SuccessResponse>. To aid readability you could implement a newtype public class CommandResponse : NewType<CommandResponse, Either<UrrorResponse, SuccessResponse> { /* ... */ } and define all desired fields and extension methods like Map, Bind, Match, etc.

If you do feel like Either<L, R> lacks something you need, consider the following approaches:

td;dr: Functional programming and OOP-style inheritance don't mix well, but you can make use of inheritance (as a language feature) to implement sum types in C#.

mrpmorris commented 2 years ago

Thanks for your reply!

I achieved it like so, but it did mess with my brain for quite some time trying to get the generic constraints correct.

public static class WebApplicationExtensions
{
    public static WebApplication MapApiRequest<TRequest, TResponse>(this WebApplication app, string url)
        where TRequest : IRequest<Either<ErrorResponse, TResponse>>
        where TResponse : SuccessResponse
    {
        app.MapPost(
            url,
            ([FromBody] TRequest request, [FromServices] IMediator mediator) => mediator.Send(request).AsHttpResultAsync());
        return app;
    }
}

My hierarchy is Response<|--SucessResponse <|-- SignUpResponse Response<|--ErrorResponse <|-- BadRequestResponse

In my HTTP API project I have the following Either<L,R> extension to convert the result to an HTTP response with the correct return code

public static class EitherExtensions
{
    public static IResult AsHttpResult<TError, TSuccess>(this Either<TError, TSuccess> source)
        where TError : ErrorResponse
        where TSuccess : SuccessResponse
    {
        ArgumentNullException.ThrowIfNull(source);
        IResult json =
            source.Match(
                Right: x => Results.Json(x),
                Left: x =>
                    x.Status switch
                    {
                        ResponseStatus.UnexpectedError => Results.StatusCode(500),
                        ResponseStatus.Conflict => Results.Conflict(x),
                        ResponseStatus.BadRequest => Results.BadRequest(x),
                        ResponseStatus.Unauthorized => Results.Unauthorized(),
                        _ => throw new NotImplementedException()
                    });
        return json;
    }

    public static async Task<IResult> AsHttpResultAsync<TError, TSuccess>(this Task<Either<TError, TSuccess>> source)
        where TError : ErrorResponse
        where TSuccess : SuccessResponse
    =>
        (await source).AsHttpResult();
}

Is there a better way to achieve the Async using your library? I don't mind doing away with the sync version completely.

I did like the look of the Validation<TFail, TSucc> class, but the Fail was enumerable and my failures are a single object.

CK-LinoPro commented 2 years ago

First of all, this should probably be continued as a discussion, not as an issue.

I don't fully understand the intent of MapApiRequest and your hierarchy, but I'd just like to re-iterate that "aggregation > inheritance" in most cases, especially in functional programming. (Nerd side-note: Learn You A Haskell For Great Good is a great introduction to functional programming and data modelling, but it's all about Haskell, no trace of C#.)

Asynchronous programming can be abstracted using various of the options I've mentioned, the simplest in this case being EitherAsync<L, R> as an abbreviation of Task<Either<L, R>>. Unfortunately there is no ValidationAsync<F, V> at the moment. Another option is using Eff and Aff as mentioned above or utilizing IObservable and Task in a functional way, either monadic (Bind, from .. in .. select), applicative or co-monadic (which is just C#'s async/await).

I often find myself writing my own monadic types that unify all monadic features I need. For example, I've written a type that combines monad reader, monad writer and asynchronous value realization with the applicative two-state logic of Validation<F, V> because that's what I need a lot in my workflow. Many Haskell frameworks define a dedicated reader-writer-state monad that encapsulates interaction in a limited environment. Does C# Dream Of Electric Monads? explains it very well, as do some examples @louthy has postet as replies to various issues.

Yes, Validation<F, V> gives you a list of errors by default. This is because applicative error handling does not stop the evaluation when encountering the first error. Instead it continues as far as it can and collects all errors along the way. There is however an alternative implementation Validation<FMonoid, F, V> that lets you define your own monoidal error type. ("Monoidal" meaning there are a concatenation operation and a neutral element for F defined through FMonoid.)

mrpmorris commented 2 years ago

I am using MediatR. Requests are decorated like so

public record GetUserInfoQuery(Guid id) : IRequest<GetUserInfoResponse>;
public record GetUserInfoResponse(Guid id, string Name);

I have decided to try using your library in here, so my request now looks like this

public record GetUserInfoQuery(Guid id) : IRequest<Either<ErrorResponse, GetUserInfoResponse>>;
public record GetUserInfoResponse(Guid id, string Name);

So when registering my routes against WebApplication I can simply write the following

app.MapApiRequest<GetUserInfoQuery, GetUserInfoResponse>("/some/url");

which uses the following extension

public static WebApplication MapApiRequest<TRequest, TResponse>(this WebApplication app, string url)
    where TRequest : IRequest<Either<ErrorResponse, TResponse>>
    where TResponse : SuccessResponse
{
    app.MapPost(
        url,
        ([FromBody] TRequest request, [FromServices] IMediator mediator) => mediator.Send(request).AsHttpResultAsync());
    return app;
}

the AsHttpResultAsync() returns the relevant HTTP status based on the type of ErrorResponse I get...

public static class EitherExtensions
{
    public static IResult AsHttpResult<TError, TSuccess>(this Either<TError, TSuccess> source)
        where TError : ErrorResponse
        where TSuccess : SuccessResponse
    {
        ArgumentNullException.ThrowIfNull(source);
        IResult json =
            source.Match(
                Right: x => Results.Json(x),
                Left: x =>
                    x.Status switch
                    {
                        ResponseStatus.UnexpectedError => Results.StatusCode(500),
                        ResponseStatus.Conflict => Results.Conflict(x),
                        ResponseStatus.BadRequest => Results.BadRequest(x),
                        ResponseStatus.Unauthorized => Results.Unauthorized(),
                        _ => throw new NotImplementedException()
                    });
        return json;
    }

    public static async Task<IResult> AsHttpResultAsync<TError, TSuccess>(this Task<Either<TError, TSuccess>> source)
        where TError : ErrorResponse
        where TSuccess : SuccessResponse
    =>
        (await source).AsHttpResult();
}

This ties everything up nicely. There is nowhere I can get the request/response combination wrong, and this works as a generic constraint because I am able to use Either<,> as my constraint because it is wrapped in Mediator.IRequest<> (where TRequest : IRequest<Either<ErrorResponse, TResponse>>).

But my problem now is that I am trying to add FluentValidation to my pipeline. The following gets triggered because I am specifying the exact response type SignUpResponse.

public class MediatRValidatingMiddleware<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
        where TRequest : IRequest<TResponse>, IRequest<Either<ErrorResponse, SignUpResponse>>

But that is not what I need. What I need is

where TRequest : IRequest<TResponse>, IRequest<Either<ErrorResponse, TResponse>>

But this doesn't work because Either<L, R> isn't define as Either<in L, in R> so cannot be typecast from Either<ErrorResponse, SignInResponse> to Either<ErrorResponse, SuccessResponse>, meaning the is will return false and my middleware won't get called.

If Either<L, R> implements IEither<out L, out R> then I could replace Either<,> with IEither<,> and the constraint would work. Or, I could write

public class MediatRValidatingMiddleware<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
        where TRequest : IRequest<TResponse>
        where TResponse: IEither<ErrorResponse, SuccessResponse>

Does that make the scenario more clear?

CK-LinoPro commented 2 years ago

In my personal opinion, you're making your life harder than it needs to be by using two non-compatible paradigms. I've never used MediatR myself but the mediator pattern itself is a highly object-oriented concept. When using object-oriented and functional concepts side-by-side you need to be aware where the border is and what restrictions arise from your approach -- such as the restriction you're experiencing right now.

If you are fine with investigating alternatives to the mediator pattern, have a look at the Actor or Agent model. Actors are kind of the original concept behind "object-oriented programming", but actually completely functional. An actor is a unit of closed computation, a "black box", that can only communicated with using message types. Agents are a lite version of actors without supervision and without distributed computation. Agents are covered very well in Functional Programming in C#. @louthy has written a great library for actors, echo-process. There are other implementations that try to integrate OO-patterns as well. (Also, have a look at the language of Erlang where actors are a fundamental concept of the language.)

mrpmorris commented 2 years ago

I wanted to use LanguageExt so that instead of using inheritance (All responses have ValidationErrors property from a base class) I could use Either<ErrorResponse, SuccessResponse> - this seemed like a really tidy way to avoid inheritance.

But switching away from MediatR at this point isn't an option for me. So, does this mean the upshot is I cannot use LanguageExt with MediatR?

mrpmorris commented 2 years ago

Of course, the answer was to not use pipelines but to wrap the call to MediatR in something else. My solution is in the answer to my question on StackOverflow - https://stackoverflow.com/questions/70984368/constraining-a-generic-param-with-eitherl-r

Thanks all!