`async let` and scoped suspension points

I'd like to note that we don't have much problem with withTaskGroup or functions with async trailing closures in general. They very much have the same conceptual baggage—there may be suspensions at the end of the closure (technically after the end of the closure). In fact, any async let ceremony for these closures would be a burden. Instead, they rely on the caller (and caller’s caller) to use await to signify that the entire structure may suspend.

So I think marking await at the beginning of the associated if/for/do/while/repeat/switch blocks is also an interesting direction:

await if ... {
  async let ...
} else {
  // Share `await` with `if` clause
  async let ...
}

await do {
  async let ...
} catch {
  // Share `await` with `do` clause
  async let ...
}

// `await` is inside, so that we can share it with `AsyncSequence` or condition
for await ... in ... {
  async let ...
}
while await ... {
  async let ...
}

await repeat {
  async let ...
} while ...
// or
repeat {
  async let ...
} while await ...

await switch ... {
default: async let ...
}

let foo = {
  async let ...
  // No need for `await` because the call-site already requires it
}
await foo()

This also plays into Swift's idea that the trailing closure is (intentionally) very similar to control flow structures. Here, the similarity remains:

// control flow
await if cond1 {
} else {
}

// trailing closures
await customIf(...) {
} else: {
}

I believe this would also scale in case of more complex control flow like nested if/else blocks, mixed if/else and while blocks, etc.

This structure can also be shared with other ideas that tie the lifetime of the task to the scope of the associated variable, e.g., non-movable types with async deinit,

struct Foo {
  async deinit { ... }
}
await if ... {
  var x = Foo()
}
// `x` is `deinit`ed, asynchronously.

Arguably, if all paths await all async let, we can omit the await at the scope level, so each can be an extension of one another

1 Like