-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Add await
as a dotted postfix operator
#4076
Comments
await
as a postfix dotted operatorawait
as a dotted postfix operator
Wouldn't it be better to introduce pipes to the language than something very specific to await? |
Why would pipes be related to this proposal? Other methods aren't involved here. Pipes would seemingly need both null-propagation and await syntax on their own. |
Why wouldn't it? and why would you need both null propagation and await syntax? you don't really need that. |
I find This will encourage people to inline everything into a single line without any benefit other than just that and it still doesn't strike me as a common scenario. All the runtime repo has four instances of chained awaits, that should tell you something about it. To me, "a second way to do things." is too much cost for making such change. |
Can you explain why they would be related? My understanding of the pipe operator from the proposal here and from F# is that it is for taking an expression and passing the result to a method or delegate as an argument. That would not be related to this proposal. |
Null-conditional I was asked to open this proposal by @CyrusNajmabadi who probably agrees with you but thought it was worth bringing it up in the LDM. |
@HaloFour What I'm talking about is using something like the following:
So expanding the pipe proposal to support something like this. |
Then |
The pipe proposal isn't for member access, it's for taking the results of one expression and passing it as an argument to a function. Your expression would translate (somewhat) into: var result = await? TaskProp2(await? TaskProp1(await? task1)); |
@HaloFour I know that and this is the reason I stated the following "So expanding the pipe proposal to support something like this.". |
@eyalsk I think you need to come up with such an expansion first. The pipe operator works well in functional languages, but I think is tricky to get right in object oriented / procedural langages. |
So maybe open a separate proposal and make that case? But having the pipe operator also be member access makes very little sense to me. |
I disagree for reasons I stated clearly in my proposal. Null-conditional await and chained awaits do not work well together for the same reason that left-associativity did not work for null-conditional operators in general. You need to collapse the subexpression into a result before you chain, which means having to put If your distaste is in chained awaits in general I can understand that, but there is clearly some interest in it by the community. |
@HaloFour Well, I just wanted to share my point of view whether it makes sense to you is irrelevant, it reads better at least to me. |
Which is totally fine, but without explaining that you would rework the pipe operator to mean member access your comment that this proposal would be better solved by the pipe operator did not make any sense. As it stands I find them to be completely orthogonal concerns. |
@HaloFour Sure, I hope it makes more sense now. |
I'm not saying I don't like it, I'm saying it's not a common scenario and by that I don't mean it doesn't happen. I think that's why you had to include TaskProp to demonstrate usages, that's not really what I usually see or recommend to do. I think you'd agree with that. To be clear, I understand this proposal is trying to hit two birds at once at a cost of using a shotgun but I'd argue the other bird is not usually hanging around here and this is overkill. |
Will bring to next triage meeting. |
While this is unlikely to be an issue in practice, this could result in confusing behavior where the same valid code would behave differently in async and non-async functions: class MyTask<T> : Task<T> {
public T await => default;
public MyTask(Func<T> f) : base(f) { }
}
async Task Foo() {
// Currently errors, but would await under this change
var _ = new MyTask<int>(() => 5).await;
}
void Bar() {
// Currently no error, but has different behavior than awaiting the task
var _ = new MyTask<int>(() => 5).await;
} |
A) This is so unlikely, it's really not worth considering IMO using System.Threading.Tasks;
public class C {
public void await(Task t){}
public void M1() {
await (Task.CompletedTask);
}
public async void M2() {
await (Task.CompletedTask);
}
} |
This is already the case today. Consider something as simple as |
I strongly dislike this proposal.
|
this is not the scenario. That would be writable as |
Yes.
And so on and so forth. Actual real world examples (of many) in our codebase.
There is nothing poor about this. If these were synchronous methods no one would bat an eye at:
But the moment parts break up into being task-based, it becomes much harder to do basic data flow/manipulation like you see here.
Then don't use it if it's noisy for you. For me it would not be noisy and it would read a lot better than the current prefix-notation.
I have no idea how we even got onto functional programming. Pipeline operators are just a form of operators. They fit equally well into language like C# (which are hybrid-functional anyways) as they do into others. |
Is effective at its purpose. |
@HaloFour Pipes would require an implicit parameter token to support this:
|
When Rust first introduced this syntax, I was vehemently against it. But honestly, it's much cleaner to do something like this: var channel = client.GetGuildAsync(1234).await.GetChannelAsync(4321).await; Compared to this: var channel = await (await client.GetGuildAsync(1234)).GetChannelAsync(4321); The readability increase here is huge, and I can see myself using this a lot in the future, if it were added. |
There is an open PR open in roslyn dotnet/roslyn#47511 where we discussed to support auto-completion for await dotnet/roslyn#47511 (comment) (There is a screencast In the PR description that shows how this looks like for explicit casts, which basically is the same transformation): someTask.aw$$ // this triggers completion which contains "await" and "awaitf". $$ = cursor position
// committed suggestions ("await"):
await someTask$$
// or ("awaitf")
await someTask.ConfigureAwait(false)$$ This eases the pain of Having a postfix version would not just improve fluent chaining but would also improve code writing. |
You literally just proved my point of write everything in a single line, lol.
Why not then use ContinueWith or extension methods to accomplish this? It took me 1 minute to write 4 methods to chain tasks like this: var newRoot = await OrganizingService.OrganizeAsync(document).Then(x => x.GetSyntaxRootAsync());
return await task.Then(t => t.ContinueWithAsync(code, options, cancellationToken)).ConfigureAwait(false); This looks far better than
"Then don't use it if it's noisy for you" is very unprofessional. Using this excuse you can literally add any dumb feature to c# and say "if you don't like it don't use it". So this argument is completely invalid.
I think I'm being more subjective on that. I guess if I had to pick between I still don't like the proposal, this is perfectly achievable using extension methods without adding alternative syntax to do the same thing. |
These codebases can perfectly add a F# .dll to their project 😉
No problem, they can perfectly add a F# .dll to their project.
I don't need a mind shift, people needs to learn that you can perfectly add F# libraries to C# projects instead of converting C# into F#.
Very clear. 😉 |
In my opinion this proposal doesn't offer enough to make up the 1000-point deficit it starts with. It doesn't make something previously impossible now possible. It doesn't really save on characters. It doesn't provide any benefits to typing speed that tooling couldn't. All it does is make a controversial change to the aesthetics of the language which will likely be disallowed by many analyzers anyways for the sake of consistency with existing code. |
Right, because every dev shop run with a F# programmer on the bench...
You're so stuck about the fact that I mentioned pipes when I mainly referred to the syntax that to me fits better than the dot syntax as presented in the OP but even then introducing pipes into the language wouldn't turn C# into F# and honestly repeating yourself doesn't help so I'm not sure what you're on about, seriously. |
Because i want to use async/await. It's like asking why use
No, it's the standard for adding new language features. Language features will never be popular among all users. So we accept that "not using the feature" is a totally fine stance for people to take. There's nothing unprofessional about that at all.
Please demonstrate that showing how this would be done with extension methods. As a good thought experiment, please show how this would work while using a try/finally and also a loop with breaks/continues in it that you want to be able to invoke. |
I don't understand how this helps. Roslyn, for example, is not going to rewrite itself in F#. Furthermore, as C# language designers we don't eschew features just because F# has them. Many features we've added have been borne out of many different language camps from the past 60 years. C# is not a language for zealots. It's a language we've built by looking at what is useful and valuable to programmers elsewhere and asking ourselves how we might best be able to bring that value to our own ecosystem. You may not like this, but that is how we have always designed the language, and how we will continue to do so for the forseeable future. |
I don't think it's reasonable to switch to a different language, with all that entails (including switching projects), whenever I find a line of code that could be better written in F#. |
IMO this feature is at least worth considering. Nothing must-have in my opinion but over the years I stopped being that sceptical about all this little "not really necessary" features that C# added to make code more succinct and easier to read/write because I simply started using them 😆 Maybe it's worth noting that in Typescript's IDE you'll get auto completion for promise and auto insertions of await. It doesn't make code easier to read though, just easier to write. |
With Resharper postfix templates you can just write
and it becomes
I use this all the time. There are other such templates such as |
We considered this in LDM today, and came to the conclusion that we will not implement this proposal as is. While we like the space of improving await, we think the big issue to address is |
I dunno if it's worth commenting on a closed issue, but I know that now that Rust has gone with a dotted postfix await, people now generally love it (despite a ton of teeth gnashing during the design phase), and that some folks I know who use both C# and Rust wish that C# had dotted postfix await for better chaining support. |
One of the reasons for rejection:
Could someone shed some light on what is meant by this? The pipe operator discussed above? I have a hard time imagining something that would achieve this. |
Did not notice the proposal. I would like something like- var result = task1 ?> await |> @?.TaskProp1 ?> await |> @?.TaskProp2 ?> await;
// or
var result = task1 ?> await ?> @.TaskProp1 ?> await ?> @.TaskProp2 ?> await; Just my opinion, not that serious. |
looks much nicer than
to me. |
If possible, Furthermore, considering that C# now has the #region Declare an asynchronous method
IAsyncEnumerable<int> GetNumbersAsync(int start, int finish)@
{
for (var number = start; number <= finish; number++)
{
Task.Delay(1)@;
yield return number;
}
}
#endregion
#region Iterating with Async Enumerables
foreach (var number in GetNumbersAsync(1, 5))@
{
Console.WriteLine($"This number is {number}.");
}
#endregion
#region Replace the parenthesized asynchronous expression
Task<int> GetContentLengthAsync(string url)@
{
return new HttpClient().GetStringAsync(url)@.Length;
}
#endregion
#region Register an asynchronous lambda expression
Button.ClickEvent += (o, e)@ =>
{
var contentLength = GetContentLengthAsync("http://msdn.microsoft.com")@;
Console.WriteLine($"This length is {contentLength}.");
};
#endregion Although both Rust's But we don't have a time machine, considering the principles of "explicit declaration of asynchronous invocation", "one thing, one way" and "backward compatibility", But I believe that a solution in the more distant future will be "fully automatic", allowing developers to focus entirely on business logic, "writing synchronous code, where execution is always asynchronous", eliminating the need to worry about I/O-bound ( |
May be It would be better to get rid of "await" at all for postfix case and use special symbol instead? So instead of initial proposal: |
Postfix await has replaced extension properties as my number-one C# wish-list item. |
Add
await
as a dotted postfix operator inasync
methodsSummary
I propose that within the context of an
async
member that theawait
keyword can be used as a dotted postfix operator toawait
the task-like operand. This will enable fluent chaining of async operations.This was suggested by @paulhickman-a365: #35 (comment)
Other relevant discussions:
#4058
#1117
#827
Motivation
There have been several proposals opened looking to create more fluent ways to chain awaiting multiple tasks. The current syntax can be awkward in those cases as it requires using parenthesis to group the expression resulting in each task-like value:
By allowing the
await
keyword to follow the expression as a dotted postfix operator it would make this chaining much easier:This is a seemingly minor change, but I think it becomes more important with a null conditional
await
syntax being considered. Such a syntax would not be able to propagatenull
with short-circuiting, requiring the developer to wrap each expression in parenthesis which will evaluate to the result of the task-like type and require chaining of the null-conditional await operator:Any of those missing
?
characters could result in aNullReferenceException
. If theawait
keyword could be used as a dotted postfix operator then the existing null propagation syntax and its shortcircuiting behavior would seemingly fit better:Here the null check only needs to be applied once and if
task1
isnull
then the entire expression is short-circuited. Additional null propagation operators would only be necessary ifTaskProp1
orTaskProp2
could also returnnull
, as with null propagation operators in synchronous scenarios.Detailed design
The dotted postfix form is an alternative to the existing prefix form of the
await
operator:The operator applies to the expression off of which it is dotted and has a higher precedence that the prefix
await
operator, which obviates the need for parenthesis:Both forms can be mixed:
When used with null-propagation:
Drawbacks
await
embedded within an expression.Alternatives
Unresolved questions
Design meetings
https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-11-11.md#add-await-as-a-dotted-prefix-operator
The text was updated successfully, but these errors were encountered: