4

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?

  • What is the question? – JacquesB Jun 9 '16 at 17:41
  • How can I reduce the verbosity of my return types? – mamidon Jun 9 '16 at 17:45
  • Well, if you want your return types shorter, these's not much you can do except defining your own type. Also, these types are still pretty readable to my taste. – scriptin Jun 9 '16 at 17:49
  • If you want to work functionally in the .NET world, have you considered using F# instead of C#? It provides better support for this kind of problem, including type abbreviations which is the usual solution to it. – Jules Jun 9 '16 at 17:53
  • Indeed I have been reading up on it in my spare time, but this is at work. – mamidon Jun 9 '16 at 17:53
3

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).

2

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>>
{
}
  • 1
    Does this have any particular subtle problems, or unexpected advantages? – Nathan Tuggy Jun 9 '16 at 17:53
1

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.

  • They may be idiomatic, but I find them lacking as it's easy to forget to handle them when you're writing code that throws them/no real way to know what exceptions code throws this side of reading every line. – mamidon Jun 9 '16 at 17:52
  • In the above case I'm somewhat expecting to hit some errors, like unauthenticated or unauthorized. The socket getting closed on me would be an exception. – mamidon Jun 9 '16 at 17:53
  • If you want to avoid exceptions, write your apis like this instead: ApiError GetFoo(int fooId, out MyResultType x) and then make sure ApiError tells the caller whether he succeeded or hit an error. – Brandin Jun 9 '16 at 20:00
  • @Brandin Resurrecting HResults. I love it! I'm having 90's flashbacks already! – Mike Jun 10 '16 at 0:22
  • @marmidon: You are fighting against the language by introducing your own error-handling mechanism on top of the built in exception system. This is bound to be more verbose. – JacquesB Jun 10 '16 at 9:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.