Contemplating Default Isolation Control

I think there are really two sides to this feature.

One is just about concision. You have a bunch of stuff, ranging from a few types to the entire module, and you want to avoid writing "@MainActor" on them. But, isolation is semantic information that is useful for other programmers too. To me, this is not quite the same thing as traditional boilerplate, though it can be close.

Because of this, I am a little less sympathetic to these use cases for isolation control. Yes, you can save typing. But, that comes with a loss of clarity and a possible increase in complexity should that isolation not quite be appropriate everywhere. These feel like very rough trade-offs. Per-file isolation control is also coming and I think it makes a mode-based reduction in typing even less compelling.

Then, there is the other case, where a programmer might encounter concurrency errors when there is no intended concurrency. This is really the case that was made for the feature when it was introduced. My experience is that this does not pan out that frequently, but can also be completely reversed. It can force a developer to think about concurrency-related problems that would not otherwise have come up.

I'm much more interested in the improvements to progressive disclosure and smoothing out the learning curve. But, I want to at least acknowledge both cases.

2 Likes

I'm very interested in this discussion, though I don't have much to add at this point beyond what has already been said.

One thought, admittedly from a UI developer's perspective: in most UI-related cases, MainActor isolation could be inferred automatically. For example, UIKit / SwiftUI views are implicitly MainActor-isolated, and the same could potentially apply to Observable classes. This would reduce – and in some cases eliminate – the need for those noisy explicit attributes.

1 Like

If a non-Sendable version of an Observable type can be created, the @MainActor annotation might actually be unneeded. That's potentially a big if, as non-Sendable types have limitations. But they have gotten considerably more ergonomic with NonisolatedNonsendingByDefault.

This is an important element to the discussion, because the language already has a more general-purpose means of making explicit global actor annotations unnecessary without modifying specific framework APIs. (Though that certainly can help a lot too.)

3 Likes

I think the problem with this is that as soon as they start interacting with pretty much any of Apple's APIs, they will need to write nonisolated. But the diagnostics often don't even guide them to that, either.

This is the main thing that is on my mind in these discussions. Many times I have realized the beauty of using non-sendable types for subsystems that are intended to be single-threaded. It allows the caller to choose where that execution occurs, and therefore has often seemed to me to be strictly superior in many cases. I haven’t thought deeply enough about this to know off the top of my head which situations this approach doesn’t work for, but surely there are some big ones, otherwise I assume the Swift team would not have built this rather unpalatable main-actor-by-default dialect. But at a glance I don’t know why new developers couldn’t have just defined their classes as usual and unwittingly instantiated them in their main actor contexts, and let RBI handle the progressive disclosure of concurrency concerns, and no one needs to default their whole module to the main actor.

5 Likes

Despite the massive improvements with 6.2, non-sendables still have some very poor ergonomics. The biggest ones are around capturing them in sending closures (like internal Task creation). It's often doable, but requires very careful and non-obvious use of isolated parameters. They also have some other, edge-case limitations, like incompatibility with lazy properties that themselves involve concurrency.

But, the other side of the coin is that non-sendables an insufficient solution. Global state is, in my mind, the big one. It's very common as part of learning to program and I believe is an important use case. Apart from an implicit MainActor, there aren't too many other solutions that could work, potentially none.

(I think it is possible that whole-program analysis was dismissed too early when used in the special-case of single-file programs. However, I have not investigated deeply enough here to know if that would be feasible and/or useful enough.)