I have a protocol where implementations hook into some events and forward them via a listener closure:
protocol Interceptor<Event> {
associatedtype Event
func listen(_ listener: @escaping (Event) -> Void)
}
With Swift concurrency, the listener needs an isolation annotation. The obvious choice is @Sendable, but that's overly restrictive. Some interceptors guarantee they call the listener on a specific actor. For example, some interceptors always fire on @MainActor. Marking the closure @Sendable forces callers to hop back to MainActor unnecessarily:
// With @Sendable listener, caller can't assume MainActor:
interceptor.listen { event in
MainActor.assumeIsolated { self.updateUI(event) } // runtime check, not compile-time
}
I want the protocol to let each conformance declare the isolation context (or lack thereof) in which the listener is called, so callers get compile-time safety.
Solutions considered
1. Protocol hierarchy
protocol SendableInterceptor<Event> {
associatedtype Event
func listen(_ listener: @escaping @Sendable (Event) -> Void)
}
protocol MainActorInterceptor<Event> {
associatedtype Event
func listen(_ listener: @escaping @Sendable @MainActor (Event) -> Void)
}
That works. @MainActor on the closure gives the caller synchronous MainActor access with no await:
func register(interceptor: some MainActorInterceptor<Event>) {
interceptor.listen { event in
updateUI(event) // MainActor-isolated, compile-time safety
}
}
But it requires a separate protocol per isolation context, which is a bit weird.
2. Async callback + caller annotation
func listen(_ listener: @escaping @Sendable (Event) async -> Void)
Callers could annotate { @MainActor event in ... }, but every callback goes through Task even when you're already on the right actor.
Is there a way to express a protocol that is generic over isolation context (including nonisolated), without splitting into separate protocol types? Something like an Isolation generic parameter that can be MainActor, another global actor, or "nonisolated," and that annotates the closure in a way the compiler understands at the call site?