Does anyone have advice for defining or passing an isolated and synchronous closure that should also be Sendable?
Suppose I start with an actor. I need to perform some work with withTaskCancellationHandler. I begin an operation and run a handler if this task is cancelled.
actor A {
func operation() {
}
func handler() {
}
func f1() async {
await withTaskCancellationHandler(
operation: self.operation,
onCancel: self.handler
)
}
}
This compiles with no errors.
But what if I need some more state here? Some additional information that should be communicated to operation and handler?
extension A {
func operation<T: Sendable>(_ value: T) {
}
func handler<T: Sendable>(_ value: T) {
}
}
extension A {
func f2<T: Sendable>(_ value: T) async {
await withTaskCancellationHandler(
operation: {
self.operation(value)
},
onCancel: {
self.handler(value)
}
)
}
}
This fails with an error:
error: call to actor-isolated instance method 'handler' in a synchronous nonisolated context [#ActorIsolatedCall]
25 | }
26 |
27 | func handler<T: Sendable>(_ value: T) {
| `- note: calls to instance method 'handler' from outside of its actor context are implicitly asynchronous
28 |
29 | }
:
37 | },
38 | onCancel: {
39 | self.handler(value)
| `- error: call to actor-isolated instance method 'handler' in a synchronous nonisolated context [#ActorIsolatedCall]
40 | }
41 | )
At this point I'm not completely clear I understand. The withTaskCancellationHandler function should AFAIK inherit the isolation of this actor.
Let me try a closure to capture my state and pass that directly to withTaskCancellationHandler:
extension A {
func f3<T: Sendable>(_ value: T) async {
let onCancel = {
self.handler(value)
}
await withTaskCancellationHandler(
operation: {
self.operation(value)
},
onCancel: onCancel
)
}
}
This fails again with a different error:
error: converting non-Sendable function value to '@Sendable () -> Void' may introduce data races
53 | self.operation(value)
54 | },
55 | onCancel: onCancel
| `- error: converting non-Sendable function value to '@Sendable () -> Void' may introduce data races
56 | )
57 | }
Ok. Let me try making the closure Sendable:
extension A {
func f4<T: Sendable>(_ value: T) async {
let onCancel = { @Sendable in
self.handler(value)
}
await withTaskCancellationHandler(
operation: {
self.operation(value)
},
onCancel: onCancel
)
}
}
Here's another error:
error: call to actor-isolated instance method 'handler' in a synchronous nonisolated context [#ActorIsolatedCall]
25 | }
26 |
27 | func handler<T: Sendable>(_ value: T) {
| `- note: calls to instance method 'handler' from outside of its actor context are implicitly asynchronous
28 |
29 | }
:
61 | func f4<T: Sendable>(_ value: T) async {
62 | let onCancel = { @Sendable in
63 | self.handler(value)
| `- error: call to actor-isolated instance method 'handler' in a synchronous nonisolated context [#ActorIsolatedCall]
64 | }
65 |
My closure is synchronous but not isolated to my actor. Fair enough. Here is another attempt with a nested local function:
extension A {
func f5<T: Sendable>(_ value: T) async {
func onCancel() {
self.handler(value)
}
await withTaskCancellationHandler(
operation: {
self.operation(value)
},
onCancel: onCancel
)
}
}
Another Sendable error:
error: converting non-Sendable function value to '@Sendable () -> Void' may introduce data races
83 | self.operation(value)
84 | },
85 | onCancel: onCancel
| `- error: converting non-Sendable function value to '@Sendable () -> Void' may introduce data races
86 | )
87 | }
Here is another attempt:
extension A {
func f6<T: Sendable>(_ value: T) async {
@Sendable func onCancel() {
self.handler(value)
}
await withTaskCancellationHandler(
operation: {
self.operation(value)
},
onCancel: onCancel
)
}
}
Which gets me back to where I was before:
error: call to actor-isolated instance method 'handler' in a synchronous nonisolated context [#ActorIsolatedCall]
25 | }
26 |
27 | func handler<T: Sendable>(_ value: T) {
| `- note: calls to instance method 'handler' from outside of its actor context are implicitly asynchronous
28 |
29 | }
:
91 | func f6<T: Sendable>(_ value: T) async {
92 | @Sendable func onCancel() {
93 | self.handler(value)
| `- error: call to actor-isolated instance method 'handler' in a synchronous nonisolated context [#ActorIsolatedCall]
94 | }
95 |
But since it's a function now instead of a closure⊠can I use SE-0420 to pass isolation directly?
extension A {
func f7<T: Sendable>(_ value: T) async {
@Sendable func onCancel(
isolation: isolated (any Actor)? = #isolation
) {
self.handler(value)
}
await withTaskCancellationHandler(
operation: {
self.operation(value)
},
onCancel: onCancel
)
}
}
This works on making my nested location function isolated to my actor. But there are two new errors:
error: actor-isolated synchronous local function 'onCancel(isolation:)' cannot be marked as '@Sendable'
105 | extension A {
106 | func f7<T: Sendable>(_ value: T) async {
107 | @Sendable func onCancel(
| `- error: actor-isolated synchronous local function 'onCancel(isolation:)' cannot be marked as '@Sendable'
108 | isolation: isolated (any Actor)? = #isolation
109 | ) {
/Users/rick/Desktop/ActorDemo/Sources/ActorDemo/main.swift:117:17: error: cannot convert value of type '@Sendable (isolated (any Actor)?) -> ()' to expected argument type '@Sendable () -> Void'
115 | self.operation(value)
116 | },
117 | onCancel: onCancel
| `- error: cannot convert value of type '@Sendable (isolated (any Actor)?) -> ()' to expected argument type '@Sendable () -> Void'
118 | )
119 | }
I can try and remove the Sendable:
extension A {
func f8<T: Sendable>(_ value: T) async {
func onCancel(
isolation: isolated (any Actor)? = #isolation
) {
self.handler(value)
}
await withTaskCancellationHandler(
operation: {
self.operation(value)
},
onCancel: onCancel
)
}
}
But this just moves my error around somewhere else:
error: converting non-Sendable function value to '@Sendable () -> Void' may introduce data races
132 | self.operation(value)
133 | },
134 | onCancel: onCancel
| `- error: converting non-Sendable function value to '@Sendable () -> Void' may introduce data races
135 | )
136 | }
/Users/rick/Desktop/ActorDemo/Sources/ActorDemo/main.swift:134:17: error: cannot convert value of type '(isolated (any Actor)?) -> ()' to expected argument type '@Sendable () -> Void'
132 | self.operation(value)
133 | },
134 | onCancel: onCancel
| `- error: cannot convert value of type '(isolated (any Actor)?) -> ()' to expected argument type '@Sendable () -> Void'
135 | )
136 | }
It seems like what I'm trying to do should be possible without major ceremony. Is something like this blocked on closure isolation control?