Enqueuing a partial task is the primitive, low-level operation that we want all executors to provide. The partial task fully encapsulates the unit of work that needs to be scheduled without any added overhead. Wrapping that up as an ordinary first-class function value would introduce a lot of overhead: partial tasks are one-shot and self-consuming, function values are not. It would also be semantically problematic because a call to an async function always happens as part of a task, but an executor itself is not a task; the async function would have to ignore its given context and introduce the appropriate task context.
Ideally, I would like global actors to be global (singleton) instances of an implicit corresponding class, so that you wrote something like:
public global actor UIActor {
func enqueue(...) {}
}
and the identifier UIActor
would (in most contexts) resolve as a reference to the singleton instance of the UIActor
class. I think that is much cleaner. It is also, however, a whole feature in and of itself.
PartialAsyncTask
needs to stay as is for performance reason, got it.
What about execute
being protocol requirement, and not separated extension, and other non-final
shenanigan? It feels like this is more appropriate to be in an extension:
extension Actor {
func execute(...)
or even global function:
func execute(partialTask: ..., using actor: ...)
And run seems like it should be overridable:
protocol Actor {
func run(...)
}
enqueue
needs to be the customization point, or else the general enqueue
has to forward to something.
The details of turning the function passed to run
into a partial task and enqueuing it are not actually interesting to customize in an executor implementation.
I'm pretty sure we do need another value here for "actor-confined"; withoutActuallyEscaping(_:do:)
's documentation explicitly discusses using it to run non-escaping closures concurrently, and changing our minds about that being legal seems tantamount to a source break.
Copying a discussion from the roadmap thread per suggestion from @Lantua:
If these accesses are disallowed, from the "First Phase: Basic Actor Isolation" example in the roadmap thread:
// error: an actor cannot access another's mutable state
otherActor.mutableArray += ["not allowed"]
// error: either reading or writing
print(other.mutableArray.first)
What is the meaning of mutableArray
being declared internal
?
I see how this proposal adds an axis beyond access control—mutableArray
is restricted even beyond what private
would signify:
synchronous functions may only be invoked by the specific actor instance itself, and not even by any other instance of the same actor class.
(my emphasis)
What I am wondering is if these axes are orthogonal: is it at all meaningful that mutableArray
is internal
here? I'm not sure that access control modifiers really matter at all for actor state. Perhaps they would matter if the state was annotated @actorIndependent
? I wonder if the language or the developer tools might clarify this at all.
At the very least, access control modifiers will affect the visibility of declarations from extensions, which are actor-isolated.
AFAICT, public
actor-dependent and private
actor-independent both make sense:
actor class X {
public var dependent: ...
@actorIndependent private var independent: ...
}
/// Same File
func foo(x: X) {
x.dependent // error: actor isolation
x.independent // ok
}
/// Separate module
extension X {
func bar() {
x.dependent // ok
x.independent // error: access control
}
}
Looks pretty orthogonal to me.
If these accesses are disallowed, from the "First Phase: Basic Actor Isolation" example:
// error: an actor cannot access another's mutable state
otherActor.mutableArray += ["not allowed"]
// error: either reading or writing
print(other.mutableArray.first)
What is the meaning of MyActor
having declared access to mutableArray
to be internal
? Is mutable actor state automatically considered private
?
I see how this is a separate axis, yes, and how actor state is restricted even beyond what private
would imply. From the link you share:
synchronous functions may only be invoked by the specific actor instance itself, and not even by any other instance of the same actor class.
(my emphasis)
What I am wondering is if these axes are orthogonal: is it at all meaningful that mutableArray
is internal
here? I'm not sure that access control modifiers really matter at all for actor state. Perhaps they would matter if the state was annotated @actorIndependent
? I wonder if the language or the developer tools might clarify this at all.
Maybe we can jump over to the discussion thread? I don't think this fits the roadmap thread.
(This was moved from the roadmap thread upon request, the question was answered a few posts above).
Something about the name actor class
feels weird to me. Will there ever be actor struct
s or actor enum
s? If not, and actors will always be classes, why mention it at every definition?
I mean, the proposal says:
Actor classes behave like classes in most respects: the can inherit (from other actor classes), have methods, properties, and subscripts. They can be extended and conform to protocols, be generic, and be used with generics.
But then goes on to show a bunch of examples which would be valid in a class
but not in an actor class
. It's worth noting that methods, properties and subscripts are in no way unique to classes, nor are extensions, protocols or generics.
The only thing actors can do which is in any way class-like is support inheritance. To be honest, inheritance is often a massive pain (especially when considering things like subclasses and Equatable
), so most of my actors will be final
. I wouldn't even mind if they didn't support inheritance.
I see actors as a fundamentally new thing. They enforce data isolation in a way that is wholly unique in the language, and if anything is more like the grouped-exclusivity rules of value types than the very liberal rules which apply to stored properties of class
es.

The only thing actors can do which is in any way class-like is support inheritance.
They're also reference types, and therefore have reference identity. They satisfy AnyObject
constraints. We call them actor class
because they are a restricted form of class, and nearly every intuition one has about classes also applies for actor classes.
Doug

nearly every intuition one has about classes also applies for actor classes.
Doesn’t this pretty-much just boil down to reference types with reference identity, though? The other things, like concurrent access to stored properties, don’t apply.
For me, I’d prefer a shorter syntax, since AIUI this is the fundamental unit of data synchronisation. Data within an actor is always synchronised with respect to the other data members, and if you want one piece to live on its own timeline you’d encapsulate it in its own actor.
I've been trying to figure this out. If I have an old custom executor, say DispatchQueue
or some kind of RunLoop
, and would like to wrap it as a (global) actor, how should I go about doing it? There don't seem to be a direct way to call async
function from inside sync function, and old executor would accept only sync ones. Feels like it would need to open up PartialAsyncTask
somehow.

Hmm. I think of value types as "types that have value semantics" and consider the TSPL's use to be incorrect. However, I can see how this proposal is confusing in that regard... and can move toward "value semantics" terminology.
I think that's a totally reasonable interpretation (and undoubtedly how a not-insignificant portion of the Swift community uses the terms), but we should make sure we're being consistent in how we're using this terminology (i.e., fix TSPL if we want to change how these terms are expected to be used). Also, if "value type" and "type with value semantics" are synonymous, do we need a general term for "type defined by a struct
/enum
/tuple"?
IIRC I've been corrected myself on this usage by @dabrahams, who may have stronger feelings than I about the terminology here. In any case, I've opened a PR to reword the Escaping reference types section in terms of "semantics." If that doesn't feel as though it more precisely communicates the intended meaning, feel free to decline!

I've been trying to figure this out. If I have an old custom executor, say
DispatchQueue
or some kind ofRunLoop
, and would like to wrap it as a (global) actor, how should I go about doing it? There don't seem to be a direct way to callasync
function from inside sync function, and old executor would accept only sync ones. Feels like it would need to open upPartialAsyncTask
somehow.
PartialAsyncTask
will have some kind of synchronous run()
operation that should allow one to do this. For DispatchQueue
, we will probably want to add some API to allow you to run an async
operation on that particular queue. The details here will evolve as more of the pieces of the prototype implementation come together.
Doug

PartialAsyncTask
will have some kind of synchronousrun()
It doesn't sound that much different from converting it to first class function (including the call-once restriction) which seems to be contrary to what @John_McCall said earlier (quote below). Or do you plan to have some fast path for default execute
implementation?

The partial task fully encapsulates the unit of work that needs to be scheduled without any added overhead. Wrapping that up as an ordinary first-class function value would introduce a lot of overhead: partial tasks are one-shot and self-consuming, function values are not.
Regarding this (code) comment, from the Actor Isolation section (near the end):
// Safe: this operation is the only one that has access to the actor's local
// state right now, and there have not been any suspension points between
// the place where we checked for sufficient funds and here.
Does this mean that, if one actor method call suspends (because it calls an async
function), then other method calls on that same actor could run while the original is suspended? That is, could separate method calls on an actor be interleaved?
From the rest of the proposal, I would have expected that a given actor method call would run completely before any others were allowed to run.
Yes, it's in the Async Function pitch.
This design currently provides no way to prevent the current context from interleaving code while an asynchronous function is waiting for an operation in a different context. This omission is intentional: allowing for the prevention of interleaving is inherently prone to deadlock.