Sorry for getting to this bonus discussion a little late. .
In the existing implementation, the actor-isolation of the property does not extend until the end of a chained access that involves that property. So, in this example access:
let s: S = ...
await s.c.anotherField.doSomething()
The load of anotherField and the call to doSomething is not guaranteed to be run on the MainActor, unless if those declarations are isolated to the main actor too. With solely the one marking of MainActor on the struct, only the load of the property c from s is performed on the MainActor. Since c is not only a let-bound property, but one that is part of a struct, having that load happen with synchronization has no benefits. Subsequent accesses are still memory-safe, as Doug brings up, only because of Sendable restrictions. In light of that, I think discussion of global-actor isolation on stored properties in this version of the proposal is half-baked. I hope to have a new version ready soon that covers this in more depth.
Can you share a bit more of how you're using this pattern, Jacob? Currently, the existing actors proposal says that global-actor isolation should not apply to an actor's stored properties, so it's a bug that the compiler is permitting that. I'd like to consider your use-case as I prepare a new version of the actor-initializer proposal.
I have an actor that manages a server access token. My network code awaits on the actor to get the current token, but the UI needs to know when the token is invalid (and not renewable). The delegate property is only written from the main thread by the interested UI component, and in the actor, I hop to the main thread before reading the delegate and calling any methods.
There are probably lots of ways to design this, but this way seemed the most natural to me (and mirrors how the component worked before it was an actor), and I didn’t realize it wasn’t allowed.
Here are other examples which ultimately got me thinking about all this. This code is part of a book on async / await in Swift:
Making some ObservableObjects an actor seems to be very tempting, however most of those stored properties would still need to obey the MainActor.
I really hope to see some clear rules for all of this. And I also would like to bump the issue Chris Lattner mentioned upthread. In the book the author eventually bumped into an error messages like the following one:
Call to global actor 'ImageDatabase'-isolated initializer 'init()' in a synchronous actor-isolated context
This forced the introduction of a IUO-stored-actor-property and some deferred initialization via a setUp method. Reading this felt unnecessarily painful to me, especially as I personally would like to avoid IUOs as much as possible. There must be a better solution to such problems.
I agree. In fact, I think the definition of EmojiArtModelhere has no reason I can think of to be an actor type, because most of the code doesn't rely on being isolated to it, except to update an unused integer counter. It should just be a MainActor-isolated class. For example, the only instance-isolated method verifyImages could be marked nonisolated without changing its body, since it only accesses a MainActor-isolated property before launching child tasks that will await to access the private method anyway.
Based on that error message alone, I think the problem is that they're trying to call an init that is isolated to a global actor, from outside of that global-actor, so an await is required. But, that call appears in a synchronous function. That diagnostic could definitely be improved in this situation.
For the actor ImageLoader, the setUp method here could be changed to be an async initializer, since it is already defined to be an async method. The same goes for the other setUp method being async throws.
The async actor initializers that Chris and I have been discussing for this proposal already work in released versions of Swift 5.5 in the way we all agree on (barring some small bugs I recently fixed). That is, there are no extra restrictions on what you can do with self in an async initializer (i.e., you're effectively isolated to self). The main contention in this proposal is on synchronous actor initializers, because the escaping-use restriction is an unnecessary pain and doesn't match up with async initializers. The new version of this proposal that I'm working on will remove that pain and simplify things a lot, I think.
I appreciate hearing about how you designed it, even if it's currently relying on a mistake in the implementation .
I can understand the desire for the UI to have the ability to access that delegate property on a reference type without giving up the main thread. Let's just ignore my proposed solution to stored properties and global-actor isolation in this version of the proposal. I'll make sure the new one strikes a better balance and considers more scenarios.