Thank you for the great discussion. I think I'm willing to call this a real "pitch" now, and have updated the proposal document with more motivation / introduction / future directions / alternatives considered based on the discussion here. I haven't covered everything we discussed yet, but hope to get there shortly.
In parallel, I have a partial implementation that I hope to complete shortly, so we can experiment with it. I'll update this post once I get the last missing known piece in place.
Why does isolation of the type matter? It seems to me that global actor isolated conformances should be possible for any types. Meaningful implementation of the protocol requirements might not be possible, but thatâs a separate question, which IMO is perfectly fine to leave to developersâ discretion.
Very in favour. Looking forward to adopting it in Swift Testing, especially in the bits of our code that directly touch metadata (and so can't statically guarantee sendability.)
I think the spelling here is going to be extremely confusing for the average engineer, because you use isolated XYZ to conform to a nonisolated protocol XYZ.
Seeing this now in the pitch as a future direction:
Infer isolated on conformances for types that infer @MainActor
If Swift gains a setting to infer @MainActor on various declarations within a module, we should consider inferring isolated on conformances for types that have had their actor isolation inferred. This should make single-threaded code easier to write, because protocol conformances will "just work" so long as the conformances themselves aren't referenced outside of the main actor.
Made me wonder whether having nonisolated conformances by default on global actor isolated types is the right default. Particularly when combined with the other future direction of allowing S: @MainActor P for isolated conformances of nonisolated types.
If infering isolated conformances for types that infer @MainActor is on the table, perhaps it would be simpler to always infer isolated for global actor isolated types, and allow nonisolated to opt-out instead?
It would be more consistent to how actor inference works for properties and methods, which infer isolation by default.
No need to use a new keyword (isolated), reuses the same syntax to make isolation explicit as that of properties and methods (nonisolated, @MainActor...).
Avoids having a different protocol isolation inference behavior for inferred @MainActor types (isolated conformances by default) vs explicit @MainActor-isolated types (nonisolated conformances by default).
It would make global actor isolated types with nonisolated conformances a bit more cumbersome to write (@MainActor S: nonisolated P vs @MainActor S: P), but from progressive disclosure point of view, the simplest use case (main actor isolated type with main actor isolated conformance) would also have the simplest syntax (@MainActor S: P). The use case of a global actor isolated type with a nonisolated conformance is a bit more "advanced" (as it involves different isolations within the same type) and would be slightly harder to write (@MainActor S: nonisolated P).
I actually like the verbosity of the @MainActor S: nonisolated P. I find useful to highlight that despite entire type being isolated to a global actor, there are operations usable from other domains.
You're absolutely right, the isolation of the type doesn't matter if we're modeling isolated conformances within the generics system.
Do you have a favorite alternative? The only one I can think of is to name the global actor specifically, e.g.,
@MainActor
class MyClass: @MainActor P { }
Since the default today is "nonisolated", we'd have to do this as part of an upcoming feature or language mode. That's not out of the question, but it's part of the reason why I am suggesting that this inference come along with "main actor inference" (should we get that). The other reason is because inferring "isolated" when you didn't expect it can make your conformance unnecessarily restricted, whereas inferring "nonisolated" will produce an error if one of your witnesses is isolated and is easy to fix.
We could replace isolated in the proposal with @MainActor independent of the defaulting decision. I picked isolated to go along with isolated parameters, but I don't feel strongly about it.
Again, this would require an independent upcoming feature. It's also a question of where on the progressive-disclosure path this decision should come up: main-actor-by-default could certainly imply that conformances are also main-actor-by-default, independently of whether you wrote @MainActor on the conforming type explicitly or had it inferred. You can make specific conformances nonisolated as needed. When transitioning out of the main-actor-by-default, the migration would put isolated (or @MainActor) on any conformances that aren't nonisolated to make the isolation explicit, just like it would for any types that were implicitly @MainActor.
The alternative considering type without isolation made me think about multiple isolated conformances. Maybe this doesnât make much sens or would be a rarely used feature but using the named approach would open the door.
class Obj: @MainActor Equatable, @LibActor LibProtocol {}
Yes, it definitely falls out of that feature. Y'all have convinced me to drop isolated P and go with @MainActor P. It's more general and clearer. I've updated the document accordingly. Thank you!
No, that's covered in the Future Directions under "Actor-instance isolated conformances". I don't actually know how to type-check that in any reasonable manner.
isolated P is not too bad, it can be supported in addition to @MainActor P. I see a benefit of not repeating actor name twice.
But I'm quite excited about @MainActor P, it opens a lot of new opportunities (and challenges)!
Can we use @MainActor P in generic requirements?
nonisolated func foo<T: @MainActor P & Sendable>(_ x: T) async {
...
await x.bar() // hop to main actor
...
}
Can we form existentials for @MainActor P? Is it @MainActor (any P) or any (@MainActor P)? Is this existential Sendable? IMO, it should be, because it does not depend on any regions.
Why not go further and allow arbitrary non-sendable types to be isolated to a global actor?
func foo(_ block: @escaping @Sendable () async -> Void) {}
func bar() {
let a = NS()
let b: @MainActor NS = a // implicit cast, connecting region to main actor
foo {
a.blub() // error: Capture of 'a' with non-sendable type 'NS' in a `@Sendable` closure
await b.blub() // ok, (@MainActor NS) is Sendable
}
}
I don't like repeating the actor name, but I don't dislike it so much that I want to propose both syntaxes. The proposal-as-written now has only the @<global actor name> syntax.
To me, this goes along with the notion of isolated requirements that are already covered in future directions. I can see how it adds expressivity; I'm not convinced that the additional complexity is worth it for that expressivity.
As an update-to-the-update, the implementation is now in sync with the proposal. I have toolchains for macOS and Linux (Windows coming soon), if anyone wants to kick the tires:
There are three experimental features at play here for this proposal:
IsolatedConformances: allows one to define and use isolated conformances, e.g., class MyClass: @MainActor P { }, the main part of the proposal.
StrictSendableMetatypes: implements the restriction on sending metatypes in generic code. This enables just the source-breaking part of the proposal. Without it, isolated conformances are not data-race-safe, but you can try it out independently.
UnspecifiedMeansMainActorIsolated: this experimental feature is the subject of the ongoing review of SE-0466. When enabled, it infers @MainActor for code that isn't explicit about its isolation. When isolated conformances are also enabled, we infer @MainActor for any conformances in the module of @MainActor types unless they are explicitly marked nonisolated.
Are we worried about ourselves into a place that rhymes with the frozen/non-frozen enum issue, where the same text (in this case, a naked T: P conformance) means different things depending on a compiler flag/language flavor?
That is Almost Surely going to fail to compile, because SomeProtocol's requirement will likely be non-isolated (if it came from any other module), but we've implicitly made satisfiesRequirementOfSomeProtocol main-actor-isolated. In a sense, this means that enabling default actor inference via SE-0466 will break some working code. The user has a few ways out, such as @preconcurrency or the nonisolated/assumeIsolation dance, but those are a lot farther along the progressive-disclosure curve.
With isolated conformances and this inference rule, we end up with the inferred result being
... which will work so long as you don't try to use the conformance from non-main-actor code. That's okay, because all of your code in this module defaults to the main actor.
I acknowledge your stated concern---that the same code means two different things based on whether we're inferring the main actor by default---but I think that's the subject of SE-0466, and the isolated conformances proposal here just follows that decision and improves the result.
That's a lot of "@MainActor". Has any consideration been given to inferring an isolated conformance by default on types that are explicitly isolated to a global actor, the same way we infer it on properties and functions within that type? And then we could call out MyClass: nonisolated SomeProtocol in the specific cases where you don't want an isolated conformance?
I admit that I'm not familiar enough with the design space to know if that would even make senseâon a @MainActor type, would we expect most conformances to be isolated? Or are the use cases for a nonisolated conformance broad enough that it would be too noisy in the common case?
There's some discussion of this at the bottom of my reply here. In short, we'd need to stage in this change via an upcoming feature, because all of the conformances of isolated types today are non-isolated conformances.
I have no data to back this up, but my hunch is that most conformances for main-actor types are either to main-actor-isolated protocols (so they are untouched by this proposal) or would want to be main-actor-isolated and are either ignoring that fact (strict concurrency is not enabled) or papering over it with @preconcurrency/assumeIsolated.
We might be able to consider inferring that a conformance is global actor isolated only in the cases where an isolation mismatch on the protocol witness is an error. I think we would only be able to do that under the following conditions:
Under the Swift 6 language mode; otherwise the isolation mismatch is suppressed or downgraded to a warning.
All witnesses with mismatching isolation are isolated to the same global actor.
Inference rules like this have downsides, e.g. it can be difficult to determine when they've applied and why, it's easy to inadvertently change inference, etc, so the convenience might not be worth it. There may also be technical tradeoffs to making conformance isolation dependent on witness matching.
Yeah, I think if the isolation of the conformance can only be deduced by looking at all witnesses, then we would need to do more work than is otherwise necessary when the conformance is declared in a secondary file.