Roslyn: Proposal: Constraining T to be awaitable

Created on 20 Sep 2016  路  10Comments  路  Source: dotnet/roslyn

Could we constrain the T to awaitable? ie IAsyncEnumerable<async T>

This would be a language based constraint. Since async and await are pattern based, the compiler would check the applied T to see if it matches that pattern. In metadata is would be supported via an attribute.

interface IAsyncIEnumerator<async T> 
{
  .GetEnumerator() : IAsync<IAsyncEnumerator<T>>
}

interface IAsyncEnumerator<async T>
{
  .MoveNext() : IAsync<Bool>
  .Current  : IAsync<T>
 }

IAsync is just used to indicate that the result is awaitable.

Area-Language Design Question

Most helpful comment

Stepping back,

_Is there value in letting someone write a general-purpose combinator which operators upon arbitrary awaitable things?_

There hasn't been much need for it so far, since we all use Task everywhere. We also use the ConfiguredTaskAwaitable type but there's not much sense in writing a combinator over that. Some people make other things awaitable, e.g. a button-click or an animation, but that seems kind of niche.

With C#7 it will become more common to use one other kind of task, ValueTask. And so yes we will want to write combinators such as Task.WhenAll which can operate over both Task and ValueTask equally.

One oddity is that, even if we can write Task.WhenAll which operates upon both Task and ValueTask equally, _it has to commit to a concrete return type_. You wouldn't be able to write this:

T WhenAll<T>(params T[] args) where T : awaitable

The only way you could do this would be by adding an extra constraint that T must both follow the _awaiter_ pattern and also the _tasklike_ pattern.

I see these ways that you could write combinators:

1: T WhenAll<T>(T arg1, T arg2) where T : awaitable, tasklike
2: WhenAll(valueTask1, task2) // rely on implicit conversion from ValueTask to Task or vice versa
3: WhenAll(valueTask1.AsTask(), task2) // rely on an explicit conversion

Solving this problem at language level with [1] would be premature right now. We should see how much it's needed, and how well it's solved at the library level with [2,3]. If it is needed a lot, and if both of those approaches seem just too heavyweight, that's the time to jump in with a language feature. Not before!

All 10 comments

Since the compiler uses duck typing for this purpose, this constraint will also fall into that category.

I don't see the point. Such an interface could just define their members to return Task<T> or, eventually, ValueTask<T>.

The awaiter pattern is pretty loose and can be handled through an extension method which would make it impossible to emit common IL.

@HaloFour The current IEnumerable<T> / IEnumerator<T> doesn't permit awaiting on the GetEnumerator or the MoveNext.

interface IEnumerable<T>
{
  .GetEnumerator() : IEnumerator<T>
}

interface IEnumerator<T>
{
  .MoveNext() : Boolean
  .Current    : T
  .Reset()
}

Also wasn't @ljw1004 implementing it that it isn't restricted to just Task<T> or ValueTask<T>, allowing any type that followed the awaitable pattern?

@AdamSpeight2008

The current IEnumerable<T> / IEnumerator<T> doesn't permit awaiting on the GetEnumerator or the MoveNext.

This proposal wouldn't change that.

Also wasn't @ljw1004 implementing it that it isn't restricted to just Task<T> or ValueTask<T>, allowing any type that followed the awaitable pattern?

He's implementing asynchronous sequences so that IAsyncEnumerable<T> isn't required as long as the enumerated follows the pattern. As such generic constraints aren't at all necessary since you aren't required to use IAsyncEnumerable<T> or make it work with other awaitables.

Stepping back,

_Is there value in letting someone write a general-purpose combinator which operators upon arbitrary awaitable things?_

There hasn't been much need for it so far, since we all use Task everywhere. We also use the ConfiguredTaskAwaitable type but there's not much sense in writing a combinator over that. Some people make other things awaitable, e.g. a button-click or an animation, but that seems kind of niche.

With C#7 it will become more common to use one other kind of task, ValueTask. And so yes we will want to write combinators such as Task.WhenAll which can operate over both Task and ValueTask equally.

One oddity is that, even if we can write Task.WhenAll which operates upon both Task and ValueTask equally, _it has to commit to a concrete return type_. You wouldn't be able to write this:

T WhenAll<T>(params T[] args) where T : awaitable

The only way you could do this would be by adding an extra constraint that T must both follow the _awaiter_ pattern and also the _tasklike_ pattern.

I see these ways that you could write combinators:

1: T WhenAll<T>(T arg1, T arg2) where T : awaitable, tasklike
2: WhenAll(valueTask1, task2) // rely on implicit conversion from ValueTask to Task or vice versa
3: WhenAll(valueTask1.AsTask(), task2) // rely on an explicit conversion

Solving this problem at language level with [1] would be premature right now. We should see how much it's needed, and how well it's solved at the library level with [2,3]. If it is needed a lot, and if both of those approaches seem just too heavyweight, that's the time to jump in with a language feature. Not before!

@ljw1004

Some people make other things awaitable, e.g. a button-click or an animation, but that seems kind of niche.

I am pretty sure it's a common practice in both Xamarin and WPF these days. If people are async-aware they do it end-to-end.

I wonder if this could be achieved by types classes (https://github.com/dotnet/csharplang/issues/110) once they are implemented.

@ymassad

Relevant conversation: https://github.com/dotnet/csharplang/issues/1454#issuecomment-380534923

This feature would allow writing of orchestrating code irrelevant of the type of the awaitable.

As a concrete example, I have created a custom awaitable (and tasklike) called DfTask to present a operation in a producer-consumer dataflow. You can find the project here: https://github.com/ymassad/ProceduralDataflow

With the proposed feature I could write generic shared code describing the flow of the processing of data, and then use it both in cases where I need simply asynchrony (individual steps in processing are simple asynchronous operations) or when I need to implement the producer-consumer pattern.

As @ljw1004 said, we would need both awaitable and tasklike constraints to do this.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

nlwolf picture nlwolf  路  3Comments

DavidArno picture DavidArno  路  3Comments

OndrejPetrzilka picture OndrejPetrzilka  路  3Comments

MadsTorgersen picture MadsTorgersen  路  3Comments

asvishnyakov picture asvishnyakov  路  3Comments