Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# 7 - Why can't I return this awaitable type from an async method?

Background

Try<T>

The application I'm working in uses a type Try<T> to handle errors in a functional style. A Try<T> instance represents either a value or an error, similar to how a Nullable<T> represents a value or null. Within the scope of functions exceptions may be thrown, but the "bubbling up" of exceptions to higher level components is replaced by "piping" them through return values.

Here is the gist of the Try<T> implementation. The Error class is basically equivalent to Exception.

public class Try<T> {

    private readonly T value;
    private readonly Error error;

    public bool HasValue { get; }

    public T Value {
        get {
            if (!HasValue) throw new InvalidOperationException();
            return value;
        }
    }

    public Error Error {
        get {
            if (HasValue) throw new InvalidOperationException();
            return error;
        }
    }

    internal Try(Error error) {
        this.error = error;
    }

    internal Try(T value) {
        this.value = value;
        HasValue = true;
    }
}

public static class Try {
    public static Try<T> Success<T>(T value) => new Try<T>(value);
    public static Try<T> Failure<T>(Error error) => new Try<T>(error);
}

async

The application I'm working on is also heavily asynchronous, and uses the standard async/await idiom. The codebase exclusively uses Task<T>, with no use of plain old Task or async void methods. Where you would normally see Task, Task<FSharp.Core.Unit> is used instead.

As you might imagine, many asynchronous operations may error, and so the type Task<Try<T>> gets used a lot. This works fine, but results in a lot of visual clutter. Since C# 7 now allows async methods returning custom awaitable types, I would like to use this feature to make a class that is effectively Task<Try<T>> which can be returned from async methods.

TryTask<T>

So I've created a custom awaitable task-like class (which really delegates most functionality to a Task<Try<T>> field), and an accompanying AsyncMethodBuilder class.

[AsyncMethodBuilder(typeof(TryTaskBuilder<>))]
public class TryTask<T>
{
    private readonly Task<Try<T>> _InnerTask;

    public TryTask(Func<Try<T>> function)
    {
        if (function == null) throw new ArgumentNullException(nameof(function));
        _InnerTask = new Task<Try<T>>(function);
    }

    internal TryTask(Task<Try<T>> task)
    {
        _InnerTask = task;
    }

    public void Start() => _InnerTask.Start();

    public TaskStatus Status => _InnerTask.Status;

    public Try<T> Result => _InnerTask.Result;

    public TaskAwaiter<Try<T>> GetAwaiter() => _InnerTask.GetAwaiter();

    public void Wait() => _InnerTask.Wait();
}

public static class TryTask
{
    public static TryTask<T> Run<T>(Func<Try<T>> function)
    {
        var t = new TryTask<T>(function);
        t.Start();
        return t;
    }

    public static TryTask<T> FromValue<T>(T value) => new TryTask<T>(Task.FromResult(Try.Success(value)));
    public static TryTask<T> FromError<T>(Error error) => new TryTask<T>(Task.FromResult(Try.Failure<T>(error)));
    public static TryTask<T> FromResult<T>(Try<T> result) => new TryTask<T>(Task.FromResult(result));
    public static TryTask<T> FromTask<T>(Task<Try<T>> task) => new TryTask<T>(task);
}

public class TryTaskBuilder<T>
{
    private AsyncTaskMethodBuilder<Try<T>> _InnerBuilder;

    public TryTaskBuilder()
    {
        _InnerBuilder = new AsyncTaskMethodBuilder<Try<T>>();
    }

    public static TryTaskBuilder<T> Create() =>
        new TryTaskBuilder<T>();

    public TryTask<T> Task =>
        default(TryTask<T>);

    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine =>
        _InnerBuilder.Start(ref stateMachine);

    public void SetStateMachine(IAsyncStateMachine stateMachine) =>
        _InnerBuilder.SetStateMachine(stateMachine);

    public void SetResult(Try<T> result) =>
        _InnerBuilder.SetResult(result);

    public void SetException(Exception exception) =>
        _InnerBuilder.SetResult(exception.AsError<T>());

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine =>

        _InnerBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine);

    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine =>
        _InnerBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
}

Problem

To make TryTask<T> really useful, the first thing I want to do is define functional let, bind, and map higher-order functions that will "unwrap" values and perform operations with them. Here is an example:

public async static TryTask<T2> Bind<T1, T2>(
    this TryTask<T1> source, 
    Func<T1, Try<T2>> binding)
    {
        Try<T1> result1 = await source;

        Try<T2> result2 = result1.HasValue
            ? binding(result1.Value)
            : Try.Failure<T2>(result1.Error);

        return result2;
    }

This method will not compile, with the error CS0029: Cannot implicitly convert type Try<T2> to T2 on the symbol result2 in the last line.

If I change the last line to return result2.Value; it will compile, but that will not be valid if result2 has an error.

Question

How can I get around this error and get this type to work as the return type of async methods? In typical async methods returning Task<T>, you can use the statement return default(T); and the compiler will wrap that T in a Task<T> for you. In my case, I want it to wrap a Try<T> in a TryTask<T>, but the compiler expects it should wrap a T in something. What method does the compiler use to decide how to do this "wrapping"?

like image 871
JamesFaix Avatar asked Apr 05 '17 02:04

JamesFaix


2 Answers

If I understand this correctly (which is kind of hard without a specification), the root problem is type inference for async lambdas, as described here by Lucian Wischik, the original author of the tasklike proposal.

In your case, that would mean something like:

void F<T>(Func<TryTask<T>> func) { }

F(async () => Try.Success(42));

The lambda returns Try<int> and you want the compiler to somehow figure out from that that the type of the lambda should be Func<TryTask<int>>. But according to the document linked above, there is no good way to do that.

This is not an issue with your Bind, but the language designers chose to have methods and lambdas behave consistently, rather than making methods stronger.

So, as far as I know, what you want to do is not possible. You might consider sharing your use case with the designers of C# by creating an issue at the csharplang repo, maybe someone will figure out how to resolve the issues and to make this work in a future version of C#.

like image 125
svick Avatar answered Nov 07 '22 03:11

svick


The accepted answer is correct, but you may want to consider the language-ext library that has a delegate based Try implementation, as well as Async variants for all of its extension methods, and a ToAsync() extension that converts a Try<A> to a TryAsync<A>. It also has TryOption<A> and TryOptionAsync<A> for returning either a Some, None, or Fail.

// Example of an action that could throw an exception
public Try<int> Foo() => () => 10;

// Synchronous Try
var result = Foo().IfFail(0);

// Synchronous Try
var result = Foo().Match(
    Succ: x => x,
    Fail: e => 0
);

// Asynchronous Try
var result = await Foo().IfFailAsync(0);

// Asynchronous Try
var result = await Foo().MatchAsync(
    Succ: x => x,
    Fail: e => 0
);

// Manually convert a Try to a TryAsync.  All operations are
// then async by default
TryAsync<int> bar = Foo().ToAsync();        

// Asynchronous Try
var result = await bar.IfFail(0);

// Asynchronous Try
var result = await bar.Match(
    Succ: x => x,
    Fail: e => 0
);

It has a massive selection of functional types which if you're implementing types like Try you may find useful.

like image 36
louthster Avatar answered Nov 07 '22 03:11

louthster