This is a discussion thread for _arbitrary async returns_, as specified in https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns.md
You can try it out online! tryroslyn.azurewebsites.net
Please check the spec and design rationale before adding your comment - it might have been addressed in the latest draft.
(The previous discussion thread was here: #7169. I closed that thread after we more or less settled on a proposal.)
AsyncEnumerator? Not quite sure how the syntax would go (e.g. where await is) Kind smilar or same as the Observable.
foreach (var row in await dataStream)
{
// ...
}
@benaadams, that's for a different topic: https://github.com/dotnet/roslyn/issues/261
@ljw1004 Just FYI, I've added your branch to TryRoslyn, e.g. http://is.gd/Yjvb2P.
Can't promise timely builds at the moment, but might be easier for a demo than a VSIX download.
@ashmind That is totally awesome! I've updated links.
I'm having doubts about the [Tasklike]
attribute to distinguish a type as being tasklike. I amended the prototype so it also supports a static method on the tasklike type itself, which seems a lot cleaner...
class MyTasklike<T>
{
[EditorBrowsable(EditorBrowsable.Never)]
public static MyBuilder<T> GetAsyncMethodBuilder() => ...
}
The problem is that this mechanism can never work for tasklike _interfaces_ like IObservable
and IAsyncAction
and ITask
.
@ljw1004 could it pick up interfaces via extension? e.g.
static class MyTasklikeExtensions<T>
{
[EditorBrowsable(EditorBrowsable.Never)]
public static MyBuilder<T> GetAsyncMethodBuilder(this IAsyncAction asyncAction) => ...
}
We'd discussed this when we first implemented async and were pretty down on the idea of an extension method where you deliberately have to pass a null this
argument...
I'm also a bit worried that the question of whether something is tasklike is no longer an inherent property of the type but instead depends on the context you're looking it up from. I think this context information has never previously had to be threaded into this part of overload resolution.
More notes on the subject: https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns%20-%20discussion.md#discuss-connection-between-tasklike-and-builder
@ljw1004 quite tricky! I suppose as long it doesn't preclude a future use on interfaces.
It could be deferred and either be resolved as option 1, or in conjunction with one of the other interface proposals as a follow up, after seeing where they go?
e.g. Proposal: Virtual extension methods #258, Proposal: Static Interfaces #2204
I'm loving this. Having access to the task-like type construction lets you do all sorts of things that aren't really in the spirit of async
but are cool nonetheless.
For example, I threw together an implementation of NullableTaskLike
that acts like Haskell's Maybe
monad with do-notation. You can check it out here:
https://github.com/ckimes89/arbitrary-async-return-nullable/
edit: I should note that the current implementation isn't compatible with asynchronous Tasklikes.
An addition to the rational of why things like ValueTask are important.
Due to async
's viral nature its never just one async
method but a whole stack chain of async methods.
So if one method at the bottom of the chain is actually async and the call chain is 30 methods deep with each on await
ing the method below; that's 30 tasks that are being allocated and awaited with associated costs.
Love to see this as well, it always felt a little off to me that async, the language feature, had a dependency on a specific type instead of a pattern as is common with most of the other language features
@aL3891 Well there's precedent for the language having a dependency on a type, given how yield
works with IEnumerable
. However, I'd like to see that given the same treatment as async
/await
and Task
.
True, but at least that an interface :)
Continuing on the theme of abusing this feature, I managed to mimic yield/IEnumerable with Tasklikes:
static void Main()
{
var list = DoListThings();
foreach(var item in list)
Console.WriteLine(item);
// prints 1, 2, 3
}
static async ListTaskLike<int> DoListThings()
{
await ListTaskLike.Yield(1);
await ListTaskLike.Yield(2);
await ListTaskLike.Yield(3);
return await ListTaskLike.Break<int>();
}
Also, it's got a neat little monadic property to it as well in that you can await child ListTaskLikes and all of the values will be included (like LINQ's SelectMany). Doing this in C# with yield requires a foreach loop:
static async ListTaskLike<int> DoListThings()
{
await ListTaskLike.Yield(1);
await DoNestedListThings();
return await ListTaskLike.Break<int>();
}
static async ListTaskLike<int> DoNestedListThings()
{
await ListTaskLike.Yield(2);
await ListTaskLike.Yield(3);
return await ListTaskLike.Break<int>();
}
foreach(var item in DoListThings())
Console.WriteLine(item);
// prints 1, 2, 3
Code is here: https://github.com/ckimes89/arbitrary-async-return-nullable/blob/list-tasklike/NullableTaskLike/ListTaskLike.cs
I want to step back and take stock of the key open issues remaining in the design rationale:
Q1. How to identify a tasklike, and find its builder? [[link](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns%20-%20discussion.md#discuss-how-to-identify-tasklikes-and-find-their-builder)]
GetAsyncMethodBuilder()
method on the tasklikeGetAsyncMethodBuilder()
method on default(Tasklike)
TasklikeAttribute
or AsyncMethodBuilderAttribute
Q2. Can we reduce the number of heap allocations? [[link](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns%20-%20discussion.md#discuss-reduce-number-of-heap-allocations)]
Q3. Should await even be handled by the builder? [[link](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns%20-%20discussion.md#discuss-awaitoncompleted-in-the-tasklikes-builder)]
AwaitOnCompleted
to be hard-coded by the compiler+BCL in terms of how they capture execution context and box.Q4. Can an async method body interact with its builder? [[link](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns%20-%20discussion.md#discuss-async-method-interacts-with-the-builder-instance)]
async
a reserved keyword inside async methods, similar to this
and base
, and have it refer to the Async
property of the builder.async.CancellationToken
or async.Progress
, if your tasklike is one where cancellation and progress are inherent to the tasklike rather than parameters to the method.Q5. Debugging support missing? [[link](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns%20-%20discussion.md#discuss-debugging-support)]
Q6. Dynamic? [[link](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns%20-%20discussion.md#discuss-dynamic)]
Q7. IObservable? [[link](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns%20-%20discussion.md#discuss-iobservable)]
IObservable
. Probably that will rely upon async iterators.IObservable
and async iterators?Q8. Back-compat breaks? [[link](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns%20-%20discussion.md#discuss-back-compat-breaks)]
Q2. Always nice :smile_cat:
Q5. Soft launch feature with C#7, big announce when debugging working?
Q6. dynamic would defeat a lot of the advantages anyway?
Q7. suppose it depends on probably of a better design arising for arbitrary-async-returns based on the further exploration of IObservable and async iterators.
Arbitrary-async-returns so far is a very powerful feature by itself and stands on its own merits.
However, if there is likely to be conflict between the two designs then might be cause for pause.
Would really like the arbitrary-async-returns though so it would make me :cry:
Q1. How to identify a tasklike, and find its builder?
I'm a big fan of the static method since it seems way cleaner, but as you mentioned elsewhere it doesn't work with interfaces. It could be combined with another way of specifying the builder, but it seems really goofy to have two different ways to do it.
If a builder can be specified by an extension method, are there any requirements on where that extension method is located (e.g. same namespace, assembly)? If not, how are conflicts resolved? Normal extension methods can be invoked statically but that's obviously not going to work here. Here, I lean towards enforcing that the extension method is defined in the same assembly and namespace. It doesn't allow you to extend third-party types, but I'd argue that it's up to the designer of the type whether or not it should be used as an async return. Also, if you want to extend a third-party type then you can still just extend the actual type and create your own builder.
It seems like an attribute would work for the majority of cases, but it disallows some edge case uses of builder construction. For example, with an extension or static class method you could return different concrete instances of builders, switched on the generic type, that may have some type-specific behavior or optimizations. It seems like an unlikely use case, but it's completely precluded when using an attribute. EDIT: Disregard this paragraph, forgot that the attributed type requires a static Create
method.
I don't like attributing the async method at all. It's incredibly cumbersome.
Q3. Should await even be handled by the builder?
I'm a bit torn on this one. On the one hand, implementing AwaitOnCompleted correctly will probably have plenty of landmines for an implementer to trip on so they shouldn't be exposed to what amounts to an implementation detail. On the other hand, you would lose the ability to do some of the weirder stuff I did like the ListTaskLike (though it could be argued that that's a benefit).
Is there some middle ground, like inheriting from a TasklikeBuilder class that has some methods implemented for you already?
Q2. Maybe possible for some new tasklikes that haven't yet been written? I think you would need to inline some of the stuff in AsyncMethodBuilder.cs from the clr to get the cold path down to one allocation (and keep a hot path at zero). A bunch of that code is internal though.
Q3. By default we can shell out to an async method builder that already exists (this is what I did in the enumerable value task builder), but allowing us to inject at this point on the ASM is nice.
Q4. I think intellisense/tooling around this is at least as important as debugging features.
Q6. Would not changing the late-binder now introduce backwards compat issues if/when we do consider changing it?
Q7. Option 2 sounds right from my perspective and should be related to IAsyncEnumerable features I think. On the other hand, why not both? In the IAsyncEnumerable code it feels a little strange to call .Start()
on the builder when that doesn't actually start anything until the first bool e.MoveNext()
is called. I think a better solution would be producing
public IObservable<string> Option1()
{
var m = new SM1();
m.State = -1;
m.Builder = ObservableBuilder<string>.Create();
m.Builder.Start(ref machine);
return m.Builder.Task;
}
by virtue of not having any yield
in it and having return <string>
and
public IObservable<string> Option2()
{
var m = new SM2();
m.State = -1;
m.Builder = ObservableBuilder<string>.Create();
m.Builder.Prepare(ref machine);
return m.Builder.TaskFactory;
}
when yield
is there?
I added a new "key question":
Q8. Back-compat breaks? [[link](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns%20-%20discussion.md#discuss-back-compat-breaks)]
// Example 1: in C#6 this code compiles fine and picks the first overload,
// but in C#7 it picks the second for being better
void f(Func<Task<double>> lambda)
void f(Func<ValueTask<int>> lambda)
f(async () => 3);
// Example 2: in C#6 this code compiles fine and picks the first overload,
// but in C#7 it gives an ambiguity error
void g(Func<Task<int>> lambda)
void g(Func<ValueTask<int>> lambda)
g(async () => 3);
// Example 3: in C#6 this code compiles fine and picks the first overload with T=Task<int>,
// but in C#7 it picks the second with T=int for being more specific.
void h<T>(Func<T> lambda)
void h<T>(Func<ValueTask<T>> lambda)
h(async () => 3);
@bbarry could you spell out what additional "intellisense/tooling" you're thinking about, beyond debugger support?
Editor features mostly, for example on the method:
async IAsyncActionWithProgress<string> TestAsync(HttpClient client)
{
await client.GetStringAsync(url, async.CancellationToken);
async.Progress?.Invoke("10%");
}
It would be nice if the keyword async
identified as an instance of some IAsyncActionWithProgressAsync<string>
class (because that is what the builder's Async
property is) for the purposes of hover help text, property and method discovery and so on.
Q8. should it break? In that the ValueTask
path would all be sync currently with no use of await
. Or will awaiting on a ValueTask
change in behaviour?
If it did break, users of ValueTask
are innovators and it won't have hit early adopters yet (using the Technology adoption life cycle terms) and would likely see this as an acceptable breaking change for the gain in being able to use them in async
methods; which increases their utility enormously and opens the door for wider adoption.
However... if the await
has breaking behaviour then there might be upstream breaks on people that use the libraries; but haven't written the code?
Which might mean new type other than ValueTask
that has the same functionality StructTask
(Not so good though and maybe confusing having two)
What I was trying to express is better summarised by: What would compat issues mean cross version e.g. C#6 app using C#7 lib and C#7 app using C#6 lib
@benaadams, I'm sorry but I can't understand what you've written :) Maybe this will help... I wrote out in precise detail what I think the back-compat issues are and how we might mitigate them:
https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns%20-%20discussion.md#discuss-back-compat-breaks
We'll be discussing this proposal at today's C# Language Design Meeting. Fingers crossed!
@ljw1004 wow... I think you've covered everything :+1:
@benaadams @rynowak I'm confused by the memory-savings of ValueTask...
Were you talking about different benchmarks? I'm suspicious because the numbers "30k vs 3k" and "750MB vs 770MB" and "10% vs 90%" seem too similar to be coincidence...
We discussed this proposal at C# language design meeting. Mads Torgersen took notes and will post them online in the coming days.
The team were generally supportive of the idea in general. As for specifically doing it in C#7 rather than leaving it until C#8, they didn't rule it out for C#7, and were willing to continue investigation, but with the big caveat that there simply might not be enough time.
I didn't have time to take full notes during the meeting. There were just a few things that I jotted down:
Use GetAwaiter. There was a fascinating suggestion. In the proposal currently, given f(async () => 3)
in void f<T>(Func<ValueTask<T>> x)
it goes from the expression 3
to the type int
to inferring the lambda return type ValueTask<int>
to inferring T=int
. This relies on the "magic" of assuming a single generic type parameter, as discussed in the rationale. But what if we could remove that magic? What if we said that tasklikes _must be awaitable_ (similar to how a collection initializer can only be done on a type that's foreachable). Then we could look at the tasklike's return type from GetAwaiter().GetResult()
to determine what we're talking about. That could potentially allow for tasklikes with more than one generic parameter. It might also open up the door to IAsyncActionWithProgress
being used for async iterators. Needs further investigation. One potential downside is that the determination of whether something's awaitable requires extension method lookup...
Different hammer for overload resolution. There's a central problem with f(async () => 3)
being passed to f<T>(Func<T> x)
and inferring T=Task<int>
. The problem is that no one can honestly implement such a method f
which works graciously and correctly regardless of whether it's given an async lambda (which you have to await) or a normal lambda (which you don't). What we've done so far is encourage authors to write a second overload f<T>(Func<Task<T>> x)
to catch the async lambda case so it can do the awaiting, and we've relied on the very delicate overload resolution rules. But this proposal is running into the same problem. I regret that we ever allowed it to infer T=Task<int>
in the first place, but it's too late to change that. How about we change it from a different angle? How about we allow you to declare an overload like these?
// this first option is like a constraint
void f<T>(Func<T> lambda) where T:not an inferred tasklike out of an async lambda
// this second option uses the "async" keyword specifically to light up the possibility
// of allowing tasklikes
void f<T>(async Func<ValueTask<T>> lambda)
void f<T>(async Func<T> lambda)
Efficiency. There was the suggestion that ValueTask
is a bit less efficient than Task
in cases where you do end up awaiting. Therefore folks might have to examine each and every one of their methods, judge whether it's likely to await or not, and choose Task
vs ValueTask
based on that guess.
API design. A crucial part of the feature, which I missed entirely, is consideration in practical terms of how people will upgrade their existing Task-based codebases to ValueTask while maintaining source and binary compatibility, and while also opting into the new features easily. For instance,
// EXAMPLE 1:
void f<T>(Func<Task<T>> lambda) // I have this API already
void f<T>(Func<ValueTask<T>> lambda) // I wish to introduce this API
// In the proposal as it stands, this would be an error: it'd be impossible to add
// ValueTask-consuming APIs alongside your existing ones
// EXAMPLE 2:
async Task<string> GetStuffAsync() // I have this API already
async ValueTask<string> GetStuffAsync() // I wish to introduce this API
// It's simply impossible to do this (for maintaining binary compatibility).
// It's never allowed to add a new method that differs only in return type.
// EXAMPLE 3:
async Task<string> GetStuffAsync() // I have this API already
async ValueTask<string> GetStuffAsync() // I will replace it with this one
// Let's say that binary compat isn't important, but source compat is.
// If I change over to ValueTask then it'll likely break consumers
Implicit conversion. This was a fascinating idea. Imagine if ValueTask<T>
has an implicit conversion to Task<T>
. That way, in the following example, we don't need to change the rules for overload resolution. We could rely on the existing "better conversion from target" to say that Func<ValueTask<int>>
is a better conversion target than Func<Task<int>>
. It would therefore be preferred over both the other candidates without even needing to enter the tie-breakers.
void f<T>(Func<T> lambda)
void f<T>(Func<Task<T>> lambda)
void f<T>(Func<ValueTask<T>> lambda)
f(async () => 3);
The advantage is we don't have to mess with overload resolution, and users would be able to add ValueTask-consuming APIs to their existing libraries. And although it doesn't help binary-compat, it does help source-compat when folks replace Task-returning APIs with ValueTask-returning APIs.
At the language design meeting, there was also the suggestion that this proposal would remove the problem of back-compat-breaks (on the grounds that we could now say that Task candidates are uniformly preferred to other-tasklike candidates), but as I type up my notes now I'm not seeing it...
@stephentoub articulated the downside of this implicit conversion. The conversion from ValueTask to Task causes a heap allocation, and with it being implicit then it would be easy to do, which kind of defeats the whole point which is people using ValueTask because they want to avoid heap allocation. It's similar to the implicit allocation you get when boxing, e.g. converting a struct to an interface or to object. It's a shame that the language actively fights non-allocating folks, makes it so easy for them to allocate, but maybe that's just the legacy and nature of the language. Maybe it should be the job of an analyzer to warn about this and other cases of implicit "boxing-like" code.
Not attributes. Not during the meeting itself, but in discussion with language design team members, there was a common sentiment that it's bad for attributes to influence language semantics. I'm strongly inclined to favor the "static GetAsyncMethodBuilder()
method on tasklike" option. This would mean that at time of release, we'd be unable to make interfaces tasklike (e.g. ITask
, IAsyncAction
, IObservable
). That would have to await until such time as the language gets "extension everything" or at least "extension statics".
@ljw1004 same benchmark (used the repo @rynowak linked to at top)
I increased the repetitions x10 to ensure that the buffer pool and value task were completely amortising the allocations and not just reducing them (so at 300k it would have likely saved 99% of allocations) if that makes sense?
I had a thought related to ValueTask<T>
; though slightly adjacent and to do with the virality of async; but for the non-generic, non-value returning Task
.
Another issue with allocation and async is it isn't just the first Task
but often a long call stack chain of Task
s that get allocated. Even if you create a non-allocating custom awaiter; when someone await
s it that function becomes Task
returning and allocating.
You can often do some refactoring; pass cached Task
s, or do pass-through where the last statement is a return of the called function. However, its often very complicated to do this for an entire call stack.
@ljw1004 your 0 heap allocation example for arbitrary async returns has inspired me a little:
async ValueTask<int> ValueTaskAsync(int delay)
{
await Task.Delay(delay);
return 10;
}
Could this be done for regular Task
s to make an entire call chain non-allocating?
async ValueTask A(int delay)
{
// some work
await Task.Delay(delay);
// some work
}
async ValueTask B(int delay)
{
for (var i = 0; i < 10; i++)
{
// some work
await A(delay);
// some work
}
}
async ValueTask C(int delay)
{
for (var i = 0; i < 10; i++)
{
// some work
await B(delay);
// some work
}
}
async ValueTask D(int delay)
{
for (var i = 0; i < 10; i++)
{
// some work
await C(delay);
// some work
}
}
Regular Task
return await D(0);
would create 1 + 10 + 10 * 10 + 10 * 10 * 10 * 2 = 2111 Tasks?
Whereas the ValueTask
chain above would allocate 0 (for delay = 0)
May have got that wrong...
Could this be done for regular Task s to make an entire call chain non-allocating?
If delay here is 0, this would be non-allocating with Task. If a Task-returning async method completes synchronously, you get back Task.CompletedTask. There is no benefit to a non-generic ValueTask.
If delay here is 0, this would be non-allocating with Task. If a Task-returning async method completes synchronously, you get back Task.CompletedTask.
And if delay wasn't 0 they'd all be created either way..?
And if delay wasn't 0 they'd all be created either way..?
Right (assuming of course that it actually caused the method to yield).
I obviously need more coffee :frowning: Will stop going off topic then as it already does correct thing and related optimizations are covered by https://github.com/dotnet/roslyn/issues/10449. Sorry for the diversion.
A note where I'm at with this: We have two different options on the table for overload resolution:
Task
today. But to avoid back-compat-breaks, prefer candidates which don't involve converting an async lambda to a non-Task-returning delegate parameter. [[link: precise spec](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns.md#overload-resolution-option-1-treat-tasklikes-same-as-task)]ValueTask
having an implicit conversion to Task
. [[link: precise spec](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns.md#overload-resolution-option-2-rely-on-implicit-conversion-valuetask-to-task)]I want to analyze those two options in the light of what are the important use-cases:
ValueTask
as a wholesale replacement for Task
, every bit as good. [[link: specific examples](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns.md#i-should-be-able-to-use-valuetask-as-a-wholesale-replacement-for-task-every-bit-as-good)]ValueTask
, maintaining source-compatibility and binary-compatibility. [[link: specific examples](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns.md#i-should-be-able-to-migrate-my-existing-api-over-to-valuetask)]ValueTask
is already tasklike. When I upgrade to C#7, I don't want the behavior of my code to change. [[link: specific examples](https://github.com/ljw1004/roslyn/blob/features/async-return/docs/specs/feature%20-%20arbitrary%20async%20returns.md#i-dont-want-to-break-backwards-compatibility)]So my next step is to fill out this evaluation table, to see which option satisfies the use-cases best. (The table has multiple columns for whether there's an implicit conversion between ValueTask
and Task
, since that has a big effect on overload resolution.)
| | Option1 | Option1 with VT->T | Option1 with VT<-T | Option1 with VT<->T | Option2 with VT->T |
| --- | --- | --- | --- | --- | --- |
| Is VT as good as T? | ? | ? | ? | ? | ? |
| Can I migrate from T to VT? | ? | ? | ? | ? | ? |
| Is back-compat okay? | ? | ? | ? | ? | ? |
Okay, I've finished my investigation into overload resolution. Here are the two options:
[Option "IC"]: Don't change overload resolution much; instead rely on a user-defined implicit conversion from ValueTask
to Task
. (The only tweak needed to overload resolution is that an async lambda async () => 3
should be considered an exact match not only for Func<Task<int>>
but also for Func<OtherTasklike<int>>
.)
[Option "E"]: Make overload resolution treat tasklikes equivalently as it treats Task
today (so it has the same "exact match" tweak as above, and also "betterness" will dig into Tasklike<T>
just like it digs into Task<T>
, and also the more-specific-conversion tie-breakers will be used if candidates are identical up to tasklikes). But build in a preference for Task
over ValueTask
to preserve back-compat (so that async lambdas prefer to be converted to Task-returning than other-tasklike-returning delegates).
Task <-> ValueTask
implicit conversions there might happen to be.Task
over other tasklikes, which preserves back-compat at the expense of being worse for API authors. We could go in the opposite direction and preserve other tasklikes over Task
, to favor API authors at the expense of back-compat.The above table only shows the 13 interesting "language design unit tests". The full set is here
Just to clarify on
user-defined implicit conversion from ValueTask to Task
TaskLikes would require the conversion for this rule to engage; however would the framework defined ValueTask<T>
have this implicit conversion defined so always engage?
I need to go back to the drawing board on my analysis. I had misunderstood the way step#1 "ExactMatch" vs step#2 "better conversion target" play off.
[EDIT:] I've updated the above post. (I stepped through in the debugger to ensure my understanding is now correct!) This has reversed my conclusions.
Proposal: "tasklike" modifier on types.
@gafter proposed this neat idea for overload resolution. It stems from unease that overload resolution should be affected by a static method inside a type (potentially even an extension static method in a type). He suggests to add a new modifier to type declarations:
tasklike struct ValueTask<T>
{
}
tasklike interface IAsyncAction
{
}
(I've written the modifier tasklike
, but it might be async
). It would be emitted in IL as an attribute [System.Runtime.CompilerServices.Tasklike]
.
The compiler needs knowledge of tasklikes in four places:
CreateAsyncMethodBuilder
.CreateAsyncMethodBuilder
.Parts [3,4] are deep language semantics and it feels odd for them to be controlled by a method, especially an extension static method. It feels nicer for them to be driven by a modifier on the type. So we'd have two separate concepts:
Probably it would be a handy compile-time error if you have the modifier but no static method within the type.
Advantage: overload resolution would now be unaffected by which extension methods are visible.
Disadvantage: you could no longer change a third-party type to be tasklike. Also the concept "tasklike" has become more difficult.
Unclear: We have to figure out an end-to-end story for how users get hold of the attribute. Which NuGet package would it be in? Would it be in the desktop .NET Framework 4.7 or whatever the next version will be? How about..._ What if, when compiling a piece of code that uses the tasklike modifier but doesn't reference a library which defines the attribute, then the compiler synthesizes an internal copy of the attribute into the assembly? Must test to see whether that works.
@ljw1004 Automatically synthesizing internal attributes is problematic since they will be visible to any assembly to which the current assembly has internals-visible-to access. That could lead to ambiguities.
It would be very useful if we could however. For example, another use case is state machine attributes. Currently EnC doesn't work if you don't have these attributes available (the compiler doesn't emit them on async/iterator methods). If we could emit them this restriction would go away.
Perhaps we need [ReallyInternal] attribute that prevents an internal type from being visible outside of the containing assembly regardless of internals-visible-to.
I do like the type modifier suggestion from a language point of view. it doesn't seem incongruous or too much of a deviation from the rest of the language/async:
async class TaskType<T>
{
}
async struct ValueTask<T>
{
}
async interface IAsyncAction
{
}
For tasklike for 3rd party; could always create a wrapper struct to make it tasklike, should be fairly low cost?
We have a tweak on this feature up for discussion. I'd love to hear what you fellow designers on github think...
To recap, there are four distinct parts to this feature proposal:
async ValueTask<int> f() {...}
Func<ValueTask<int>> lambda = async () => {...}
void f<T>(Func<ValueTask<T>> lambda)
and f(async () => {... return 3;})
it infers T=int
void f<T>(Func<T> lambda)
and void f<T>(Func<ValueTask<T>> lambda)
it should use the same smarts it already has for Task
to prefer the more specific overload.In the proposal as stands, we use the presence of a static method ValueTask<T>.CreateAsyncMethodBuilder()
to enable all four features.
_The complaint has been raised that this feels wonky: it's wonky that the presence or absence of a member should affect something as fundamental as type inference and overload resolution. Once we get static extension methods in a future release, it might feel even more wonky._
So here are three proposals. Which one do you like?
CreateAsyncMethodBuilder()
method on the tasklike type;async
modifier on the tasklike type, e.g. async struct ValueTask<T> {...}
async
modifier and the CreateAsyncMethodBuilder()
methodasync
modifierCreateAsyncMethodBuilder()
methodCreateAsyncMethodBuilder()
method.If we opted for [A] or [B], they involve a new async
modifier. We would encode this in metadata with a new attribute System.Runtime.CompilerServices.TasklikeAttribute
or similar. (Exact name TBD).
System.TasklikeAttribute
. If you want to declare a tasklike type yourself, you'd need to add a reference to this NuGet package. If you want to use a library from NuGet with tasklike types (e.g. System.Threading.Tasks.Extensions
which contains ValueTask<T>
) then the attribute will be pulled in automatically. If you want to reference a raw DLL which contains a tasklike type, then you'll have to reference the NuGet package System.TasklikeAttribute
yourself.System.TasklikeAttribute
as above (or have it transitively referenced by another NuGet package that you depend upon).System.TasklikeAttribute
will be included automatically. (It will just be a type forwarder to mscorlib in the desktop case).System.Runtime.CompilerServices.TasklikeAttribute
yourself somewhere. The compiler will happily recognize it wherever it is defined.IAsyncAction
or IObservable
to be tasklike. Proposal mixed would let anyone do that for purposes of declaring methods at least.IObservable
and similar types to be tasklike,. That could only be done by the authors of IObservable
.ValueTask
to be tasklike.,Disadvantage: Proposal modifier wouldn't let third parties make IObservable and similar types to be tasklike,. That could only be done by the authors of IObservable.
What be the effect of IObservable
and similar types being modified with async
to existing libraries that used it?
There's another tweak up for discussion...
Proposal: making async
a contextual keyword _inside_ async tasklike-returning methods, similar to how value
is a contextual keyword inside property property setters.
In general, I've encountered the need for the async method to communicate with the _currently active instance of the async builder_. Here are three examples:
// EXAMPLE 1: in the WinRT type IAsyncAction, cancellation is a faculty of the returned
// object (hence of the async method builder); not just an argument passed into the async method.
IAsyncAction a = f();
a.Cancel();
async IAsyncAction f()
{
... await http.GetStringAsync(url, async.CancellationToken);
// Maybe we could get at it this way!
}
// EXAMPLE 2: once we get to IAsyncEnumerable, it's possible that cancellation will be
// done at the level of MoveNext or GetEnumerator -- i.e. not at the level of the async iterator
// method itself
IAsyncEnumerable<int> a = f();
using (IAsyncEnumerator<int> e = a.GetEnumerator(cancel))
{
while (await e.MoveNextAsync()) Console.WriteLine(e.Current);
}
async IAsyncEnumerable<int> f()
{
yield return 1;
await Task.Delay(100, async.CancellationToken);
// Maybe we could get at the cancellation token this way!
yield return 2;
}
// EXAMPLE 3: to write a "aspect oriented programming" task (which inserts a method-call
// before and after every cold await), the async method instance needs to communicate with
// its builder...
async WeavingTask f()
{
async.ConfigureBeforeAwait( () => Console.WriteLine("before await"));
await Task.Delay(10);
}
The way this would work is that things like async.CancellationToken
and async.ConfigureAwait
are not baked into the language...
IAsyncAction
would expose a particular member and that member would have a CancellationToken
property.for
IAsyncEnumerablewould expose a particular member and that member would have a
ConfigureBeforeAwait` method.We have identified a reasonable need for the body of an async method to communicate with the current _instance_ of its builder. It already does so under the hood using stock methods like SetResult
, SetException
, AwaitOnCompleted
. We will probably want to open this up so library authors can allow other ways to communicate, specific to the particular tasklike.
async
a contextual reserved keyword inside async tasklike-returning methods for now. In a future release of C#, that will give us the freedom to wire it up and make it do something interesting.nameof
. Just like the pseudo-keyword nameof
works in C#6, let's say that async
works in the same way: when you use this identifier e.g. to write async.CancellationToken
, if async
binds to an identifier that already exists, then bind to that identifier. But if it doesn't, then treat it as the contextual keyword.The reserved proposal has a slight advantage in elegance of Roslyn APIs.
The nameof proposal has the advantage that, should we ever decide to expand Task
in future with some kind of stuff in this vein, that will be fine.
@benaadams wrote _"What would be the effect of IObservable
being modified with async
to existing libraries that used it?"_
There might be slight changes in type inference and overload resolution, in rare edge cases.
Ok, but it wouldn't be a versioning and interface break everything type change?
Other question would be could tasklikes work with generic constaints in someway? So you could have a method that took a <T> where T : async
or <T<TValue>> where T : async
and then you could await
the type T
?
Not sure if its important; but might give a different perspective...
I don't like an attribute-based approach because the following disadvantages are too large.
Two cents on the modifier approach -- if you go with explicit annotation, why not use [Tasklike]
directly instead of a keyword (tasklike
or async
)? After all there is no keyword for [Serializable]
or [CallerMemberName]
and this feature feels even more edge-case than those.
Also I think static extension methods are not the only future extensibility story. I'm sure there are cases for extension attributes as well (even though it might require a framework change).
@ashmind For whatever reason there's a strong reluctance in C# to let attributes influence language semantics. That's why C# uses "this" keyword to indicate an extension method, rather than [Extension] like VB does.
@ljw1004 It would be great if C# team described this goal and its exceptions — e.g. tasklike vs caller attributes. This would be useful for future issues as well. For example if the choice is based on target audience or usage frequency, this issue seems OK — advanced feature, likely to be used for only a few classes.
Disadvantage: Proposal mixed allows for a goofy state where a type looks and feels and smells tasklike in some cases, but not in others.
I think this alone is enough to discount the mixed proposal.
If the modifier proposal went ahead, I think it would be best to accept both a keyword modifier and an attribute modifier form; both async class MyTask
and [Tasklike] class MyTask
should work within and across assemblies. In addition if this is how it is implemented, it would be nice to have a (separate proposal... wholly optional) attribute on async methods when they get compiled that could also be used in place of the keyword there (there is a bit of reflection wierdness when looking at a method and determining if it is intended to be awaited).
As to a decision of async
being a reserved contextual keyword vs a magic identifier, I'd prefer a keyword, but that sounds like it might be too much of a breaking change. Either it would not be a contextual keyword in async Task<>
methods (only in non-Task
tasklikes?), or it would potentially conflict with variables named async
as well as members and types in scope in existing code.
@ashmind Caller attributes do not affect the language's static semantics.
This was the topic for yesterday's LDM. We rejected my three proposals, and picked up the suggestion of @ashmind above (which indeed several people have independently proposed). Thanks @ashmind. I'll be updating the feature spec shortly. In the meantime, here are the raw notes from LDM.
Previously we recognized a tasklike by the presence of a static CreateAsyncMethodBuilder()
on it. Now instead we want to recognize it by an attribute: e.g.
// non-generic tasklike
[AsyncBuilder(typeof(MyBuilder))] struct MyTasklike {...}
struct MyBuilder { public static MyBuilder Create() {...} }
// generic tasklike
[AsyncBuilder(typeof(MyBuilder<>))] struct MyTasklike<T> {...}
struct MyBuilder<T> { public static MyBuilder<T> Create() {...} }
namespace System.Runtime.CompilerServices {
internal class AsyncBuilderAttribute : Attribute {
public AsyncBuilderAttribute(Type t) { }
}
}
If an attribute with the fully-qualified name System.Runtime.CompilerServices.AsyncBuilderAttribute
is present on a type, and that type has arity 0 or 1 or is Task
or Task<T>
, then the type is considered to be tasklike.
The typeof
argument to the attribute is called the tasklike's _builder type_.
The attribute can be defined anywhere. The compiler merely checks for it by name. One interesting thing about the way C# works, you can have _internal_ attribute classes (as shown in the above example). It doesn't have to be internal but we recommend this is how to write tasklike types in a library DLL. When a user writes a project that references your DLL, the compiler will know that your type is tasklike, but the user's code won't be able to use that attribute. (that said, it's possible that a future version of mscorlib will include the attribute as public, and so if you also declare it in your library you'll get a compile-time error; at that time you'd have to split into two versions of your library.)
Requirement 1: a tasklike's builder type must have the same generic arity as the tasklike type itself.
Requirement 2: a builder type must have a public static parameterless method "static BuilderType Create()" which returns a value of the builder type.
Requirement 3: the attribute must have a constructor which takes a System.Type
It's implementation-defined whether the above two requirements are validated when we declare a tasklike type or validated when we declare an async method/lambda that returns that tasklike type. If the former, then we would also have to validate the requirements upon meta-import of a tasklike type, and deem it non-tasklike if the requirements aren't met. I prefer the latter. (Requirement 3 might instead be rolled into the test of whether a type is tasklike).
We had discussed the idea of using attributes early on, and indeed my first prototype used attributes. In my evaluation at the time I had said "it's ugly" and "it requires us to ship the attribute". The second objection proved false because of the "internal" trick described above, which I hadn't known. The first objection -- well, it is ugly, but we also came to feel the alternatives were a little ugly too.
Should we have used a modifier like async(typeof(BuilderType<>))
rather than an attribute? Maybe, but that would be a crazy amount of language design work for a tiny audience. We estimate that in the eventual C# ecosystem maybe 5 people will write tasklike types that get mainstream adoption. (Most people will just write async methods that produce pre-existing tasklikes such as ValueTask
).
What about the objection from people like @gafter that it's wrong for an attribute to influence the static semantics of the language (i.e. typing, type inference, type-directed overload resolution)? Well, @gafter was mollified by the thought that whenever he sees a tasklike type with the attribute, he'll close his eyes and imagine his "happy place" where it really is a modifier. Only the above 5 people will write it as an attribute anyway.
The attribute has one neat benefit -- it can be used on WinRT types like IAsyncAction
as-is, without requiring an additional language feature "extension statics". I will kick off a discussion with the WinRT and CLR-projection-layer folks to see if that's possible.
There's also a downside to the attribute. It means that you can't take someone else's pre-existing type and make it tasklike via "extension statics" or similar, as noted by @ufcpp. That is a shame. We figure that the need for this is pretty small. It will be ameliorated if the type authors themselves do the work in a timely fashion (e.g. like IAsyncAction
above), and by the paragraph below...
We discussed one avenue for future expansion that was advocated by @stephentoub, and I think @bbarry that this is what you described too. Imagine if you wanted to return a pre-existing type, but for sake of your own method, you want to pick a different builder type. We might decide to re-use the attribute for this purpose but place it on the async method rather than the tasklike type. (This won't work for async lambdas of course since they have no place to hang the attribute; it's also not clear if we'd limit this feature solely to return types which are already considered tasklike).
[AsyncBuilder(typeof(MyOwnTaskBuilder))]
async Task FredAsync() {...}
[AsyncBuilder(typeof(MyOwnBufferBlockBuilder))]
async BufferBlock<int> JonesAsync() {...}
We discussed another avenue for future expansion that was described by @MadsTorgersen. The previous proposal let us have a builder type whose generic arity differed from that of the tasklike. If we ever decide that this flexibility is desirable in future, we're open to achieving the same thing with a [AsyncBuilderFactory]
attribute.
// Previous proposal:
public class MyTasklike<T>
{
public static MyBuilder<string,int,T> CreateAsyncBuilder() => ...
}
// How that might be done with attributes
[AsyncBuilderFactory(typeof(BF))] public class MyTasklike<T> {...}
internal struct BF
{
public static MyBuilder<string,int,T> Create<T>(MyTasklike<T> dummy) => ...
// The compiler would construct default(tasklike), and invoke this function with it,
// and that's how it would get an instance of the builder.
}
Yeah it could be nice to be able to write:
[AsyncBuilder(typeof(MyOwnBufferBlockBuilder))]
async BufferBlock<int> JonesAsync() {...}
though it isn't exactly what I was suggesting (I did think it might be nice but left that out of my comment as extra fluff that might be distracting). As you have said, it is likely there might be 5 people who write their own tasklikes. I suspect it is more likely that there will be several people who come up with interesting unforseen abuses of the system to do things like @ckimes89's nullable stuff above, so this level of customization seems unnecessary.
I was suggesting that the compiler adds such an attribute when building the method to annotate that the method is in fact built by some async builder to reflection. There is a slight tooling issue today in that Task
is a return type used both for async and for concurrency, but unless the author decides to follow conventions and name their method *Async
there is no way for a user to know the method is intended to be awaited (vs a background compute task or an unstarted task). Thus I was suggesting that an async
method get an attribute when compiled:
user writes:
public static async Task Foo() { await ...; }
builds as:
[AsyncBuilder(typeof(TaskBuilder))]
public static Task Foo() ...
And the user (and tools) can see this attribute via reflection and present a richer experience when suggesting await
in code analysis and editing.
Further I was suggesting it would be nice for authors to be able to omit the async
keyword if they provide the attribute:
[AsyncBuilder(typeof(TaskBuilder))]
public static Task Foo() { await ...; }
(in this hypothetical, the attribute and keyword are interchangeable and if both are used, the attribute wins)
Impact on existing libraries would be perhaps worse tooling interactions (though suggesting an await pattern on every use of Task
today is wrong as well).
@bbarry wrote:
There is a slight tooling issue today in that Task is a return type used both for async and for concurrency, but unless the author decides to follow conventions and name their method *Async there is no way for a user to know the method is intended to be awaited (vs a background compute task or an unstarted task).
I'm suspicious of that... I think you generally still want to await a background compute task or (after starting it) an unstarted task.
Could you spell out (maybe with concrete examples) what kinds of things are intended to be awaited and which ones aren't?
[this is a derail from the topic of this issue, but I figure we can continue with it for a couple of posts before it merits its own issue...]
@ljw1004
(that said, it's possible that a future version of mscorlib will include the attribute as public, and so when you write
Did you mean to continue this sentence?
suppose I was writing some parallel code:
static int ParallelTaskImageProcessing(Bitmap source1,
Bitmap source2, Bitmap layer1, Bitmap layer2,
Graphics blender)
{
Task toGray = Task.Factory.StartNew(() =>
SetToGray(source1, layer1));
Task rotate = Task.Factory.StartNew(() =>
Rotate(source2, layer2));
Task.WaitAll(toGray, rotate);
Blend(layer1, layer2, blender);
return source1.Width;
}
Someone could have instead done this:
static int ParallelTaskImageProcessing(Bitmap source1,
Bitmap source2, Bitmap layer1, Bitmap layer2,
Graphics blender)
{
Task toGray = SetToGray(source1, layer1);
Task rotate = Rotate(source2, layer2);
Task.WaitAll(toGray, rotate);
Blend(layer1, layer2, blender);
return source1.Width;
}
which could be written:
static Task<int> ParallelTaskImageProcessing(Bitmap source1,
Bitmap source2, Bitmap layer1, Bitmap layer2,
Graphics blender)
{
return Task.WhenAll(
SetToGray(source1, layer1),
Rotate(source2, layer2)
)
.ContinueWith(t =>
{
Blend(layer1, layer2, blender);
return source1.Width;
});
}
and could have a tool assisted conversion:
static async Task<int> ParallelTaskImageProcessing(Bitmap source1,
Bitmap source2, Bitmap layer1, Bitmap layer2,
Graphics blender)
{
await Task.WhenAll(
SetToGray(source1, layer1),
Rotate(source2, layer2)
);
Blend(layer1, layer2, blender);
return source1.Width;
}
I am fairly sure that would still work, but now the code is going through the machinery for SetContinuationForAwait
instead of ContinueWith
and is compiling a state machine and so on... which may not be a decision I want to make without a profiler around.
Then again perhaps I'm thinking too hard and shouldn't worry about trying to await IO bound workflows that are likely to wait and not awaiting CPU bound ones.
@bbarry I think the only solution is convention and developer education. Consider a "mixed" async method:
async Task DoStuffAsync()
{
var buf = await GetBufAsync();
for (int i=0; i<buf.Length; i++) do_cpu_bound_stuff;
}
Should you be awaiting this or not? Ultimately the answer has nothing to do with whether or not the method was written with the async modifier. The answer is that, as an "abstraction" principle, you should write methods which are either CPU-light (and they may very well return an awaitable) or CPU-heavy (and they shouldn't block on IO and the caller should be the one to decide which and how many threads to allocate to them).
I've now implemented the [AsyncBuilder(typeof(builderType))]
approach. During implementation I noticed a few things that we hadn't thought of. They're not important; I'm just calling them out for completeness.
Which of the following are tasklike?
[AsyncBuilder("hello")] class Tasklike1 { }
[AsyncBuilder(typeof(void))] class Tasklike2 { }
[AsyncBuilder(typeof(B3))] class Tasklike3 { }
class B3<T> { } // generic arity differs from that of tasklike
[AsyncBuilder(typeof(B4))] class Tasklike4 { }
class B4<T> { private static string Create() => null; ... } // Create method returns wrong type
I adopted the following rule: A type is considered tasklike if it has the AsyncBuilder(System.Type)
attribute constructor on it. All of the above are tasklike types. They generate other errors of course...
Which of the following should be allowed?
class Outer<T>
{
class Builder { }
[AsyncBuilder(typeof(Builder))] class Tasklike1 { }
[AsyncBuilder(typeof(Outer<>.Builder))] class Tasklike2 { }
[AsyncBuilder(typeof(Outer<T>.Builder))] class Tasklike3 { }
[AsyncBuilder(typeof(Outer<int>.Builder))] class Tasklike4 { }
}
Outer<>
?The following is allowed, because the builder isn't in a generic class:
class Outer
{
class Builder { }
[AsyncBuilder(typeof(Outer.Builder))] class Tasklike { }
}
On the builder type, it looks for a public Create
method and a public Task
property with the correct signatures. I honestly couldn't be bothered spending effort on cases where they have more limited accessibility.
Can you can make the builder type itself have less accessibility, e.g. make it internal? That will allow your library to declare async methods that return your tasklike type, but no one else will be able to. I think this is a recipe for confusion and I'll disallow it.
That will allow your library to declare async methods that return your tasklike type, but no one else will be able to. Is that okay? What do people think?
I think this is ok.
You are leveraging the machinery of the compiler to build a method in a particular way (as an instance of this state machine pattern). It is possible that the builder has no purpose in the public api or even that it requires particular knowledge to write and could be harmful for a casual user to build an async method with.
An example I am thinking is perhaps there is an unmanaged API out there which could otherwise be used in an async pattern, except that various parameters need to be configured before each await state. Given an async
keyword someone might write something like this:
public async ITaskLike<Result> DoIt()
{
var config = SetItUp();
async.Configure(config);
var step1 = await Step1();
var nextconfig = Determine(config, step1);
async.Configure(nextconfig);
return await Step2();
}
where
[AsyncBuilder(typeof(BarTaskLikeBuilder<>))]
public interface ITaskLike<T> ...
internal sealed class BarTaskLike<T> : ITaskLike<T> ...
internal sealed class BarTaskLikeBuilder<T> ...
It would be possible to require the library to hand write the machinery here, but why should the compiler enforce such an arbitrary restriction?
It seems strange to me to have a tasklike that only you can actually use as a tasklike. The reason for using an internal builder would be that it provides some useful functionality for you, but not useful enough that you would want to expose it to a caller of your library - which seems like a pretty strange situation. Also, given the attribute based approach, no caller of your library could introduce their own tasklike functionality for your ostensibly tasklike type.
On the other hand, allowing internal builder types is a superset of the functionality offered by requiring all public builder types, so it's not like we're sacrificing capabilities to support internal builders. If a strange situation arises (like the example from @bbarry) then you're not forced by the language to make a potentially unsafe API.
I switched to requiring the builder to have the same accessibility as the tasklike... it's a minor issue, and the arguments in both directions (keep them the same vs allow them to be different) aren't all that major, and the implementation turned out to be easy+clean to require them to be the same.
The PR for the change is here: https://github.com/dotnet/roslyn/pull/13405
@jaredpar spotted a flaw with the attribute approach. We had proposed that a DLL which declares a tasklike type would also declare its own internal copy of the AsyncBuilderAttribute class. But consider what happens when you try to produce a reference assembly out of this DLL. You'd need to include the internal type! This seems crummy and against the spirit of either reference assemblies or the internal modifier or both.
So instead: we won't rely on the "internal" trick. Instead, ValueTask.dll will declare AsyncBuilderAttribute as public. Library authors who wish to declare tasklikes can either declare a public AsyncBuilderAttribute themselves too, or take a dependency on ValueTask, whichever is easier.
In future we still do anticipate adding AsyncBuilderAttribute to mscorlib (and to System.Runtime in the .NETCore nuget world). On such platforms, ValueTask would switch to having a type forwarder for its AsyncBuilderAttribute.
@ljw1004
Instead, ValueTask.dll will declare AsyncBuilderAttribute as public. Library authors who wish to declare tasklikes can either declare a public AsyncBuilderAttribute themselves too, or take a dependency on ValueTask, whichever is easier.
Won't that cause issues if I want to reference both ValueTask
and a library that declares its own public AsyncBuilderAttribute
? Something similar happened with [Serializable]
in NUnit: https://github.com/dotnet/corefx/issues/10822.
@svick it will cause an issue, but only if you wish to refer to the attribute (i.e. if you're authoring a tasklike). If you merely write an async method that returns a tasklike, then your code won't refer to the attribute, and you won't suffer the ambiguity warning.
_There will be far fewer people authoring tasklikes than writing [Serializable]
data-structures!_
I think "easier" cuts both ways in this case. I'd hope that tasklike authors will take care to optimize for ease of consumption of their tasklikes.
"Why would a type be considered task-like in overload resolution if the builder type is inaccessible?"
I want to delve into this question a little further.
The way I implemented is that the compiler calls a IsCustomTasklike()
function, which merely checks for the presence of the [AsyncBuilder(System.Type)]
attribute, nothing more. That’s a nice fast check (could even be done at meta-import time) and is independent of anything else. And I deferred all further validation of the builder type to the point where it’s actually needed for codegen to construct a tasklike-returning async method/lambda.
In this design, a type type can pass the IsCustomTasklike()
test (as used in overload resolution) even despite having inaccessible builder.
Why should it be so? Why not come up with a different scheme, to which the question alludes, where IsCustomTasklike()
does some more validation of builder (e.g. of its accessibility) and maybe even all validation (e.g. of its AwaitOnCompleted
method)?
I didn’t go for this option because it would be a more costly check without benefit. And unless we do the full costly check, it would feel like a halfway-house. Halfway never feels right!
In C# Language Design Meeting, @gafter pushed for us not to use a static CreateAsyncMethodBuilder()
because of the following argument, which folks generally agreed with:
tasklike class C { }
to control OverloadResolution+TypeInference for this reason, again independently of any implementation details about the builder.CreateAsyncMethodBuilder()
was a fine way to do this.We settled on using an attribute to control both bullets, [AsyncBuilder(typeof(builder))] class Tasklike { }
.
There are two opposing language design philosophies in play here: (1) the two bullets are conceptually different; (2) it's confusing if the two bullets are out of sync.
In respect of (2), I required builders have the same accessibility as the tasklike. This rules out the ability to have a type which consumers of your library think of in some technical ways as something that can be returned from an async method/lambda without themselves being able to return it from an async method/lambda.
In respect of (1), and in a design that only really is noticeable in error situations, I said that the first bullet is controlled solely by the presence of [AsyncBuilder(System.Type)]
attribute irrespective of its argument, and the second bullet is controlled by its argument being valid.
One thing I like about this approach is that it lets us relax the rules for what is a valid builder type in future (e.g. allow it to be more generic, or covariant, or come from an inheritance hierarchy) without it being a breaking change. The typical way the language gets broken is because relaxed rules cause overload resolution changes. But, with this approach, that won't happen.
Now that the final PR has been merged, I think it's time to close this thread.
https://github.com/dotnet/roslyn/pull/13405#event-779778762
Thanks everyone for your help -- it's been a fun ride!
Thanks for this really great feature.
I'm about to release a new version of my package, and I wanted to be reasonably sure that this language feature isn't going to be rolled back at the last minute (as happened with some C# 6.0 features in VS2015). I understand we're still in RC and no guarantees can be made, but is it reasonable to depend on generalized async return types?
Most helpful comment
I'm loving this. Having access to the task-like type construction lets you do all sorts of things that aren't really in the spirit of
async
but are cool nonetheless.For example, I threw together an implementation of
NullableTaskLike
that acts like Haskell'sMaybe
monad with do-notation. You can check it out here:https://github.com/ckimes89/arbitrary-async-return-nullable/
edit: I should note that the current implementation isn't compatible with asynchronous Tasklikes.