@MainActor access in closures created in Main Actor functions

When looking into moving from integrating Async-Await with some Combine code, I came into something odd I'm hoping I can get some clarity on.

@MainActor
func configureCancellables() {
    Just(true)
        .receive(on: DispatchQueue.main)
        .sink { [unowned self] in printBool($0) }
        .store(in: &cancellables)
}

@MainActor
func printBool(_ value: Bool) {
    print(value.description)
}

The closure type here is implicitly considered to be (Bool) -> Void rather than @MainActor (Bool) -> Void. Indeed, if I add @MainActor to the closure, it throws the error:
"Converting function value of type '@MainActor (Bool) -> ()' to '(Bool) -> Void' loses global actor 'MainActor'".

My question is how does the closure infer @MainActor access from the function it is being created within? The closure can then be passed around to an API that calls this on any thread, so I'm curious what the rule is and how this is set up.

On a side note, I'm somewhat lucky that this does work, as otherwise there would seem to be no way to perform synchronous actions in the closures where they are actually on the main thread already. This affects cases if you perform a map or other dependent function that relies on on state of self. If this were to change, I'd expect any closures in Combine to become basically unusable in Swift with any API that was not nonisolated, which is a lot of them seeing UIKit etc has been completely annotated with @MainActor.

Does it still work in Swift 5.7? See swift-evolution/0338-clarify-execution-non-actor-async.md at main · apple/swift-evolution · GitHub

Yes, this does still work. The changes for nonisolated async functions don't affect this behaviour. :slight_smile:

This is a bug in the compiler, and if you change .receive(on: DispatchQueue.main) to be .receive(on: DispatchQueue.global()) you will even get a runtime purple warning letting you know that a race condition is possible. The code should produce a warning, and to workaround the warning you could make printBool nonisolated.

There are also a few issues tracking something similar, if not exactly the same thing:

I suspect fixing this defect will, in practice, cause massive issues for those people using Combine with any UI related content, as all UIKit's view and view controller code for example, is tagged as @MainActor isolated. This would seem like it would break anyone using Combine where they're touching UIKit-isolated content in Combine without an explicit Task.

Indeed. I think I misread your original question. Thanks for the reply!