I'd like to suggest changing inferred isolation of actor properties accessbile to non-isolated functions from actor-isolated (the current behavior) to non-isolated. The change wll cause almost no behavior change, but may make it more natural for user to reason about whether an actor property can be accessed by non-isolated functions.
The Problem
Let's see this example, which works by design:
actor MyActor {
let value: Int = 1
nonisolated func foo() {
print(value) // This is OK
}
}
There are three rules involved to understand why it works:
- General rule: Actor's properties are actor-isolated by default (unless their isolation are declared explicitly)
- General rule: it's OK for nonisolated function to access nonisolated values
- Excpetion rule: it's OK for nonisolated to access immutable actor property of value type
The exception rule isn't difficult to understand in above case, until we have more such rules. Let's see another example:
class C: @unchecked Sendable {
var value: Int = 1 // let's assume the value is protected by mutex
}
actor MyActor {
let c = C()
nonisolated func foo() {
print(c.value) // This is OK
}
}
We need a new exception rule to understand why it works:
- Exception rule 2: it's OK for nonisolated function to access mutable actor property of reference type which is Sendable
Not sure about others, but I find it's difficult to document and remember these rules. The approach I personally take is to not do it. Instead, when I'm not sure, I first think if it's possible to cause data race and then do experiments to verify it. That's not an efficient approach though.
The Proposal
So I'd like to propose the following changes:
-
Actor property isolation inference: if an actor property doesn't have an explicit isolation declaration, it's inferred as either actor-isolated (the default) or non-isolated if a) it's immutable and of value type, or b) it's mutable and of a Sendable reference type. The rule applies to properties of global actor too.
-
Accessibility by non-isolation function: non-isolation function can only access non-isolated values. No exceptions.
One may argue that the change doesn't get rid of the complexity but rather moves it from one place to another place. That's true. The complexity is still there, but by moving it to a proper place it becomes more natural. IMO the current rules are unnatural because they don't fit isolation's semantics. What do we mean when we say a value is actor-isolated? I think it means the value should be accessed from the actor's executor only. Unfortunately that's not true in the above examples because of those exception rules. Or think like this, since those values can be accessed from any domain, shouldn't they be non-isolated in the first place? That's the foremost reason why I propose the change.
Source compatibility
For actor properties with inferred isolation the new rules won't change their behavior, because only those that used to be accessible by non-isolated functions will be inferred as non-isolated.
For actor properties with explicit isolation declaration the new rules may change their behavior. For example, the following code compiles currently but will fail under the new rule:
actor MyActor {
@MainActor let value: Int = 1
nonisolated func foo() {
print(value) // This doesn't compile under the new rule
}
}
I think it's actually good to break code like the above because the use of @MainActor
in it is meaningless. It does nothing other than confuse users.