Question

I've recently been writing some code that deals with 3rd parties -- obviously errors will happen so I'm using Either/Maybe monads where appropriate. As this is C# I'm also using async Tasks too.

My problem is after it's all said & done my return types are quite verbose. e.g.

Task<Either<ApiErrorEnum, ActualApiResultDto>> GetSomeFoo(int fooId);

But wait it can get even better

Task<Either<ApiErrorEnum, IEnumerable<ActualApiResultDto>> GetSeveralFoos()

It's a little much, but it all seems necessary to me. Obviously the Task<..> is because this is an IO bound operation. the Either is needed because this can go wrong in several different ways, and the actual result DTO is needed because that's what I'm really after.

I have written some code to basically typedef Either<ApiErrorEnum,T> but it's a little lacking as the infrastructure code for Either I've written can't quite handle derived types (I've tried, there's just always an edge case or two).

TLDR question: How can I reduce the verbosity of my return types?

Was it helpful?

Solution

I don't like using inheritance for this, because the API is going to be clunky. You will loose the nice functional chaining when you use Either if you wrap it in Task, because then, you will need to await it after every call.

Instead I would opt in to either create EitherTask, that is Either which is asynchronous inside or even go step further and create something like ApiResult that also includes the error code.

Something like :

public class ApiResult<TResult>
{
    Task<Either<ApiErrorEnum, TResult>> _task;

    public ApiResult(Task<Either<ApiErrorEnum, TResult>> task)
    {
        _task = task;
    }

    public ApiResult<TOther> Select<TOther>(Func<TResult, TOther> selector)
    {
        var task = _task.ContinueWith(x => x.Result.Select(selector));
        return new ApiResult<TOther>(task);
    }

    public async Task<TOther> Final<TOther>(Func<TResult, TOther> valid, Func<ApiErrorEnum, TOther> errored)
    {
        var eitherVal = await _task;
        return eitherVal.Match(valid, errored)();
    }
}

Which can then be used as :

public ApiResult<IEnumerable<ActualApiResultDto>> GetSeveralFoos()
{
    // do whatever
}

var result =
    await
    GetSeveralFoos()
    .Select(x => x.Count())
    .Final(x => x.ToString(), x => "Error :" + x.ToString());

Of course, catching all edge cases (like exceptions when using ContinueWith) will take some time, but it is doable.

One problem that I can see with this is that GetSeveralFoos() cannot be implemented as async, because it doesn't return Task. And all solutions that I can think of require spelling out the Task<Either<ApiErrorEnum, IEnumerable<ActualApiResultDto>> somewhere inside the API. Maybe something like Task.Then could make it more readable without using await (you could even replace the ContinueWith with it).

OTHER TIPS

One possible option

public class ActualApiResultWrapper : Either<IApiErrorEnum, IEnumerable<ActualApiResultDto>>
{
}

Task<ActualApiResultWrapper> GetSeveral();

Then the next result, OtherApiResult...

public class OtherApiResultWrapper : Either<IApiErrorEnum, IEnumerable<OtherApiResultDto>>
{
}

You can reduce verbosity by using exceptions, which is the idiomatic and established way of handling error conditions in C#.

You are basically introducing a form of typed exceptions (like Java) or typed error codes on top of a language with unchecked exceptions. This is bound to be more verbose, and in particular you can't use try/catch constructs to handle you own error codes - but you still need try/catch to handle the built-in exceptions. In general "fighting against the language" is going to be more verbose. You should consider using a language more to your liking like Java (for checked exceptions) or F# (for built-in support for pattern matching, which will make handling this kind of errors less verbose).

If your requirement is to have typed errors codes explicitly as types of the signature, I don't really see how the return values could be simpler.

Licensed under: CC-BY-SA with attribution
scroll top