Chaining two functions () -> Task and A->Task

2020-06-03 06:38发布

问题:

I don't know if I am thinking in the wrong way about TPL, but I have difficulty understanding how to obtain the following:

I have two functions

Task<A> getA() { ... }
Task<B> getB(A a) { ... }

This seems to occur often: I can asyncronously get an A. And given an A, I can asynchronously get a B.

I can't figure out the correct way to chain these functions together in TPL.

Here is one attempt:

Task<B> Combined()
{
    Task<A> ta = getA();
    Task<Task<B>> ttb = ta.ContinueWith(a => getB(a.Result));
    return ttb.ContinueWith(x => x.Result.Result);
}

The ContinueWith is where I get confused. The returned type is a "double-Task", Task<Task<B>>. This somehow just seems wrong to me.

UPDATE 2011-09-30:

By coincidence I found the extension method TaskExtensions.Unwrap that operates on a Task<Task<T>> to give a Task<T>. So until we get C# 5.0, I can do ta.ContinueWith(a=>...).UnWrap() in situations like this where the continuation itself returns a task.

回答1:

Does your getB have to be a method which returns Task<B> rather than B?

The problem is that ContinueWith is:

public Task<TNewResult> ContinueWith<TNewResult>(
    Func<Task<TResult>, TNewResult> continuationFunction,
    CancellationToken cancellationToken
)

So in your case, because getB returns Task<B>, you're passing in a Func<Task<A>, Task<B>>, so TNewResult is Task<B>.

If you can change getB to just return a B given an A, that would work... or you could use:

return ta.ContinueWith(a => getB(a.Result).Result);

Then the lambda expression will be of type, Func<Task<A>, B> so ContinueWith will return a Task<B>.

EDIT: In C# 5 you could easily write:

public async Task<B> CombinedAsync()
{
    A a = await getA();
    B b = await getB(a);
    return b;
}

... so it's "just" a matter of working out what that ends up as. I suspect it's something like this, but with error handling:

public Task<B> CombinedAsync()
{
    TaskCompletionSource<B> source = new TaskCompletionSource();
    getA().ContinueWith(taskA => {
        A a = taskA.Result;
        Task<B> taskB = getB(a);
        taskB.ContinueWith(t => source.SetResult(t.Result));
    });
    return source.Task;
}

Does that make sense?



回答2:

In case you are familiar with LINQ (and the Monad concept behind it), then below is a simple Task monad which will allow you to compose the Tasks.

Monad implementation:

public static class TaskMonad
    {
        public static Task<T> ToTask<T>(this T t)
        {
            return new Task<T>(() => t);
        }

        public static Task<U> SelectMany<T, U>(this Task<T> task, Func<T, Task<U>> f)
        {
            return new Task<U>(() =>
            {
                task.Start();
                var t = task.Result;
                var ut = f(t);
                ut.Start();
                return ut.Result;
            });
        }

        public static Task<V> SelectMany<T, U, V>(this Task<T> task, Func<T, Task<U>> f, Func<T, U, V> c)
        {
            return new Task<V>(() =>
            {
                task.Start();
                var t = task.Result;
                var ut = f(t);
                ut.Start();
                var utr = ut.Result;
                return c(t, utr);
            });            
        }
    }

Usage example:

        public static void Main(string[] arg)
        {
            var result = from a in getA()
                         from b in getB(a)
                         select b;
            result.Start();
            Console.Write(result.Result);
        }


回答3:

While the accepted answer would probably work

Task<B> Combined()
{
    Task<A> ta = getA();
    Task<B> ttb = ta.ContinueWith(a => getB(a.Result)).Unwrap();
    return ttb;
}

Is a much more elegant way to implement this.



回答4:

If you are unable to use await, you can certainly use Unwrap, but it handles exceptions sub-optimally. The method I prefer is Then as described in this article. Composition becomes simple and elegant:

Task<B> Combined()
{
  return getA().Then(getB);
}

For those interested in the details, I wrote a blog post a while ago about exactly this problem of composing asynchronous methods, and how monads provide an elegant solution.