[Pitch #3] Async Let

I don't understand what you mean by this last sentence.

Yes. An actor-isolated function will run its synchronous portions in the actor's execution context. Now, we retain the right to optimize away the "hop" over to the actor's execution context if we can prove that the actor is unused within the synchronous portion, i.e., there's no references to "self".

This is fine. It's a concurrent task, but it'll end up serializing on the same actor until (e.g.) it ends up suspending while waiting for data to come across the network. This is a good description:

Yes, that's right.

Yes, you can call asynchronous actor functions from inside the actor. You'll still need to await them, of course, because they could suspend in their execution.

We can look to clarify the wording. The initializer of an async let is not isolated to the actor, but like any code outside the actor, it can interact asynchronously with the actor---it just needs to await its turn.

One could imagine having async parameters of some sort, and allowing async let to be passed down to those:

func f(x: async Int) async { ... }

func g() async {
  async let y = doSomething()
  f(x: y)
}

The restrictions would have to be similar to inout, where we don't allow captures in non-escaping closures. It would be a lot of implementation work to make this happen, but I think it does fit.

Doug

1 Like

I'm uncertain at this point if this is the answer to my actual question. Here's the example again, it its async let variant:

actor MyActor {
	func dataFromNetwork() async -> Data { … }
	func doStuff() async {
		async let data = await dataFromNetwork()
		…
	}
}

The proposal says the initializer of the async let is equivalent to a closure that's "@Sendable and nonisolated". I read that as meaning this closure:

		{ return await self.dataFromNetwork() }

But surely self is invalid in a nonisolated closure?

Why would self be invalid in a nonisolated closure? self is captured, which is fine. Actor types are Sendable, because they handle their own synchronization.

  • Doug
2 Likes

Thanks this clarification makes a lot of sense. I think I was getting confused with so many names and different descriptions being thrown around.

I also think the names make a lot of sense when you understand structured concurrency. Which is a key part of the conversation. We need to somewhat forget about what we are used to with other systems like promises because this is not that.

Quite happy with the pitch. +1!

1 Like

I like the proposal and the async let spelling.

The proposal has this code:


func makeDinner() async throws -> Meal {
  async let veggies = chopVegetables()
  async let meat = marinateMeat()
  async let oven = preheatOven(temperature: 350)

  let dish = Dish(ingredients: await [try veggies, meat])
  //                                  ^^^
  return try await oven.cook(dish, duration: .hours(3))
}

and I would like to ask a question about the location of that try that I marked.

For me this location seems to be not intuitive. I now have to remember at this location that veggies is produced by a throwing function.

To me the following code would feel much more natural:


func makeDinner() async throws -> Meal {
  async let veggies = try chopVegetables()
  //                  ^^^
  async let meat = marinateMeat()
  async let oven = preheatOven(temperature: 350)

  let dish = Dish(ingredients: await [veggies, meat])
  return try await oven.cook(dish, duration: .hours(3))
}

What is the reaon that the try is not matched to the throw and we have to remember this at the time of usage.

I am assuming that the try on the last line is because the method cook is throwing.

1 Like

One additional question about the await on the Dish line. This is just for clarification.
suppose that there is an additional ingredient that is not async.

let additionalIngredient = getFromStock()

If I then create the Dish what would the syntax be:

let dish = Dish(ingredients: await [veggies, meat, additionalIngredient])

or

let dish = Dish(ingredients: [await veggies, await meat, additionalIngredient])

or could I even do

let dish = await Dish(ingredients: [veggies, await meat, additionalIngredient])

Any of those syntaxes is fine:

async let one = ... 
async let two = ... 
let not = ...

Dish([await one, await two, not])
Dish(await [one, two, not])
await Dish([one, two, not])

Generally the rule is currently the same as with try wrt if an expression is "covered" with an await.

2 Likes

Thanks

1 Like

Then maybe write it like this:

func makeDinner() async throws -> Meal {
  let (veggies, met, oven, _) = try parallel {
    try run { chopVegetables() }
    run { marinateMeat() }
    run { preheatOven(temperature: 350) }
  }

  let dish = Dish(ingredients: [veggies, meat])
  return try await oven.cook(dish, duration: .hours(3))
}
1 Like

The async let and closures code sample in the proposal is failing for me with Capturing 'async let' variables is not supported which I thought was strange given the * Status: Implemented (Swift 5.5)