The response to my new Functional Programming with C# course on Pluralsight has been far better than I ever imagined it would be while I was writing it. Thanks, everyone for your support!
One viewer, John Hoerr, asked an interesting question about how to include async methods within a chain. I have to be honest that I never really thought about it but I can definitely see how it would be useful.
In his hypothetical example John provided the following three async methods:
public async Task<int> F(int x) => await Task.FromResult(x + 1); public async Task<int> G(int x) => await Task.FromResult(x * 2); public async Task<int> H(int x) => await Task.FromResult(x + 3);
He wanted to chain these three methods together such that the asynchronous result from one task would be passed as input to the next method. Essentially he wanted the asynchronous version of this:
1 .Map(H) .Map(G) .Map(F);
These methods can’t be chained together using the Map method I defined in the course because each of them want an int value rather than Task<int>. One thing John considered was using the ContinueWith method.
1 .Map(H) .ContinueWith(t => G(t.Result)) .ContinueWith(t => F(t.Result.Result));
This approach does play well with method chaining because each method returns a task that exposes the ContinueWith method but it requires working with the tasks directly to get the result and hand it off to the next method. Also, as we chain more tasks together we have to drill through the results to get to the value we really care about. Instead what we’re looking for is a more generalized approach that can be used across methods and at an arbitrary level within the chain.
After some more discussion we arrived at the following solution:
public static async Task<TResult> MapAsync<TSource, TResult>( this Task<TSource> @this, Func<TSource, Task<TResult>> fn) => await fn(await @this);
Rather than working with TSource and TResult directly like the Map method does, MapAsync operates against Task<TResult>. This approach allows us to define the method as async, accept the task returned from one async method, and await the call to the delegate. The method name also gives anyone reading the code a good visual indication that it is intended to be used with asynchronous methods.
With MapAsync now defined we can easily include async methods in a chain like this:
await 1 .Map(H) .MapAsync(G) .MapAsync(F);
Here we begin with the synchronous Map call because at this point we have an integer rather than a task. The call to H returns a Task so from there we chain in G and F respectively using the new MapAsync method. Because we’re awaiting the whole chain, it’s all wrapped up in a nice continuation automatically for us.
This version of the MapAsync method definitely covers the original question but there are two other permutations that could also be useful.
public static async Task<TResult> MapAsync<TSource, TResult>( this TSource @this, Func<TSource, Task<TResult>> fn) => await fn(@this); public static async Task<TResult> MapAsync<TSource, TResult>( this Task<TSource> @this, Func<TSource, TResult> fn) => fn(await @this);
Both of these overloads awaits results at different points depending on the input or output but they each operate against a Task at some point.
So there you have it, a relatively painless way to include arbitrary async methods within a method chain.
Thanks, John, for your question and your contributions to this post!