Lack of SendableMetaType for Combine's Subscriber makes it impossible to use it in Swift 6.2

(Cross-posted from the feedback FB18050331)


The Combine Subscriber protocol does not currently conform to SendableMetaType.

This lack of conformance makes it impossible to use it without compiler errors or warnings similar to "Capture of non-sendable type 'SomeSubscriber.Type' in an isolated closure".

Indeed, the subscriber is supposed to be notified of published values in whatever isolation a Publisher or Subscription is working in (frequently, nonisolated). Hence a Subscriber type can not support isolated conformances. This is by design of pub/sub in Combine, where subscribers can not specify their preferred isolation. Hence a Subscriber type can not support isolated conformance introduced in SE-0470.

Please fix this by adding SendableMetaType conformance to Subscriber:

-public protocol Subscriber<Input, Failure> : CustomCombineIdentifierConvertible {
+public protocol Subscriber<Input, Failure> : CustomCombineIdentifierConvertible, SendableMetaType {

Possibly related to Library protocols can not overtake the default isolation of the importing module · Issue #82249 · swiftlang/swift · GitHub (conforming to Subscriber should probably imply nonisolated, even for subscribers defined in modules that are MainActor-isolated by default).

4 Likes

I was able to suppress those warnings by forcing Subscribers to be Sendable:

struct MyPublisher<Output, Failure: Error>: Publisher {
  func receive<S>(
    subscriber: S
  ) where S: Subscriber & Sendable, // Sendable requirement
          Failure == S.Failure,
          Output == S.Input
  { ... }
}

This is weird, because the Publisher requirements say nothing about Sendable. The code above should not compile, because it does not match the requirements:

// No Sendable in sight
public protocol Publisher<Output, Failure> {
  func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}
public protocol Subscriber<Input, Failure> : CustomCombineIdentifierConvertible { }
public protocol CustomCombineIdentifierConvertible { }

This gets even weirder, because if I can force Subscribers to be Sendable as above, I can not force them to be SendableMetaType (which is weaker). Now this gives the expected compiler error:

// MyTests.swift:506:19: error: type 'MyPublisher<Output, Failure>' does not conform to protocol 'Publisher'
//     struct MyPublisher<Output, Failure: Error>: Publisher {
//            ^
// MyTests.swift:514:21: note: candidate has non-matching type '<Output, S> (subscriber: S) -> ()' [with Output = Output]
//         func receive<S>(
//              ^
// MyTestsMyTests.swift:506:19: note: add stubs for conformance
//     struct MyPublisher<Output, Failure: Error>: Publisher {
//            ^
// Combine.Publisher.receive:2:6: note: protocol requires function 'receive(subscriber:)' with type '<S> (subscriber: S) -> ()'
// func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input}
//      ^
struct MyPublisher<Output, Failure: Error>: Publisher {
  func receive<S>(
    subscriber: S
  ) where S: Subscriber & SendableMetaType,
          Failure == S.Failure,
          Output == S.Input
  { ... }
}

In conclusion, it looks like there's some compiler magic made for Combine and Sendable. It would be nice to extend this magic to SendableMetaType so that we can all be happy.

:folded_hands: we all want to work in good conditions.

1 Like

It won't be long until code that uses this "magic" enters the Swift Source Compatibility Suite, if not already.

I wouldn't jump to conclusions that there's some magic here to be honest, if anything it'll most likely have something to do with @preconcurrency somewhere, but will check to be sure.

Thanks @ktoso. Indeed it is hard to reach an educated conclusion, and to find one's way in the maze. I'm happy that some work on exhaustive concurrency reference has started in the TSPL.