Boolean operators and `async let`

Why can async let variables not be used on the RHS of boolean operators?

async let first: Int[] = someAsyncCall()
async let second: Int[] = anotherAsyncCall()

let bothEmpty = (await first).isEmpty && (await second).isEmpty
// or
let bothEmpty = await (first.isEmpty && second.isEmpty)
// or any other variation I can think of

I always get the errors

'async let' in an autoclosure that does not support concurrency

Capturing 'async let' variables is not supported

I know that && and || use @autoclosure to implement short-circuit evaluation, but the error is rather opaque; I couldn't find good information on either one on Google.

What's a good way to work around this? Do I have to use an explicit task group?

As you noted, Boolean operators are defined with the rhs parameter marked as an autoclosure, which means that the expression on the right-hand side is automatically bundled up into a closure, delaying its execution until necessary. Notably, this rhs closure property isn't marked as async ,so no await statements can appear "inside" it.

As for a workaround — you should be able to put both into an array and test them at the same time:

let bothEmpty = await [first, second].allSatisfy(\.isEmpty)

Given that, I expected this to work:

func &&(
    lhs: Bool,
    rhs: @autoclosure () async throws -> Bool
) async rethrows -> Bool {
    if lhs {
        return try await rhs
    } else {
        return false

While that does get rid of one of the errors, it still complains about capturing the async let variable. In fact, I couldn't find any overload of && that would definitely work. Making the second argument of type Bool would shadow the standard library operator. This:

func &&(lhs: Bool, rhs: Bool) -> Bool {
    return Swift.&&(lhs: lhs, rhs: rhs)

and similar complain about various things (apparently you can't qualify operators). The only way I could find is to wrap it in a local thunk:

let and: (Bool, Bool) -> _ = { $0 && $1 }
let bothEmpty = await and(first.isEmpty, second.isEmpty)

Here's a somewhat more readable solution:

async let first: Int[] = someAsyncCall()
async let second: Int[] = anotherAsyncCall()
let (firstIsEmpty, secondIsEmpty) = await (first.isEmpty, second.isEmpty)
let bothEmpty = firstIsEmpty && secondIsEmpty