Hi all, I'm new to both the concurrency pitches and speaking up on Evolution in general, so apologies if some of these questions have obvious answers I'm just not finding in the documents. Maybe they'll at least prompt some example tuning?
I don't see any examples of chained calls in async
/await
, so I'm not sure if it's possible under the current phase of proposals. I'm trying to understand how something like the following might work:
struct S {
var balance: Int = 0
mutating func deposit(_ money: Int) { balance += money }
}
class C {
var balance: Int = 0
func deposit(_ money: Int) { balance += money }
}
actor A {
var vally = S()
var reffy = C()
}
var accountBook = A()
await accountBook.vally.deposit(10)
await accountBook.reffy.deposit(10)
My first guess is that both of the await
lines would fail for one of two reasons: either await
doesn't handle chained calls or because they'd decompose as equivalent to
// left A isolation at ;, x-actor ref to setter prohibited
{ var s = await account.vally; s.deposit(10); }
// left A isolation at ;, x-actor ref to non-sendable ref type prohibited
{ var c = await account.reffy; c.deposit(10); }
But my more hopeful guess is that one or both of them would decompose to the following:
await account.runOnActor { a in a.vally.deposit(10); }
await account.runOnActor { a in a.vally.deposit(10); }
This would be my first assumption for a fully finished actor model, because Swift IME has spent a lot of effort on devolving a type's interface to supporting member types, but I'm not sure if that's enabled by the current version of await
and actor isolation.
I only started reading the concurrency pitches today yesterday, but my first pass of this one left me concerned that refactoring a given class to an actor will implicitly change all my properties to the equivalent of private(set)
and force me to declare new setX
methods that I already eschewed when designing the original class, because the members' interfaces served as extensions of the owner's interface. I understand the goals of
- Cannot pass mutable state across actor boundaries
- Prefer not to encourage storms of
async a.x += 1; async a.y += 1; ...
- Prefer not to encourage large
runOnActor
s of arbitrary computation
But these are all in tension with each other and I don't see explanation of how this design encourages or enables the healthy middle ground of messaging an actor to call one mutable method on one of its childrenābecause you've already been trying to right-size mutation, which is why you grouped some data up in a supporting type. The closest thing to a suggestion I saw was essentially redeclaring the mutable interface of each member. I'm not sure that is adequate to help us use phase 1 refactors to decide what we want out of phase 2.
Which brings me to my second question:
Is isolated
only applicable to a function parameter of actor type, or can it be used for any one parameter? I assume not, because I could not find any examples of this use, but IIUC non/isolated
is otherwise an attribute of an actor's members so using it to refer to a parameter-of-actor-type made me doubt my understanding for a moment; once I translated it to a: isolator A
it was fine. I certainly understand the desire to keep keyword count low. I also can't avoid pitching the flavorful starring A
as an alternative though 
That said, I really like the concept of allowing isolated
on any parameter type, as a way to opt a function into an arbitrary actor's executor without adding the actor to the parameter list. I'm picturing something like the following:
actor Hero { // freshly retyped from class
var stats = Stats()
}
func ponder(hero: isolated Hero) { ..; buffAll(hero.stats) }
class Stats {
var hitting: Int = 10
var thinking: Int = 10
var tolerating: Int = 10
}
func buffAll(_ s: Stats) {
s.hitting += 1
s.thinking += 1
s.tolerating += 1
}
// bad; Stats is non-sendable ref type (and maybe ill-formed to wrap async in a sync call?)
await buffAll(someHero.stats);
// signature updated as part of Hero refactor
func buffAll(_ s: isolated Stats) { ⦠}
// ok; semantically similar to
// someHero.runOnActor { hero in buffAll(hero.stats) }
await buffAll(someHero.stats)
await buffAll(someMonster.stats) // actor Monster also benefits!
Is that something this pitch currently admits? If so, I think I'd appreciate an example or two in the pitch documentāit certainly clarifies that isolated
isn't actually being repurposed in a function signature, but is the same modifier we apply to member decls. Considering Swift often uses multiple related types to support the final interface of a type, it would be nice to see examples of how supporting types might update alongside the newly minted actor.
If this pitch doesn't currently consider thatācould it? IIUC the compiler already needs to know the actor isolating each argument at each call site. Here's my reasoning:
- There is nothing special about
self
as of pitch 4; it is just a decl isolated to a Hero
-
stats
is another decl isolated to a Hero
- Some compiler operator
isolating(nonisolated Actor)
exists such that await ponder(someHero)
decomposes to await isolating(someHero) { a in ponder(a) }
- The compiler uses some sort of
isolatorOf()
operator to check each of these decls at each call site, to determine things like "is this x-actor ref declared let
in its parent?"
-
buffAll(s: isolating Stats)
could decompose to isolating(isolatorOf(someHero.stats)) { a in buffAll(a.stats) }
- That decomposition could, possibly as future work, be a projection with a keypath or internal equivalent:
isolating(someHero.stats) => isolating(someHero, projecting: \.stats) { s in buffAll(s) }
This would also be another plausible justification for allowing multiple isolated
parameters: to support functions that modify two non-actors that share an owning actor/executor.
extension Stats {
// or global func steal(from: isolated Stats, toPay: isolated Stats)
// only safe when (isolator(self) == isolator(other))
isolated func steal(from other: isolated Stats) {
other.tolerating -= 1
tolerating += 1
}
}
actor HeroOfTime {
var link = Stats()
var fairy = Stats()
// ok with or without non-actor isolated annotations
func emergency() { link.steal(from: fairy) }
}
// ok: isolatorOf(fairy) == isolatorOf(link)
await aTimelyHero.link.steal(from: aTimelyHero.fairy);
I believe (hope) this would be an extension of the previous reasoning plus Chris's first claim:
- it is trivially safe to pass
self
to multiple isolated
parameters of a function
- this is true because both copies of
self
trivially share an isolation context
- Both
link
and fairy
are also isolated data members of HeroOfTime
- Their shared isolation context already needs to be checked in
emergency
even without the new modifiers.
One more reason this is potentially interesting IMHO: one of the "workarounds" for my first buffAll
example would be to re-type Stats
as an actor, because actor references are Sendable
. Semantically, Stats
is just a way to group up some related Int
s that a lot of actors use; it's not really appropriate for it to synchronize separately from its owners, who will synchronously manage other properties in response to stat updates. This means I may want actor Stats
to be passed its owner's executor upon initialization. Even if I expect all Hero->Stats
messages to be optimized onto the same thread, Hero
contains many synchronous calls to buffAll
and they would have to be rewritten to async
, muddying the waters on Stats
's supporting role within its owner.
I'm not saying that's the correct architecture, but it's one I can see the current limitations encouraging, with some classes becoming actors out of convenience, who semantically ought to be managed by their owner's executor. Another pitch looks like it will enable that shared executor, so the ability to say "I know I can reference my member actor's members synchronously because I know they're isolated to my context" would be nice.
Okay last question: why are key paths currently disallowed, out of curiosity? Is it just a matter of waiting on Sendable
to be accepted before it's workable, or are there some other aspects of actor that would make a readonly IsolatedKeyValue
hard?