Why does this "seem natural"? I cannot point to any other place in the language where a value changes type within a nested context, and we've resisted such changes in the past (e.g., an value x: Int?
becoming type Int
inside an if x != nil
).
We're generally quite careful about adding implicit conversions, because they can have unintended consequences. In this case, the type-directed approach effectively needs two implicit conversions, one in each direction. Going from @sync
-> @async
is always fine (and needs to happen at some point), but you also need to be able to go from @async
-> @sync
in certain limited places. This is an "interesting" conversion because it's presence means you are interacting with an actor asynchronously, so each such conversion needs to be part of something that can be done asynchronously (function call, read from an actor property, etc.) and must be covered by an await
.
That's a fair point. I suppose I was thinking about how the sequence of conversions you take can affect type identity, which I find a bit odd. For example, consider this:
func f(a: @sync MyActor) {
let a1: Any = a
let a2: Sendable = a
let a3: Any = a2
}
What are the types dynamically stored in a1
and a3
? They're actually different, because it's a1
will have dynamic type @sync MyActor
, while a3
will have dynamic type @async MyActor
because it had to be Sendable
to initialize a2
. Per your comment about optional and conversions, I bet you can construct a similar case with Optional<T>
, so maybe this oddity is a big deal.
Let's go more dynamic, though. What if now I write:
if let a4 = a3 as? @sync MyActor { ... }
Does this dynamic cast fail, because the type stored in a3
is @async MyActor
, or is the runtime dynamically checking whether we are currently executing on the this actor and allow the cast to succeed?
Happy to discuss!
There's more rationale in one of the early pitch thread documents, but essentially the argument is that the self
of an actor shouldn't be special: you should be able to write a free functions and closures that can have synchronous access to the actors, for example, because that's normal factoring of code. It's very similar to how mutating
methods of structs/enums are really just inout
on the self
parameter, so mutating
just isn't all that special.
isolated
parameters on closures also let you write things like the runOnActor
operation mentioned above.
Doug