async let is a very versatile construct. It is particularly well-suited to beginning work that you will only need later within the same function. But, it can also be very useful even if the results are needed in the very next step of computation. This pattern, of selectively shifting small amounts of work off the current actor, is a very common need in real projects.
let data = await retrieveDataAsynchronously()
async let result = decodeSynchronously(data)
// even if there's nothing else to do, this is still useful
return await result
This version works nicely when you have a nonisolated synchronous function that produces a result. But that's not always the case. An inline version of this can be constructed with an immediately-executed closure.
async let result = {
// inline work not expressed as a single function
}()
I think this is a reasonable solution, and looks very similar to what you might need for a lazy var initializer. However, the logic might rely on side effects alone. Or, it could just be that it's important the work be completed before the next step is executed. In either case, async let isn't particularly well-suited.
There is another option that can work, again using an immediately-executed closure:
await { @concurrent () async -> Void in
print("i am on the global executor")
}()
This is a very general solution. It provides a nice way to wait for work that may or may not return a value. It makes it easy to schedule the work in a series of sequential steps. But I find it pretty awkward to write.
As a solution, I'd like to pitch await blocks.
await {
print("i am on the global executor")
}
This is just syntactic sugar for a concurrent closure that is immediately executed. I think this could mostly support explicit function signatures just like a regular closure, if that's something you need. One exception to this is it would be invalid to accept arguments.
await { () async throws(MyError) -> MyResult in
// ...
}
An interesting consideration is global actor annotations. I think it is reasonable to accept this in all places where the function conversion is valid. But I'm definitely interested in hearing thoughts on this part, because it does feel slightly strange.
await { @MainActor in
// ...
}
The most obvious objection that occurs to me is that this could be confusing. I am perpetually sympathetic to these kinds of concerns. But I don't think that this is any more confusing than the existing semantics of async let. It also doesn't have any implicit interaction with scope. And, further, since this construct would be less-likely to be used as a one-liner, I think the odds of compiler diagnostics being easier to interpret is improved.
Along similar lines, I do not think that this would lead to any more accidental concurrency. The compiler already diagnoses invalid concurrency usage anyway, so even if you did, it would have to be safe on top of being sequential within the current task.
Encountering small amounts of code that need to be moved off of the current actor is extremely common. I think a more versatile, lightweight syntax for doing so would be appreciated. What do you think?