await
is a suspension point marker and must always be visible. Like throw
it is for the developer benefit, not the compiler (compiler knows it already). That and the viral effect on the code base is one of the distinction / selling points of async/await compared to fiber based implementations (strictly speaking those could also use visual markers / function colouring to denote suspension points, I just haven't seen that done anywhere).
I'd say that there should be even more await
markers than now. Example:
await foo() + bar()
as written it's quite vague to me what's going on here. I know I can put the subexpressions in parens:
(await foo()) + (await bar())
but that's quite awkward and perhaps still impossible to represent some of the cases of a promise-based implementation:
foo().await + bar().await // both promises awaited and results added normally
(foo() + bar()).await // the sum of two promises (a promise) is awaited
foo() + bar() // the sum of two promises (a promise) is returned
foo().await + bar() // the sum of a value and another promise is returned
(foo().await + bar()).await // ditto the above, but awaited
OK, I see that the common conception is that one should be explicit here and having to use the await keyword is a good thing.
But: If the creator of an API decides that it should be usable without the await keyword, so he annotates the functions accordingly? Good or bad?
The other thing I asked somewhere else is if the async version of a function should have to be named differently (as it is currently the case), I do not really see a good reason, why not call the async version of forEach also forEach? I really think that at least both requirements make it quite cumbersome to e.g. switch from a synchronous piece of code to an asynchronous one.
What does this mean, exactly? The await
keyword is not a matter of API design. It's necessary for the same reason some form of try
is necessary for expressions which can throw.
await
is necessary because of the effect it has on the caller, not the callee.
The only situation I can imagine where this might apply is something like a function that witnesses an async
protocol requirement but is known by the conformer to not actually suspend, ever. It would be okay to call this without await
in a concrete context since there's no chance of suspension. But such a type could just provide a synchronous version of the function.
You can apply that argument to any function tagged with async
that is known to never suspend.
I would imagine that having a blanket rule so you can reason about the call site without inspecting the body of the called methods greatly outweighs any perceived or actual benefits of being able to exclude it in some scenarios.
After all, the same arguments can be, and have been, made for try
.
It's also worth calling out the horrible footgun that is "actor reentrancy" + "suspension points". await
is the lone defense against this exploding in very bad ways.
This has been my bane for the past week or so. Knowing I'm safe if I eliminate await
able calls is really a life-saver.
Right, I was just trying to envision a situation like @sspringer mentions where "[annotating] the functions accordingly" might be appropriate for the API author to indicate "I know this function is async
but it doesn't actually suspend." In most cases I think the response can just be "get rid of the async
then," but if it's witnessing a protocol requirement that might not be an option.
In any case, I agree that I don't really feel like this is a problem.
In current Swift, the one place I see await
becoming a bit onerous is for orchestration of a complex series of async tasks in a component with no local state. One example of this would be a shell-script-like program (the kind I enable with Shwift
: GitHub - GeorgeLyon/Shwift: Shell scripting in Swift). In the body of a script, almost all statements are expected to be async
, and the script itself does not have any state that could be accidentally mangled by suspension, nor do we expect reentrant behavior. In these cases await
becomes a little noisy and provides little-to-no value.
You can do this already:
protocol P {
func a() async
}
class C: P {
func a() {
print("a!")
}
}
C().a()
Oh cool, I had missed that synchronous functions could witness async
requirements!
Hmm. The actual method that is called is not async. But the class fullfills the requirements. ... So the async keyword in the protocol is kind of without meaning so to speak, observed something like this already. I am not really understanding this case. Does the protocol say that asynchronuous implementations are allowed, but not required?
Is this really true though? With SE-0338 it seems like any async function can suspend and even without awaiting anything in its body.
The async
keyword in the protocol allows a conformer to use an async
method, but it doesn't require one. If you think about it, this makes sense, as a synchronous function is a degenerate case of an async
method.
This ability gains more importance when the conformer is an actor. Take this modified example:
protocol P {
func a() async
}
actor A: P {
func a() {
print("a!")
}
func b() async {
print("b!")
}
func c() async {
a() // no await is necessary because it is invoked internal to the actor
await b() // await is necessary because the function is tagged async
}
}
Task {
await A().a() // await is necessary because the call is made from outside the actor
await A().c() // await is necessary because the call is made from outside the actor
}
We see the value in allowing a()
to be non-async even though it's witnessing an async
protocol requirement.
I don't see how SE-0338 changes the argument. It is always true that a function can never suspend anywhere except at a point where another async function is invoked. If this were not so, actors would be completely impractical, as one would be spending more time rechecking invariants than getting useful work done.
What I meant is that even though an async function may not suspend in its body, calling the function might suspend because of SE-0338.
What's wrong with calling it with await
(even when it known to never suspend)? In that case await is quick anyway. Removing a few characters doesn't worth it.
Bad because he thinks he's too smart for the language to hold his hand but he's wrong.
Good answer.