Skip to content
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

Closed
1 of 4 tasks
HaloFour opened this issue Oct 27, 2020 · 52 comments
Closed
1 of 4 tasks

Proposal: Add await as a dotted postfix operator #4076

HaloFour opened this issue Oct 27, 2020 · 52 comments
Assignees
Milestone

Comments

@HaloFour
Copy link
Contributor

HaloFour commented Oct 27, 2020

Add await as a dotted postfix operator in async methods

  • Proposed
  • Prototype: Not Started
  • Implementation: Not Started
  • Specification: Not Started

Summary

I propose that within the context of an async member that the await keyword can be used as a dotted postfix operator to await 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:

var result = await (await (await task1).TaskProp1).TaskProp2;

By allowing the await keyword to follow the expression as a dotted postfix operator it would make this chaining much easier:

var result = task1.await.TaskProp1.await.TaskProp2.await;

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 propagate null 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:

// task1 is nullable
var result = await? (await? (await? task1)?.TaskProp1)?.TaskProp2;

Any of those missing ? characters could result in a NullReferenceException. If the await keyword could be used as a dotted postfix operator then the existing null propagation syntax and its shortcircuiting behavior would seemingly fit better:

var result = task1?.await.TaskProp1.await.TaskProp2.await;

Here the null check only needs to be applied once and if task1 is null then the entire expression is short-circuited. Additional null propagation operators would only be necessary if TaskProp1 or TaskProp2 could also return null, 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:

var result = task1.await;
// equivalent to
var result = await task1;

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:

var result = task1.await.TaskProp1.await;
// equivalent to
var result = await (await task1).TaskProp1;

Both forms can be mixed:

var result = await task1.await.TaskProp1;
// equivalent to
var result = await (await task1).TaskProp1;

When used with null-propagation:

int? result = task1?.await.TaskProp1.await;
// equivalent to

int? result;
if (task1 != null) {
    result = await (await task1).TaskProp1;
}
else {
    result = null;
}

Drawbacks

  1. This creates a second way to do things.
  2. This could make it easier to miss an 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

@HaloFour HaloFour changed the title Proposal: Add await as a postfix dotted operator Proposal: Add await as a dotted postfix operator Oct 27, 2020
@HaloFour
Copy link
Contributor Author

cc @CyrusNajmabadi

@iam3yal
Copy link
Contributor

iam3yal commented Oct 27, 2020

Wouldn't it be better to introduce pipes to the language than something very specific to await?

@HaloFour
Copy link
Contributor Author

HaloFour commented Oct 27, 2020

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.

@iam3yal
Copy link
Contributor

iam3yal commented Oct 27, 2020

Why wouldn't it? and why would you need both null propagation and await syntax? you don't really need that.

@alrz
Copy link
Contributor

alrz commented Oct 27, 2020

I find foreach? far more common than null-aware or chained await. when you use an async method you have zero chance to get a null task, and NRT is making sure of it.

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.

@HaloFour
Copy link
Contributor Author

HaloFour commented Oct 27, 2020

@eyalsk

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.

@HaloFour
Copy link
Contributor Author

HaloFour commented Oct 27, 2020

@alrz

Null-conditional await has been championed. The result of a null-conditional await will be a null, which I believe will make this situation more common.

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.

@iam3yal
Copy link
Contributor

iam3yal commented Oct 27, 2020

@HaloFour What I'm talking about is using something like the following:

var result = await? task1
 |> await? TaskProp1
 |> await? TaskProp2

So expanding the pipe proposal to support something like this.

@alrz
Copy link
Contributor

alrz commented Oct 27, 2020

Null-conditional await has been championed. The result of a null-conditional await will be a null, which I believe will make this situation more common.

Then await? will be sufficient. It has nothing to do with chained awaits which this proposal is trying to make easier by proposing a completely new syntax. IDE can already help with that using suffix snippets. That's an editing experience which applies to a few language constructs, not just await.

@HaloFour
Copy link
Contributor Author

@eyalsk

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));

@iam3yal
Copy link
Contributor

iam3yal commented Oct 27, 2020

@HaloFour I know that and this is the reason I stated the following "So expanding the pipe proposal to support something like this.".

@YairHalberstadt
Copy link
Contributor

@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.

@HaloFour
Copy link
Contributor Author

@eyalsk

I know that and this is the reason I stated the following "So expanding the pipe proposal to support something like this.".

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.

@HaloFour
Copy link
Contributor Author

HaloFour commented Oct 27, 2020

@alrz

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 ?s all over the place.

If your distaste is in chained awaits in general I can understand that, but there is clearly some interest in it by the community.

@iam3yal
Copy link
Contributor

iam3yal commented Oct 27, 2020

@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.

@HaloFour
Copy link
Contributor Author

@eyalsk

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.

@iam3yal
Copy link
Contributor

iam3yal commented Oct 27, 2020

@HaloFour Sure, I hope it makes more sense now.

@alrz
Copy link
Contributor

alrz commented Oct 27, 2020

If your distaste is in chained awaits

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.

@CyrusNajmabadi
Copy link
Member

Will bring to next triage meeting.

@TheUnlocked
Copy link

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;
}

@YairHalberstadt
Copy link
Contributor

@TheUnlocked

A) This is so unlikely, it's really not worth considering IMO
B) That can already happen:

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);
    }
}

@CyrusNajmabadi
Copy link
Member

this could result in confusing behavior where the same valid code would behave differently in async and non-async functions:

This is already the case today. Consider something as simple as await(x). It has vastly different meaning in async/non-async contexts.

@JustNrik
Copy link

I strongly dislike this proposal.

  • The proposed syntax is horrible, I don't like piping syntax either since c# is not a functional language.
  • The example looks more like made by the kind of people that wants to do everything in 1 single line, which makes the proposal to look like an excuse of poor coding. Why not just await every method in their respective line and store the result in a variable even if it's only used once?
  • It kills readability, await task2(await task); doesn't offer any readability advantage over task2.task.await.await; // or whatever, both are equally unreadable and I would consider both as code smell.
  • It adds more unnecessary syntax, you already mentioned it, we don't need more ways to do the same thing, specially if it's for zero gain...

@CyrusNajmabadi
Copy link
Member

It kills readability, await task2(await task); doesn't offer any readability advantage over task2.task.await.await;

this is not the scenario. That would be writable as task2(task.await).await. The scenario this is for is await (await (await task)); Or things like await? (await? (await? task1)?.TaskProp1)?.TaskProp2;

@CyrusNajmabadi
Copy link
Member

Can you provide a real life example?

Yes.

var newRoot = await (await OrganizingService.OrganizeAsync(document)).GetSyntaxRootAsync();
return await (await task.ConfigureAwait(false)).ContinueWithAsync(code, options, cancellationToken).ConfigureAwait(false);

And so on and so forth. Actual real world examples (of many) in our codebase.

Because this looks like an excuse of poor coding for me.

There is nothing poor about this. If these were synchronous methods no one would bat an eye at:

var newRoot = OrganizingService.Organize(document).GetSyntaxRoot()

But the moment parts break up into being task-based, it becomes much harder to do basic data flow/manipulation like you see here.

I can see the point, but await is a bit more verbose and having many awaits in a single line will be really noisy,

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.

But if people want functional programming

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.

@CyrusNajmabadi
Copy link
Member

Define "works well" because Rust syntax isn't any far from horrible.

Is effective at its purpose.
Is easy to read and maintain.
Composes well with itself and other language features.

@orthoxerox
Copy link

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.

@HaloFour Pipes would require an implicit parameter token to support this:

var result = await? task1 |> await? @?.TaskProp1 |> await? @?.TaskProp2;
var newRoot = await OrganizingService.OrganizeAsync(document) |> await @.GetSyntaxRootAsync();
return await task.ConfigureAwait(false) |> await @.ContinueWithAsync(code, options, cancellationToken).ConfigureAwait(false);

@FiniteReality
Copy link

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.

@MaStr11
Copy link

MaStr11 commented Oct 30, 2020

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 await being a prefix operator during writing code a bit. This downside of prefix operators ("Oh, I need to await this task. Let's move the cursor to the other side of the expression, add await, and move it back again.") was not even mentioned in the proposal.

Having a postfix version would not just improve fluent chaining but would also improve code writing.

@JustNrik
Copy link

JustNrik commented Oct 30, 2020

Can you provide a real life example?

Yes.

var newRoot = await (await OrganizingService.OrganizeAsync(document)).GetSyntaxRootAsync();
return await (await task.ConfigureAwait(false)).ContinueWithAsync(code, options, cancellationToken).ConfigureAwait(false);

And so on and so forth. Actual real world examples (of many) in our codebase.

You literally just proved my point of write everything in a single line, lol.

Because this looks like an excuse of poor coding for me.

There is nothing poor about this. If these were synchronous methods no one would bat an eye at:

var newRoot = OrganizingService.Organize(document).GetSyntaxRoot()

But the moment parts break up into being task-based, it becomes much harder to do basic data flow/manipulation like you see here.

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 .await, even adding null handling for this is trivial.

I can see the point, but await is a bit more verbose and having many awaits in a single line will be really noisy,

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.

"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.

But if people want functional programming

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.

I think I'm being more subjective on that. I guess if I had to pick between .await and pipeline operators, I would go for the operator.

I still don't like the proposal, this is perfectly achievable using extension methods without adding alternative syntax to do the same thing.

@JustNrik
Copy link

Why not move to f# then? slowing converting c# into f# doesn't make sense when f# already exist. I don't mind c# getting some functional features because many of them are really useful, like pattern matching. But if people want functional programming, then just move to f# already, there's no reason to convert c# into a functional language. c# grabbing features from f# or (functional languages in general) to be multi-paradigm is fine, c# being slowly converted into a f# clone is not.

Let me break it down for you:

1. There are existing codebases that could benefit from functional features.

These codebases can perfectly add a F# .dll to their project 😉

2. Many people can't just move from F# to C# for many different reasons.

No problem, they can perfectly add a F# .dll to their project.

3. You probably have to do a mind shift here because if a feature is useful enough to consumers of the C# language the question of whether C# is converting to F# is irrelevant and it's not really the situation here but anyway it was just an idea because I didn't like the syntax. 😄

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#.

I dislike piping because of the functional-style syntax, not because of whatever you were thinking, which btw doesn't come to the case. It just doesn't fit c# imho.

Yeah, it's clear. 😉

Very clear. 😉

@TheUnlocked
Copy link

TheUnlocked commented Oct 30, 2020

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.

@iam3yal
Copy link
Contributor

iam3yal commented Oct 31, 2020

These codebases can perfectly add a F# .dll to their project

Right, because every dev shop run with a F# programmer on the bench...

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#.

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.

@CyrusNajmabadi
Copy link
Member

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:

Because i want to use async/await. It's like asking why use await in the first place since it can just be done with .ContinueWith. async/await allows me to use normal control flow constructs. I can be in loops. I can use try/finally. I can properly jump around as my logic requires. Using .ContinueWith is a large step backwards here and negates the purpose of having async/await in the first place.

"Then don't use it if it's noisy for you" is very unprofessional.

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.

I still don't like the proposal, this is perfectly achievable using extension methods without adding alternative syntax to do the same thing.

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.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Oct 31, 2020

These codebases can perfectly add a F# .dll to their project

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.

@svick
Copy link
Contributor

svick commented Oct 31, 2020

@JustNrik

These codebases can perfectly add a F# .dll to their project

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#.

@mpawelski
Copy link

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.

typescript_auto_await

@GSPP
Copy link

GSPP commented Nov 11, 2020

With Resharper postfix templates you can just write

myTask.await<enter>

and it becomes

await myTask

I use this all the time. There are other such templates such as .if or .var.

@333fred 333fred added this to the Likely Never milestone Nov 11, 2020
@333fred
Copy link
Member

333fred commented Nov 11, 2020

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 ConfigureAwait, and while improving fluency would be a nice bonus, the costs and general mixed reaction to this syntax form aren't worth it. https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-11-11.md#add-await-as-a-dotted-prefix-operator

@333fred 333fred closed this as completed Nov 11, 2020
@ericsampson
Copy link

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.

@bert2
Copy link

bert2 commented Jan 8, 2021

One of the reasons for rejection:

A more general approach that simplified chaining generally for prefix operators would be more interesting.

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.

@gulshan
Copy link

gulshan commented Jan 8, 2021

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.

@mpawelski
Copy link

@bert2 here is nice article about pipeline operator and its concerns with async that JavaScript community tried to solve.

@SomeoneIsWorking
Copy link

 var result = collection
    .TransformElements()
    .FetchData()
    .await
    .ProcessResults()
    .Aggregate();

looks much nicer than

var result = (await collection
    .TransformElements()
    .FetchData())
    .ProcessResults()
    .Aggregate();

to me.
I really don't understand what the negative feedback is all about.
What's the such a big problem with allowing the nicer looking option?

@tzengshinfu
Copy link

If possible,
I would suggest treating async/await as "Tokens," similar to (), {}, ., +-*/, etc.
It looks similar to JavaScript's Generator (function* expression).

Furthermore, considering that C# now has the await foreach syntax,
which cannot be applied to .await (Rust) or .await() (Kotlin),
I propose using @, as shown below:

#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 .await and Kotlin's .await() (Java's Virtual Threads + Future.get() might also be considered),
"Postfix await" syntax is closer to a synchronous style.

But we don't have a time machine, considering the principles of "explicit declaration of asynchronous invocation", "one thing, one way" and "backward compatibility",
C# will never adopt "Postfix await".

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 (Task) or CPU-bound (Task.Run) and the notorious .ConfigureAwait(false).

@troepolik
Copy link

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:
var result = await (await (await task1).TaskProp1).TaskProp2;
var result = task1.await.TaskProp1.await.TaskProp2.await;
I'd like to have operator like that:
var result = task1->TaskProp1->TaskProp2;
It is smaller and dont look like field with name 'await'

@sharpjs
Copy link

sharpjs commented Apr 4, 2024

Postfix await has replaced extension properties as my number-one C# wish-list item.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests