Difference between `nonisolated(nonsending)` function and `isolation: isolated (any Actor)? = #isolation` parameter

SE-0420 - Inheritance of actor isolation introduces the possibility of inherit actor isolation of he caller with the syntax

func foo(isolation: isolated (any Actor)? = #isolation) async

while SE-0461 - Run nonisolated async functions on the caller's actor by default introduces the notation

nonisolated(nonsending) func foo() async

to specify that the function will always run on the caller's actor.

What are the differences, if any, between the two? Are they interchangable?

1 Like

not an exhaustive enumeration, but a few things come to mind – the parameterized form is more general since the caller gets to specify the actor to which the function should be isolated when invoked. it need not use the implicit value that comes from the default parameter macro expansion:

func withIsolation(actor: isolated (any Actor)? = #isolation) async { ... }

actor A {}

let a1 = A()
await withIsolation(actor: a1)

let a2 = A()
await withIsolation(actor: a2)

it also allows constraining callers to use a known-nonnull actor instance at compile time by changing the type signature of the isolated parameter:

func onlyCallableWithActor(actor: isolated (any Actor) = #isolation) {
  print("called with actor: \(actor)")
}

@concurrent
func noActor() async {
  await onlyCallableWithActor() // 🛑 error: 'nil' is not compatible with expected argument type 'any Actor'
}

actor A {
    func doit() {
        // #isolation macro expands to `self` so no await required
        onlyCallableWithActor()  // ✅ prints: called with actor: A
    }
}

additionally, nonisolated(nonsending) may currently only be used with async functions, which, as the above illustrates, is not a limitation shared by the parameterized approach.

in short – no, at least not in general. in specific cases though you can probably get away with converting between them. e.g. if you're trying to replace a function with a default isolated parameter with a nonisolated(nonsending) version and existing callers don't pass the parameter explicitly then a conversion would probably work out okay in many cases[1].


one last caveat i'll mention because it has been on my mind recently: in the nonisolated(nonsending) form, there is only access to the implicit isolated parameter via the #isolation macro. the value derived from that macro is not itself an isolated parameter – it's a binding to an isolated parameter (or at least that seems to be how the compiler treats it at the moment). unfortunately this means you can end up with confusing behavior due to the way closure isolation inference works, like this:

// build _without_ the NonisolatedNonsendingByDefault feature enabled

nonisolated(nonsending)
func perform(_ work: () async -> Void) async {
    await work()
}

func forward_param_isolation(isolation: isolated (any Actor)? = #isolation) async {
    print("parameter isolation: \(isolation, default: "<nil>")")    // Optional(Swift.MainActor)
    await perform {
        // capture of the isolated paramter makes this closure isolated
        // to that same parameter's value
        _ = isolation
        let closureIsolation = #isolation
        print("parameter closure isolation: \(closureIsolation, default: "<nil>")")    // Optional(Swift.MainActor)
        assert(isolation === closureIsolation)  // ✅
    }
}

nonisolated(nonsending)
func forward_implicit_isolation() async {
    let isolation = #isolation
    print("inherited isolation: \(isolation, default: "<nil>")")    // Optional(Swift.MainActor)
    await perform {
        // there's no capture of an isolated parameter, and this closure
        // isn't itself nonisolated(nonsending), so it's not isolated
        let closureIsolation = #isolation
        print("inherited closure isolation: \(closureIsolation, default: "<nil>")")    // nil
        // assert(isolation === closureIsolation)   // đŸ’Ĩ
    }
}

@MainActor
func test() async {
    await forward_param_isolation()
    await forward_implicit_isolation()
}

await test()

  1. IIUC there are also ABI differences b/w the two, which if you care about that sort of thing would presumably matter if trying to change from one to the other. â†Šī¸Ž

7 Likes