How come we can't mutate actor stored properties outside of the actor context but through self?

How come I can do something like in fetchNoError(), but not fetchError()? It seems pretty dubious to enforce having to mutate actor stored properties only within the actual actor context. As in fetch(), we're accessing it through self, so I imagine that any safety that is bestowed by the actor should be able to be bestowed via access through the actor itself, i.e., it's not like I'm capturing the stored property and mutating it independently of self.

Can someone lend some clarity on why it is done this way?

actor IGMConfigFetchManager: IGMConfigFetchManaging {
    private var outstandingRequests: OutstandingRequests 

    init() {
        self.outstandingRequests = .init()
    }

    func fetchError() async {
        await Global.doStuff(onCompletion: { [weak self] in
            self?.outstandingRequests.closeOut() // mutating (error in Swift 6)
        })
    }

    func fetchNoError() {
        await Global.doStuff(onCompletion: { [weak self] in
            self?.closeOut() // no error
        })
    }

    func closeOut() {
        self.outstandingRequests.closeOut()
    }
}

I suspect the difference is about using a capture of self rather than self directly. Regardless, probably neither should be allowed: Global.doStuff almost certainly does not promise anything about what actor the callback will be called on, which means all accesses to the actor are necessarily race-prone. I’m guessing that it isn’t declared as taking a @Sendable function, and so our callers here are being checked as if the closures they pass will always get called under the current isolation.

I’ve moved this to Using Swift, if you don’t mind.

2 Likes

But if we're accessing via a self capture and self is an actor, then doesn't the compiler have all the information it needs in order to know to perform mutations safely just as well as it does when the mutation is being performed w/i the actor itself?

Synchronous functions can't magically gain actor isolation on their own behalf; the caller has to invoke them with that isolation intact. This is because gaining actor isolation typically requires an async operation (enqueuing a job to run on the actor).