Making some lets be nonisolated by default
I remain in the camp that let properties should be isolated by default in an actor. Many of the arguments have already been made, but I'll try to pull them together into one argument.
The proposed "make some let properties nonisolated by default" approach is syntactic sugar that eliminates the need for one keyword on some properties in actors, but it comes with several real costs to complexity and the future evolution of Swift:
-
API Evolution and API Resilience (as well as binary resilience) are key parts of Swift's design - this is DNA inherited from Cocoa, which showed an amazing feat of API evolution from the NeXT days. The proposed design breaks a key part this. Today you can upgrade any
letproperty to a computed get-only property, mutable property or property with a private setter without breaking source or ABI, and this breaks that property. The replacement it offers (switch to anonisolatedvar) muddles the story for API evolution, making the Swift API evolution story more complicated. -
The proposed model doesn't allow all
let's to be used synchronously, it only applies to@Sendableones. The first big problem with this is for API evolution reasons: a type can become "newly sendable" as its API evolves. This would make any properties of that type implicitlynonisolated(a non-local effect!), causing new warnings/errors about unnecessaryawaits in client code. I haven't seen this discussed before. -
The proposed model doesn't allow all
let's to be used synchronously, it only allows@Sendableones - and many mutable reference types and closures will never adopt. This muddles the explanation ofletproperties in actors and will make them difficult to teach: people will probably only teach the special case. This will be experienced by people saying "why do I have to await this let property, I thought they were immutable" because only the simple part of the model got explained to them. In my preferred model you'd get a simple "non-sendables types cannot be nonisolated" error when you tried to mark theletas nonisolated. -
It would eliminate the ability to model "I have a let property that I want to be accessed through the mailbox". This is the only model supportable by distributed actors, but there may be other reasons we want this for non-distributed actors in the future (e.g. an API only wants to be usable from an async context for some reason). Eliminating this makes Swift less consistent and more difficult to teach. Further, when distributed actors are introduced, we will need to teach both the inherent differences between distributed and nondistributed ones (error handling etc), as well as this elective difference that isn't inherent. This will muddle the distributed actor story.
-
The proposed model is inconsistent with the rest of
nonisolated: why do you need the keyword on funcs, subscripts etc but notlets? All the logic that works with these properties (including computed properties derived from it) will need to be markednonisolated. This makesnonisolatedmore difficult to teach because it now has a special case, undermining it and making its conceptual model more complicated. -
This whole topic is directly related to global variables (which can be both let and vars), but we don't have a confirmed model for how they will handle isolation. It seems wise to nail that down before even considering this as they are more primal to the language than a bit of syntactic sugar.
-
The argument that "
letandvarare already inconsistent in closure capture lists" is true, but there is a huge difference with this proposal: this feature is about APIs, not about the internals of function bodies. APIs are subject to resilience, are impossible to change once established, and are the interfaces between code compiled with different swift versions. The rules for capture lists are easy to evolve and relax, and the discussion about these capture rules in theSendableproposal indicate that the model we're going with today may change in the future (through the use of dataflow analysis) because they are about by-copy captures, they aren't aboutletproperties. If and when this model evolves, the difference may go away. -
It isn't clear this is going to be a widely /valuable/ bit of syntactic sugar. While it is completely believable that actors will have immutable state, this proposal only removes a keyword in the case when those are
publicor otherwise intended to be used in a cross actor way (otherwise the absence or presence of the keyword doesn't matter). We as a community have almost no experience using these concurrency features, and it sounds like most of the usage experience folks at Apple have is migrating pre-actor code to actors. While the design of actors is obviously derived from common patterns in existing Swift code, I don't think the decisions about the need for syntactic sugar is easily derivable from that experience. We should base these decisions based on the experience people have /intentionally writing code for Swift actors/ since that is the long-term thing to optimize for.
Zooming out: it is important to understand that this is fundamentally a "syntactic sugar" feature, and that it is a one-way door. If we start actors with this sugar then we are stuck with it and can never remove it. However, if we start without it, we can gain experience, let the overall Swift Concurrency design gel a bit, then weigh the complexity / convenience tradeoff with a /lot more experiential data/ and make a call later.
One thing I find a bit concerning is that folks are arguing that taking the feature would be aligned with the idea of progressive disclosure of complexity: I disagree about this. Progressive disclosure in Swift isn't generally about sweeping the hard cases under the rug with an inconsistent model (that is what Perl 5 does, infamously) - it is about keeping the concepts simple and orthogonal, but prevent you from having to learn them until you need them.
In this case, there is no reason for a Swift programmer first learning actors to dive into any of this stuff. Then can start out by learning about the idea of a mailbox and isolation. This is a simple and consistent feature of actors. When they get to the point where they need nonisolation (e.g. when conforming to a legacy protocol) they can learn about nonisolation as a simple and consistent unit of technology that is there to solve a problem.
Conclusion: Making some let's be nonisolated by default simply waters down the actor model, the nonisolation model, and paints us into a corner that we may well regret in the future. In my opinion, doing this proactively at this early stage of the Swift Concurrency model design is unwarranted and extremely unwise.
-Chris