I am very fond of and supportive of this feature and technology direction (you could say +100!). However, I do have some concerns about the details involved and am not as supportive of this particular draft. Detailed comments below.
Yes, this is a very important part of the Swift concurrency model. I think this will align very nicely with the long term direction of Swift and take the community and its APIs to the next level.
I have been studying concurrency for a very long time, and am familiar with the actor models in both Erlang and Scala Akka. I wrote the Swift Concurrency Manifesto back in 2017, which inspired many of these directions. I was very actively involved in the pitch reviews phases and have spent a bunch of time involved with the other Swift concurrency proposals as well.
Detailed comments:
Overall
This proposal is one of the bedrock technologies for concurrency in Swift. I think it will differentiate Swift from other languages in a very positive way, and lift large scale application and server design to the next level. Furthermore, Actors provide a strong basis to build reliability and distributed compute features into Swift. I think the complementary way that Actors and Structured Concurrency compose together is very elegant: they make for beautiful duals.
This proposal itself has made a huge amount of progress over the many iterations it has been through so far. I am very thrilled to see that this tackles thread and race safety head on through isolation and building on the @Sendable
feature. There is still a large hole in the model with mutable global state, but that is independent of actors (affecting structured concurrency as well) and has been split off to a subsequent proposal (correctly, in my opinion).
I am also very +1 on many details of the proposal, including the use of the actor
keyword to introduce actor declarations, allowing sync methods to be used from outside of the actor (implicitly promoting to async accesses) etc. I'm very happy that actors are allowed to participate in the protocol system, generics, etc. I also think that reentrancy is the right default for actors: the possible future extension to support blocking actors make sense to me if we find that we need them in practice, but I'd prefer to avoid the complexity if we don't.
That said, there are a few areas of the proposal that could use more consideration and iteration.
Direct Cross Actor References to let
constants should be part of the "nonisolated" proposal
The proposal says:
A reference to an actor-isolated declaration from outside that actor is called a cross-actor reference . Such references are permissible in one of two ways. First, a cross-actor reference to immutable state is allowed because, once initialized, that state can never be modified (either from inside the actor or outside it), so there are no data races by definition. The reference to
other.accountNumber
is allowed based on this rule, becauseaccountNumber
is declared via alet
and has value-semantic typeInt
.
This rule is problematic in two significant ways:
-
This is inconsistent with the Swift resilience / API evolution model, which doesn't differentiate between
let
constants and read-only properties. Furthermore, alet
constant in a public API is allowed to be upgraded to avar
constant in an API, which this design breaks. As proposed, actors will be inconsistent from the rest of the types in Swift, which will have significant ramifications for Swift API design. -
This is inconsistent with how actors in other languages work - all cross-actor accesses are supposed to be async and go through the actor's mailbox. This matters because reliability, distribution and other features of actors only work correctly if accesses are mediated through asynchrony (distribution implies I/O, which must be async). The options here are not good: 1) exclude distributed actors from having let properties, 2) make accesses to let-properties in distributed actors block implicitly, 3) make distributed and non-distributed actors have different semantics for
let
properties.
Fortunately, the solution to this problem appears to be simple:
- remove these special rules from this proposal so
let
properties behave the same way asget
-onlyvar
properties. This fixes resilience, actor invariants, and consistency. - Allow an opt-in way for
let
properties to be annotated with a new declaration modifier (e.g.nonisolated
), and - move this concept to the companion feature dedicated to breaking actor isolation since that is what it is most closely related to.
I think that fixing this is very important - we want the resilience model to be consistent across the language, and do not want to prevent people from using let
declarations in their actors. We should have a simple and consistent programming model for Swift that works across all types wherever possible.
Actor inheritance isn't needed and doesn't seem aligned with the design goals of Swift
The proposal states:
and "alternatives considered" has a section that tries to explain why inheritance is good for actors.
However, the proposal does not fully address what actor inheritance means or how it works. For example:
- This means that actors get the full complexity of required, delegating and convenience initializers, method overriding,
final
, etc. - As the proposal points out "actors and classes cannot be co-mingled in an inheritance hierarchy, so there are essentially two different kinds of type hierarchies" which adds complexity to the ecosystem.
- Non-final actors will needlessly have "virtual method" overhead like classes do, encouraging people to sprinkle
final
around which is ugly boilerplate. - Actors would be subject to much heavier metadata that bloats apps and slows app startup time like classes.
- The proposal points out that "actor types can have
static
andclass
methods" - shouldn't these be static andactor
methods? I don't think either ofclass func foo()
oractor func foo()
are going to send the right message. - There are many other aspects to this that are suggested but not defined - can actors be marked
@UIApplicationMain
?,@requires_stored_property_inits
?@objcMembers
? Can you mark an actor as a@propertyWrapper
? Classes have a lot of legacy from Objective-C which doesn't have to be pulled forward into a shiny new feature. - Swift has two reference semantic types (classes and closures): there is no need to follow the design of classes just because actors have reference semantics.
A first principles analysis of actors shows that they don't need any of this stuff, for the same reason that enums and structs don't need it: Actors can conform to protocols and use Protocol Oriented Programming, and this is the preferred way to implement type abstraction in Swift for a wide range of reasons. I believe that it would be much simpler and cleaner for actors to omit inheritance and be more similar to the structs and classes that are generally favored for non-legacy APIs and design patterns. Actors are an entirely new thing that Objective-C doesn't have, so the legacy considerations that force Swift classes into their design points don't apply.
The proposal has a nice section in "alternatives considered" that tries to explain why inheritance is a good idea. It am glad it does (and I encourage you to read it), but I'll rebut the points it makes one by one:
- Actor inheritance makes it easier to port existing class hierarchies to get the benefits of actors. Without actor inheritance, such porting will also have to contend with (e.g.) replacing superclasses with protocols and explicitly-specified stored properties at the same time.
This is a tiny win in the face of a huge problem. Moving existing code to actors will be inherently a huge remodel, not a small refactoring. The interfaces to actors are required to be async
, and async
is an effect modifier that quickly ripples through your code. You can't just change class
to actor
and walk away.
Furthermore, there is a way to make incremental refactoring process: incrementally refactor your Swift 5 class to use protocol oriented programming techniques in place first, then adopt actors when you eliminate OOP.
- The lack of inheritance in actors won't prevent users from having to understand the complexities of inheritance, because inheritance will still be used pervasively with classes.
Well sure, but we don't have any reason to add more complexity to other parts of the language ;-).
- The design and implementation of actors naturally admits inheritance. Actors are fundamentally class-like, the semantics of inheritance follow directly from the need to maintain actor isolation. The implementation of actors is essentially as "special classes", so it supports all of these features out of the box. There is little benefit to the implementation from eliminating the possibility of inheritance of actors.
Sure, but: 1) disabling inheritance is equally easy for the implementation. 2) implementation burden isn't the primary concern of swift-evolution, long term language quality is.
Actor inheritance has similar use cases to class inheritance.
Yep, and such a move would brings both the joys and follies of implementation inheritance and all the abuses of OOP that people find familiar in traditional class-based object oriented systems.
As a final historical note, when building Swift in the first place, we had to decide whether to allow structs (and enums) to inherit from each other. There is no technical obstacle to doing so, and such a move would have made it much easier to import C++ types. However, we decided not to do this because we believed in the power of protocols for type abstraction. So far, I am very very happy we went with a simpler model here and did not infect structs and enums with the complexity of classes. I hope we make a consistent move with Actors.
If we "must" support inheritance, then I'd encourage us to do so with a simpler model that jettisons the class initialization complexity which is only necessary due to legacy Objective-C design patterns that don't apply to actors.
Note that while I am negative on supporting inheritance of actors, I do think it is important for actors to be usable from Objective-C as ObjC objects. The solution to this is to allow the existing @objc
attribute on an actor (just as we allow on structs and enums). There is no need to support inheritance, or to support the legacy NSObject
methods on actors as the proposal suggest. Such methods would almost certainly be memory unsafe anyway.
The rules for @escaping
closures aren't actor-related and don't fix the problems they set out to
The proposal states two rules for closure isolation checks:
The first rule makes sense, and composes directly with SE-0302, but the second rule does not. As the proposal goes on to describe, @escaping
and @Sendable
are orthogonal features of a closure, and it makes perfect sense for a closure to be @escaping
without being @Sendable
. As such, I see several concerns about the proposal's suggestion that we conflate their meaning:
- This suggestion has nothing to do with actors. The same concerns raised in the proposal (e.g. interaction with legacy APIs like
dispatch_async
etc) apply to structured concurrency as well. Trying to fix a memory safety issue for actors but not structured concurrency doesn't make sense to me. - There are reasonable closures in APIs that are escaping but not sendable (e.g. all captured callbacks in normal non-concurrent settings!) and this unnecessarily subjects such APIs to
@Sendable
checking (but again, only within actors?). - This suggestion isn't fully effective, because it doesn't fix memory safety issues with non-
@escaping
closures: e.g. arguments to aparallelMap
algorithm written in Swift 5 which would be non-@escaping
. This issue isn't directly correlated with@escaping
, and people using these sorts of APIs in Swift 5 code are already dealing with them. - There are lots of other memory safety issues that will exist when working with Swift 5 code and imported C code. Swift has never been memory safe when working with imported languages.
- Once this rule is instilled into Swift it will be almost impossible to take it out: this suggestion turns a short term bump into long term language debt.
I recommend that we remove this unprincipled rule and solve the problem in a different way:
-
The proposal observes that the problem is common in specific idiomatic cases, e.g. completion handlers and well-known APIs like
dispatch_async
. It seems like a better solution would be to import these as@Sendable
closures through minor changes to the Clang importer in Swift, new attributes in Clang for the few C/Obj-C APIs that need them (this is a much MUCH smaller annotation burden thanImplicitlyUnwrappedOptional
auditing). -
The narrow case left over is legacy imported Swift 5 code. If this was seen as a problem, then it would be possible to import
@escaping
closures in Swift 5 modules as@Sendable
. Working with Swift 5 modules is not going to be memory safe for a bunch of other reasons (e.g. global variables) anyway, and I think that such an approach would encourage packages to upgrade to Swift 6 faster.
It doesn't appear from the proposal that this approach was explored. The major advantage of it is that it provides a simple, pure, and composable model for Swift 6 and beyond, as opposed to being stuck with legacy compatibility stopgap effectively forever.
The Actor
protocol is misnamed
The proposal states:
The Actor
protocol is an type eraser, so it seems more natural to follow the established precedent of AnyClass
"The protocol to which all class types implicitly conform."
Beyond AnyClass
, there is precedent with AnyHashable
: "A type-erased hashable value." and the Any
protocol composition, AnyObject
, etc. The word Any
is the unifying term for a "type erased thingy" in Swift regardless of the implementation of that eraser.
I think that aligning with AnyClass
is itself enough reason to name it AnyActor
.
Overall I think that the introduction of actors into Swift is a hugely positive direction. I look forward to continued iteration on this proposal!
-Chris