Thanks a lot for looking into this Joe and writing up the potential alternatives!
I've been going around in circles about this for a long time... The "await always" seems tempting at first but devolves terribly when more complex flow control is encountered. The just implicitly awaiting has me worried about invisible suspension points...
Thanks for bringing up the (2) alternative design with the "scopes" having to be awaited on -- it is worth entertaining but in the end it somehow feels a bit weird -- it'd feel more natural if everything in swift was an expression (ifs, for loops etc) perhaps ![]()
Just so it doesn't fall through the cracks, we also spent some time with @kavon in the last weeks wrapping our heads around it and ended up going in circles and arrived back at implicit awaits being reasonable, writeup here: SE-0317: Async Let - #87 by ktoso
——
We might consider exploring option (3), i.e. no explicit awaits a bit more, before giving up on it completely. Even though perhaps we’ll (again) end up at the implicit awaits in the end (?).
On one hand it can be annoying, on another though it can be useful and perhaps we can sugar this a little bit.
Consider the following:
func fun() async {
for … {
async let x = ..
async let y = <throws>
}
}
here I missed that one of my tasks if throwing, and I really don’t want to miss that.
In reality, I'd also get warnings about un-used variables here, and we can improve those warnings to also talk about "these tasks would be cancelled immediately, so you better await on them somewhere" - I'm happy with that.
What I wanted to mention though is the following, maybe worth thinking more about:
- if at the end of a scope, exist some not awaited on variables
we'd allow to a "catch all await", like so:
func fun() async throws { // <see 1> helped me not miss that error
for … {
async let x = ..
async let y = <throws>
if something { await x }
// y was not awaited on
// x maybe was awaited on
try await // <1> "catch all await", control flow sensitive
}
}
On line <1> we'd ask developers to acknowledge the suspension point of "all that other stuff". Control flow sensitive information would be necessary to know if that would be throwing or not.
If it was just pending not-throwing tasks it'd be:
async let t = <throws>
async let x = ...
try await t
if ... { await x }
await // no `try` needed because
// all "not sure if awaited" tasks were already awaited
It feels like this might add a little bit of noise, but add a lot of clarity to the examples as well.
This would also silence warnings about "child task binding t was not used" because we acknowlaged the it with the "catch all await".
I'm not really sure if this is helping enough to warrant adding over the implicit await points, but perhaps it might be worth considering to allow writing this anyway?
Not a strong feel about this one yet to be honest, but thought it'd be worth mentioning.
Otherwise about alternatives I agree with @Joe_Groff that:
- "don't wait" (3) is not an option
- the option (2) seems pretty confusing with where the await ends up appearing in various contexts...
Perhaps we've gone another full circle to end up at implicit awaits (1) then, but I'd love to be proven wrong here as I'd personally want the suspension points not be too hidden in my code... ![]()