Closed mrpmorris closed 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:
EitherAsync<L, R>
if you need asynchronous value realization,Validation<L, R>
if you want applicative evaluation without fail-fast behavior (probably an advanced topic for you right now, it's well covered in Functional Programming in C#),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#.
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.
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
.)
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?
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.)
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?
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!
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
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
andSuccessResponse
, so the interface would need whichever type of variance that is (co or contra, I have no idea) :)