`async let` and scoped suspension points

TLDR: I think implicit suspension points are fine if our tools surface them and provide information about where they came from.

Over time, I've started to think about try and await more as granting permission to called functions rather than labeling source code, and I think this distinction is valuable when considering the async let problem. The main benefit of these markers, as I see it, has to do with maintenance: the absence of these markers means that changes in the called function won't change the behavior of the calling function without the compiler telling you about it. For me, granting permission is great, but labelling things I already understand is what feels onerous. I'd wager that even as a learning exercise "write this word here to show you understand" is not a positive experience for the user.
The novel part about async let is that the callee has the ability to potentially change behavior away from the call-site. Forcing users to label the places where this change could happen feels onerous to me, especially since they've already granted the callee permission to change the caller's behavior with async let. From this perspective, the fact that the suspension point is implicit is good... the bigger problem is that it is hidden and thus may be surprising.
This is not the only potentially surprising thing that may happen in Swift. For instance, it may be surprising when calling count on a collection is not O(1). Luckily, Swift already solves this with tooling: API docs mention this and they can be surfaced during autocomplete or in the help pane, and API that does this should call out this behavior. Without the tooling, this would not be a great situation (i.e. if folks had to go find the source file where count was declared and check that it was O(1)).
Perhaps then, tooling is the right solution to our async let problem. If Xcode (and sourcekit-lsp) highlighted potential implicit suspension points (maybe with an affordance which on hover pointed to the async lets which cause them), this would not be an issue.

Corrolary: In "On the proliferation of try..." affordances like try do and await do were discussed as an improvement for try-or-await-heavy code. The granting permission mental model allows for this since you are granting permission to a scope to throw, suspend, or both. Without tooling, this would come at the cost of decreased visibility about where behavior might change based on a callees behavior. With tooling, we would get the best of both worlds: the ability to grant permissions at a less granular level and visual indications about which API might cause changes in behavior (and where). In fact, such a thing might already be useful for more complex expressions (for instance a function call where some of the arguments might throw).

4 Likes