dotnet / reactive

The Reactive Extensions for .NET
http://reactivex.io
MIT License
6.69k stars 751 forks source link

Add IAsyncEnumerable<>.SelectAwait accetpting Func<..,Task<>> #1528

Open bsagal opened 3 years ago

bsagal commented 3 years ago

I would like to suggest adding the following methods to System.Linq.AsyncEnumerable:

public static IAsyncEnumerable<TResult> SelectAwait<TSource, TResult>(
        this IAsyncEnumerable<TSource> source,
        Func<TSource, int, Task<TResult>> selector);

public static IAsyncEnumerable<TResult> SelectAwait<TSource, TResult>(
        this IAsyncEnumerable<TSource> source,
        Func<TSource, int, Task<TResult>> selector);

public static IAsyncEnumerable<TResult> SelectAwaitWithCancellation<TSource, TResult>(
        this IAsyncEnumerable<TSource> source,
        Func<TSource, CancellationToken, Task<TResult>> selector);

public static IAsyncEnumerable<TResult> SelectAwaitWithCancellation<TSource, TResult>(
        this IAsyncEnumerable<TSource> source,
        Func<TSource, int, CancellationToken, Task<TResult>> selector);

This would allow using selector functions which already return a Task;

I currenly have the following code:

FileList
    .ToAsyncEnumerable()
    .SelectAwaitWithCancellation(async (f, ct) => await File.ReadAllTextAsync(f, ct))
    .Select(LoadFile);

If the requested overloads where added I could write is as:

FileList
    .ToAsyncEnumerable()
    .SelectAwaitWithCancellation(File.ReadAllTextAsync)
    .Select(LoadFile);
bartdesmet commented 3 years ago

Adding these overloads is problematic because we then have overloads that vary in the selector delegates returning Task<T> or ValueTask<T>, which makes it impossible to write xs.SelectAwait(async x => ...) because the compiler can't pick which return type to use to construct the async lambda.

We decided that the use of ValueTask<T> is beneficial to reduce overheads for frequently invoked callbacks such as predicates, selectors, etc. at the expense of method group conversion for existing Task<T>-returning methods requiring manual lifting in a lambda:

xs.SelectAwait(async x => await f(x))

or

xs.SelectAwait(x => new ValueTask<T>(f(x)))

Finally, note that the verbose naming with Await and AwaitWithCancellation was put in to provide these async overloads without causing conflicts with the query expression language binding rules (e.g. to disambiguate a Select that's used to construct a sequence of tasks versus one that's used to await the result of a task) and as a placeholder to have a discussion with the C# team on possibly supporting async in query expressions (e.g. await select, async select, or whatever?).