I spent a lot of time with actor initializers, and as well with Kavon syncing up about this work, so might as well provide my summary for review here.
- Yes I think the isolation model proposed sounds good, and although it has some annoying pieces to it, it does achieve race safety and is work-able in general.
- The proposal addresses a significant missing piece in our race-safety journey.
- Spent many hours battling actor initializers and reading the various version of the proposal.
Short notes;
If the friend were Sendable , then executor access is not needed, because the two friend instances would not be mutable.
Nitpick; it's not about mutable or not; it could be externally synchronized.
Non-delegating initializers
I'm happy with how we ended up with phrasing everything in terms of delegating or not. Huge yay also for the removal of convenience
Is
Implicit hop to self on fully-initialized
This works well and I'm in favor of this in general. It makes async initializers work as expected really. It is the non-async initializers which suffer limitations which is annoying but we're forced into this design somewhat.
nonisolated self initializers "decaying" the isolation
This took a while to internalize all the specific implications, but it makes sense and I'm in favor of this.
It means that we're allowed to make "one final escape of self" at the end of such initializer, which is important and useful. It's also equivalent to just adding the same line of code as "right after the init" basically.
I would really really want to get good diagnostics here. This model is sound and solid, but understanding what is going on from just "self is not isolated" messages that differ depending on line by line, can be quite confusing without proper explanation. Errors here should highlight the place where the self "decayed":
init(_ initialScore: Int) {
self.score = initialScore
greetCharlie(self) // note: self became nonisolated because of escaping use here
assert(score >= 50) // ❌ error: cannot access mutable isolated storage after `nonisolated` use of `self`
}
Parameters of initializers must be sendable
This rule I struggled with getting to terms with for a while to be honest. It often is an actor's purpose to get passed in some non-sendable thing and it shall from there-onwards always manage it.
It is true however that our language does not make this safe. What we want to express in these scenarios is "move this not sendable thing to the actor", and therefore the actor shall only manage it from here onwards. With the upcoming introduction of move() and move only semantics, we'll be able to express what I alluded to here in the type system, rather than hand-waving away the issue. Until then we'll have to use unsafe wrappers to pass such things to actors, and while unfortunate it's an ok workaround I suppose...
Really looking forward to be able to:
// today:
_ = await Gene(with: ns) // ❌ error: cannot pass non-Sendable value across actors
_ = await Gene(with: move(ns)) // ✅
or however we'll decide to spell these in the future.
Related: Distributed actor initializers
The proposal works well with distributed actors. We discuss the interaction in-depth in the distributed actor runtime pitch's "readying distributed actors" section. Summing up though:
- distributed actors have a concept of becoming ready for remote calls
- in inits with
nonisolated self this is done at the end of initializers
- in inits with
isolated self this is done immediately as the actor becomes fully-initialized, same as this proposal's "hop to self"