What is your evaluation of the proposal?
Overall +1, though I want to echo my suggestion from the pitch thread to rename the API to startImmediately
.
Rationale: "synchronous" carries heavy baggage and the name could lead to the assumption that the entire task will be run in a blocking manner. I find that "immediate" is more accurate (that word is used 6 times in the proposal document!) and doesn't carry the same baggage.
Is the problem being addressed significant enough to warrant a change to Swift?
Yes, the ability to start a Task without a hop is a primitive that isn't possible without explicit language support. It's often necessary to do this, for example when interacting with APIs bridged from Objective-C (cf Harlan's NSItemProvider example) or C. Or more generally, any API that doesn't assume Swift's concurrency model, and has both an "immediate" and "later" part. Expecting client Swift code to vend separate synchronous and asynchronous parts to solve for this is awkward in the Swift world, and being able to start tasks immediately is a very nice solution.
This can also be useful in pure Swift. Consider the following example:
struct MyView: View {
@State private var name: String?
var body: some View {
if let name {
Text(name)
} else {
ProgressView()
}
Color.clear.onAppear {
Task.startImmediately {
// API can return synchronously if cached
self.name = await API.fetchName()
}
}
}
}
If one uses .task
or .onAppear { Task { ... } }
, the view will, on occasion, briefly flicker with a visible ProgressView
even if API.fetchName
has a cached result that it can return synchronously. Meanwhile, startImmediately
will deterministically eliminate this flicker if the result is cached. (Aside: it would be great if View.task
also got a companion View.immediateTask
API that started immediately, though I know this is outside the purview of Swift Evolution.)
The above API can be spelled in a way that makes eliminating the flicker possible today, but it's harder to read, increases the API surface, and adds more logic to the view layer.
struct MyView: View {
@State private var name: String?
var body: some View {
if let name {
Text(name)
} else {
ProgressView()
}
Color.clear.onAppear {
// View needs to explicitly check for cache
if let name = API.cachedName {
self.name = name
} else {
Task {
self.name = await API.fetchName()
}
}
}
}
}
However, if API.fetchName
were a third-party API, there's no guarantee they would provide direct access to the cached property. Frequently, a bridged Objective-C API could say something like "If the result is cached, the completion handler will be invoked synchronously." in which case startImmediately
would be the only way to avoid that flicker.
I also see this being very useful in tests, where one might want to assert that a certain portion of an asynchronous method is invoked immediately.
Does this proposal fit well with the feel and direction of Swift?
I think so, especially as we also move to reduce executor hops with SE-0461. I wish we could have gone a step further and made this (Task.startImmediately
) the default behavior, in line with the observation from SE-0461 that most code probably doesn't need to executor-switch or hop at all. I assume it's a bit late for that now though.
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
I really like the mental model that Combine (RIP) had, where execution order was highly deterministic and any hops required an explicit receive(on:)
or similar. It's fair to say that such determinism isn't always necessary, and to that end I don't think it's the worst thing that Tasks start asynchronously by default, but it would be very nice to be able to regain that control.
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
I aimed to read the proposal in depth. I've also experimented with the SPI version of this primitive (Task.startOnMainActor
) and found it very useful.