Access values of async variables as they become available

I pitch this hoping to provoke a lively discussion to improve the ergonomics of the Swift concurrency.

// Given
func f () async -> Int {...}
func g () async -> Int {...}
func h () async -> Int {...}

// Declare async variables u, v, and w
async let u = f()
async let v = g()
async let w = h()

// Suspend until they all become available
let uvw = await [u, v, w]

// Use them
...

It would be really good to access the value of each variable as they become available.

For example:

await {
    case let x = u
        // use x
    case let y = v
        // use y
    case let z = w
        // use x
    case timeout 30
        // cancel took to long
        break
}
// timeout or all finished

or

await {
    case let u = f()
        // use u
    case let v = g()
        // use v
    case let w = h()
        // use h
    case timeout 30
        // cancel, took to long
        break
}
// timeout or all finished

What do you think?

Just for comparison I wanted to check how to do it today

your proposed method:

async let u = f()
async let v = g()
async let w = h()
await {
    case let x = u
        // use x
    case let y = v
        // use y
    case let z = w
        // use x
}
let uvw = [u, v, w]

without introducing anything new:

async let u = {
    let x = await f()
    // use x
    return x
}()
async let v = {
    let y = await g()
    // use y
    return y
}()
async let w = {
    let z = await h()
    // use z
    return z
}()
let uvw = await [u, v, w]
2 Likes

Yes and as a side note, make it look cooler with this idea from Arbitrary Block Evaluation - #15 by crontab

async let u = get {
    let x = await f()
    // use x
    return x
}

Sorry, but the last bit

let uvw = await [u, v, w]

wasn't in the proposed method.

To access the value of u later, there still needs to be an await, no?

If there are multiple async lets such as u, what's the benefit?

What I had in mind with this:

await {
    case let x = u
        // use x
    case let y = v
        // use y
    case let z = w
        // use x
    case timeout 30
        // cancel took to long
        break
}
// timeout or all finished

is to express some kind of parallelism.

Yes, but back to your proposal, I'm struggling to find some common real-world scenario where accessing async let variables as they arrive would justify a specialized syntax. Or that it couldn't be solved somehow else, i.e. moving that code to the async functions themselves, or doing those things after all of them arrive, or after all doing as shown in @cukr 's solution.

Do you have a real-world scenario in mind?

1 Like

I assumed you wanted to create uvw array, in which case let uvw = [u, v, w] (no await) is required

If you don't need to create uvw, then the "add nothing to the language" solution becomes even simpler

Task {
  let x = await f()
  // use x
}
Task {
  let y = await g()
  // use y
}
Task {
  let z = await h()
  // use z
}
1 Like

I assume initial idea is to get something like Go’s select, isn’t it? I wonder how it fit in Swift model for concurrency.

Upd. Maybe paired with AsyncSequence akin to channels?

Yes. (I did not want to use the word select(from go) or alt(from Limbo)

I opine that the lack of a construct akin to the switch-statement makes it harder to write code that expresses one's intentions clearly and concisely when dealing with concurrency.

Can you imagine a Swift world today, without the switch statement? :slight_smile:

1 Like

I don't have a lot of experience with Go, and with select statement particularly. Yet I feel like all of that exist in the language because they have different model, particularly for channels. What I mean, is that, for example, basic (1) showcases (2) from Go learning resources easily expressible in Swift currently (and probably in even more expressible way, but that's subjective). You can model first example just using withDiscardingTaskGroup, and second with AsyncStreams (maybe in less elegant way as Swift has no two-direction channels similar to Go), but you still able to await through the loop. Not a lot more code, compared to the select version.


And using familiar task groups IMO better communicates that you expect to wait for all to complete (from switch you expect to handle one branch only, and select behaves the same way, so it would be more correct to wrap this in a loop maybe) and they are executed in parallel, rather than this:

vs

try await withDiscardingTaskGroup { group in
    group.addTask { 
        let x = await action1()
        // use x
    }
    group.addTask { 
        let y = await action2()
        // use y
    }
    // is that correct for group cancellation?
    try await Task.sleep(for: .seconds(30))
    group.cancelAll()
}

Less compact — maybe. But not that much I believe.

1 Like

Thank you, but how to express the timeout coherently?

1 Like

Just to point out, Go's select mimics the POSIX select call (now "decommissioned" due to the c10k problem and replaced with something else, but not the point).

select's most common pattern is listening on a group of sockets/channels in a loop and reacting whenever there's an event on one of them, typically a connection request on a socket to which you create a client socket and begin communicating with your new network client.

The objects you want to listen to would more likely be async sequences or channels/queues of some kind rather than one-off async calls.

So would an equivalent of it be useful in Swift? Even though I presume you can find this pattern somewhere under the hood in SwiftNIO, it doesn't seem to be a very common pattern otherwise.

Plus, listening on a group of asynchronous channels is a tricky business in general due to the prioritization problem. I think you can always replace it with waiting on a single queue to avoid dealing with prioritization.

So what would await ... case be useful for in Swift? I'm not sure to be honest but open to see some real-world counter-examples.

2 Likes

For comparison, I have adapted @cukr's example:

Here is how it would look like:

await {
  case let x = f()
  // use x

  case let y = g()
  // use y

  case let z = h()
  // use z

  case timeout (3)
  // taking too long
}

Note that there is no use of Task statement anywhere. I maintain that this is a big plus already, because Task is revealing the implementation details.

Is this not much simpler?

Okay, one problem with it is purely syntactic, the compiler will interpret await { as a beginning of an async code block, i.e.:

await {
    // some async code
}()

I tried it and indeed it's what the compiler is expecting.

Second problem is let's say cognition-related. When I look at your await { case let ... example my first instinct is that the case labels denote alternatives, i.e. only one of them will be executed, although of course once you look closer you realize that can't be the case. This is because we are all used to the switch syntax which selects only one of the paths.

Thirdly, aren't you trying to reinvent the "zipper" pattern? I have a family of functions in my library that kind of do what you are trying to achieve.

With e.g. two-parameter zip() call you can have:

let (x, y) = await zip {
	let x = await f()
	// do something with x
	return x
} _: {
	let y = await g()
	// do something with y
	return y
}

Eventually you get x and y but you can also do something with them as they become available. Not as elegant as a built-in syntax would be, but does the job.

(These functions are experimental, I still need to figure out if I need to use rethrows and how, but overall they do the job.)

And finally, I'm still struggling with finding a real world example that would require to handle the async results as they arrive but also wait for all of them to finish at the same time. The usage of my zip function family is much simpler most of the time, i.e.:

let (x, y) = await zip(f, g)

where f and g are async functions.

1 Like