From a user's perspective I have a bit of concern about the fix-it. If I'm calling a function:
@MainActor
func usesMainActor(...) { ... }
func nonisolatedFunc() async {
await usesMainActor()
}
and I get an error because usesMainActor
has an isolated default expression somewhere in the signature, after applying the fix-it I have:
func nonisolatedFunc() async {
await MainActor.run { usesMainActor() }
}
But to a subsequent reader of this code, I don't think the end state is particularly understandable... like, it's clear we're trying to move to the main actor, but part of the promise of Swift concurrency is that it's the callee which decides where it executes, and callers shouldn't need to care—they just have to suspend. IOW, I think your analysis here is totally right:
At the very least I think it may be worth privileging the above case. To me, using MainActor.run
very much looks like an escape hatch used by someone who was trying to brute-force migrate old DispatchQueue.main.async
code. If we had something to signify "move the evaluation of this entire expression to a global actor" e.g. await(MainActor)
I think it would be wise to recommend that use here.
Again, though, I worry this slides back towards "callers need tell their callees where to run," counter to Swift concurrency's current philosophy.
This feels like an pretty unfortunate downside—the cost is not only additional code to write out, but we lose the behavior that default argument expressions may be updated transparently to the caller. Authors who wanted to maintain that behavior would essentially need to reinvent the current resilience regime where they define their own function for emitting each default argument they care about.
What sort of interactivity are you looking for here? In terms of debugging it seems like we should perfectly well be able to 'step to' the default argument expression in the interface to show when it's being evaluated. And if the concern is that there are potentially many suspension points at that line of source being hidden by an await
, is that really meaningfully different than the fact that calling into an arbitrary async
function may admit arbitrarily many suspension points internally?
I guess what I'm getting at is that I can forsee these isolation rules pushing authors of APIs like:
@MainActor class C { ... }
@MainActor func f(c: C = C()) { ... }
towards something like:
@MainActor func f(c: C? = nil) {
let c = c ?? C()
...
}
just to spare their users of the additional annoyance of using the default argument expression directly. Do we really think this is better?