owned this note
owned this note
Published
Linked with GitHub
# Reading notes: Async/await in Swift
###### tags: `reading-club`
https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md
Leave questions, observations, discussion topics below.
---
## Composition of effects
It stands out to me that one of the motivations is: "we added syntax to make error handling nice, but callbacks didn't allow us to reap any benefits from it". Composition of language features seems like a big problem for most languages, and they call it out explicitly here.
Also in terms of composition: it seems they've done the same thing we've done by separating "fallibility" and "asyncness". In JavaScript-style async/await these two are the same: "asyncness" always implies "fallibility" (`resolve` vs `reject` in JS). Swift seems to mirror a lot of the choices we've made - at least on the surface.
nikomatsakis: Interesting; I was having a conversation about this on moro. The danger if you ignore errors is that you can spawn a task that yields a `Result<(), E>` and never realize that it resulted in `Err`, and hence fail to take down your scope. Making the API more tailored to Result avoids that danger, but it bakes in "error".
yosh: Structured concurrency RFC should talk about this?
tmandry: Swift lets you coerce(?) an infallible function to fallible, so their structured concurrency doesn't have this problem.
```swift
func spawn(task: @Sendable () throws -> Void) {}
spawn({}) // infallible
spawn({ // fallible
if ... {
throw SomeError();
}
})
```
## JavaScript
The "pyramid of doom"-style callback nesting is identical to how Yosh used to write async code in JS (:
## await covers all subexpressions
nikomatsakis: Really interesting that `await` covers all async functions "inside" the await, so that e.g.
```swift
await session.dataTask(with: server.redirectURL(for: url)) // error: must be `try await`
```
works even if `server.redirectURL` is async.
## No futures
tmandry: Interesting that they eschewed the Future model completely. You always must have `await` to call an async function in Swift. A single `await` can also cover multiple calls in an expression. It marks suspension points but isn't *itself* a suspension point.
I think this means that all combinator-style interfaces happen on tasks.
guswynn: didnt really understand the motivation here? seem concerned about "futures being big", but the size is fixed based on the compiler-generated generator, regardless of if you are returning a type or building `async` into the type system?
tmandry: Part of it is consistency with `throws` and the `try` effect. Also Swift cares less about performance, more about reliability and predictability and ease of use.
## All futures are send?
> Because asynchronous functions must be able to abandon their thread, and synchronous functions don’t know how to abandon a thread (...)
This seems like perhaps unlike us, Swift doesn't distinguish between _concurrency_ and _parallelism_: all futures are multi-threaded, so all futures must be thread-safe. This doesn't seem to be the case for non-async Swift tho?
This may be the same point Tmandry makes in the previous section tho :P
tmandry: That's right, but they do some interesting stuff in the [actors RFC](https://github.com/apple/swift-evolution/blob/main/proposals/0304-structured-concurrency.md#actor-context-propagation) to propagate execution contexts. Since actors are serial queues this allows you to skip synchronization if you're already running on that queue.
tmandry: Also they can't split mut references to their enclosing context across multiple sub-tasks.
guswynn: question: does swift do anything to prevent overlapping mutable access?
Encourage people to write code using models like actors. Also copy-on-write collections.
## Async protocols and closures just work
The swift equivalent of async-fn-in-trait just works...likely because swift is willing to just box as aggressively as possible.
Similarly, async closures are part of this proposal. Again, boxing and what not avoids typical hrtb lifetime problems we see with "closures that return futures" we see in rust.
pnkfelix: recursive functions probably come out of this design as well, as guswynn pointed out to me in zoom chat
nikomatsakis: Would be good to compare and see what problems fall out.
..some discussion around HRTB problems and maybe we know how to fix them now?
Examples... wanting to write things like...
```rust
f: impl Fn(&mut Self) -> impl Future<Output = > + '_
// but this requires generic closures and richer HRTB
// plus the usual "not sufficiently generic" sort of problems that come up, likely from lack of HRTB implied bounds
// same problem we hit on GATs
```
tmandry: Also Swift Arcs everything if it's shared.
## Async operator overloading
As far as I can tell, is scoped to functions, not protocol methods. I (guswynn) am also curious to see how operator overloading plays out in practice. Will async versions of libraries using actors/the structured concurrency proposal be able to keep the same api boundary as sync versions?
niko: Interesting that they think you want `map` to be parallel/concurrent in an async context. Doesn't sound very robust. Example: Could parse something and get a sequence of commands.
Yosh: wrote a blog post on why concurrent iteration semantics are not the right default for Rust: https://blog.yoshuawuyts.com/async-iteration/
Felix: Map where order matters feels wrong. But would be better to opt in to parallel iteration.
Felix: Guy Steele talk about how iterators are wrong https://www.youtube.com/watch?v=ftcIcn8AmSY or https://www.youtube.com/watch?v=dPK6t7echuA
## `await` motivation is interesting
> Marking potential suspension points is particularly important because suspensions interrupt atomicity. For example, if an asynchronous function is running within a given context that is protected by a serial queue, reaching a suspension point means that other code can be interleaved on that same serial queue. A classic but somewhat hackneyed example where this atomicity matters is modeling a bank: if a deposit is credited to one account, but the operation suspends before processing a matched withdrawal, it creates a window where those funds can be double-spent.
Yosh thinks this is a really neat motivation that we might want to consider adapting for our conversations around "why should `.await` be annotated"?
nikomatsakis: It's interesting because `await <expr>` is intentionally "non-specific" about where the interruptions can occur.