28

I'm just learning how to use Combine. I have experience with Rx (RxSwift and RxJava) and I'm noticing that it's quite similar.

However, one thing that is quite different (and kind of annoying) is that the Publisher protocol doesn't use generics for its Output and Failure types; it uses associated types instead.

What this means is that I can't specify a polymorphic Publisher type (such as Publisher<Int, Error>) and simply return any type that conforms to Publisher with those types. I need to use AnyPublisher<Int, Error> instead, and I am forced to include eraseToAnyPublisher() all over the place.

If this is the only option, then I'll put up with it. However, I also recently learned about opaque types in Swift, and I'm wondering if I might be able to use them to get around this.

Is there a way for me to have, say, a function that returns some Publisher and use specific types for Output and Failure?

This seems like a perfect case for opaque types, but I can't figure out if there's a way for me to both use an opaque type and specify the associated types.

I'm picturing something like this:

func createPublisher() -> some Publisher where Output = Int, Failure = Error {
    return Just(1)
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
jchitel
  • 2,999
  • 4
  • 36
  • 49
  • "it uses associated types instead" That _is_ a generic. That's what a generic protocol _is_. – matt May 03 '20 at 17:36
  • 1
    Yet “Unlikely” section of the the [generics manifesto](https://github.com/apple/swift/blob/master/docs/GenericsManifesto.md#generic-protocols) includes a subsection titled “Generic protocols”, implying that Swift does **not** have generic protocols, and that having an associated type doesn't make a protocol “generic” (in the sense that struct, enum, etc. can be generic). – rob mayoff May 03 '20 at 18:16

4 Answers4

39

UPDATE

The future is now! If you use Xcode 14.0 beta 2 or later, you can write your createPublisher function like this, and it should back-deploy to all systems that support Combine:

func createPublisher() -> some Publisher<Int, Never> {
    return Just(1)
}

Furthermore, you can use an opaque parameter type to make a function take a generic Publisher argument with less syntax and without resorting to an existential or to AnyPublisher:

func use(_ p: some Publisher<Int, Never>) { }

// means exactly the same as this:

func use<P: Publisher>(_ p: P) where P.Output == Int, P.Failure == Never { }

// and is generally more efficient than either of these:

func use(_ p: any Publisher<Int, Never>) { }
func use(_ p: AnyPublisher<Int, Never>) { }

ORIGINAL

Swift, as of this writing, doesn't have the feature you want. Joe Groff specifically describes what is missing in the section titled “Type-level abstraction is missing for function returns” of his “Improving the UI of generics” document:

However, it's common to want to abstract a return type chosen by the implementation from the caller. For instance, a function may produce a collection, but not want to reveal the details of exactly what kind of collection it is. This may be because the implementer wants to reserve the right to change the collection type in future versions, or because the implementation uses composed lazy transforms and doesn't want to expose a long, brittle, confusing return type in its interface. At first, one might try to use an existential in this situation:

func evenValues<C: Collection>(in collection: C) -> Collection where C.Element == Int {
  return collection.lazy.filter { $0 % 2 == 0 }
}

but Swift will tell you today that Collection can only be used as a generic constraint, leading someone to naturally try this instead:

func evenValues<C: Collection, Output: Collection>(in collection: C) -> Output
  where C.Element == Int, Output.Element == Int
{  
  return collection.lazy.filter { $0 % 2 == 0 }
}

but this doesn't work either, because as noted above, the Output generic argument is chosen by the caller—this function signature is claiming to be able to return any kind of collection the caller asks for, instead of one specific kind of collection used by the implementation.

It's possible that the opaque return type syntax (some Publisher) will be extended to support this use someday.

You have three options today. To understand them, let's consider a concrete example. Let's say you want to fetch a text list of integers, one per line, from a URL, and publish each integer as a separate output:

return dataTaskPublisher(for: url)
    .mapError { $0 as Error }
    .flatMap { data, response in
        (response as? HTTPURLResponse)?.statusCode == 200
            ? Result.success(data).publisher
            : Result.failure(URLError(.resourceUnavailable)).publisher
    }
    .compactMap { String(data: $0, encoding: .utf8) }
    .map { data in
        data
            .split(separator: "\n")
            .compactMap { Int($0) }
    }
    .flatMap { $0.publisher.mapError { $0 as Error } }

Option 1: Spell out the return type

You can use the full, complex return type. It looks like this:

extension URLSession {
    func ints(from url: URL) -> Publishers.FlatMap<
        Publishers.MapError<
            Publishers.Sequence<[Int], Never>,
            Error
        >,
        Publishers.CompactMap<
            Publishers.FlatMap<
                Result<Data, Error>.Publisher,
                Publishers.MapError<
                    URLSession.DataTaskPublisher,
                    Error
                >
            >,
            [Int]
        >
    > {
        return dataTaskPublisher(for: url)
            ... blah blah blah ...
            .flatMap { $0.publisher.mapError { $0 as Error } }
    }
}

I didn't figure out the return type myself. I set the return type to Int and then the compiler told me that Int is not the correct return type, and the error message included the correct return type. This is not pretty, and if you change the implementation you'll have to figure out the new return type.

Option 2: Use AnyPublisher

Add .eraseToAnyPublisher() to the end of the publisher:

extension URLSession {
    func ints(from url: URL) -> AnyPublisher<Int, Error> {
        return dataTaskPublisher(for: url)
            ... blah blah blah ...
            .flatMap { $0.publisher.mapError { $0 as Error } }
            .eraseToAnyPublisher()
    }
}

This is the common and easy solution, and usually what you want. If you don't like spelling out eraseToAnyPublisher, you can write your own Publisher extension to do it with a shorter name, like this:

extension Publisher {
    var typeErased: AnyPublisher<Output, Failure> { eraseToAnyPublisher() }
}

Option 3: Write your own Publisher type

You can wrap up your publisher in its own type. Your type's receive(subscriber:) constructs the “real” publisher and then passes the subscriber to it, like this:

extension URLSession {
    func ints(from url: URL) -> IntListPublisher {
        return .init(session: self, url: url)
    }
}

struct IntListPublisher: Publisher {
    typealias Output = Int
    typealias Failure = Error

    let session: URLSession
    let url: URL

    func receive<S: Subscriber>(subscriber: S) where
        S.Failure == Self.Failure, S.Input == Self.Output
    {
        session.dataTaskPublisher(for: url)
            .flatMap { $0.publisher.mapError { $0 as Error } }
            ... blah blah blah ...
            .subscribe(subscriber)
    }
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • 1
    Thanks for writing it out, this explanation was exactly what I was looking for. – jchitel May 03 '20 at 20:38
  • It is worth noting that in option 1, changing the implementation will almost always change the return type. Thus, if you are vending some API, with option 1, you are essentially guaranteeing that your implementation will stay the same until the next breaking release. This is not the case with the other two solutions. – deaton.dg May 27 '21 at 18:03
  • Future not quite here yet. The following works... let c3 = Environment.live.messages.sink { _ in } But the following does not... let c4 = Environment.live.messages.map { $0 + "!" }.sink { _ in } Doesn't help a lot to provide publishers on which one can't use operators. – Michael Long Sep 11 '22 at 19:35
  • @MichaelLong You're right. I've removed the discussion of `any Publisher`. – rob mayoff Oct 03 '22 at 21:40
2

With opaque return the type is defined by what is exactly returned from closure, so you can use just

func createPublisher() -> some Publisher {
    return Just(1)
}

let cancellable = createPublisher()
   .print()
   .sink(receiveCompletion: { _ in
       print(">> done")
   }) { value in
       print(">> \(value)")
   }

// ... all other code here

and it works. Tested with Xcode 11.4.

demo

Asperi
  • 228,894
  • 20
  • 464
  • 690
  • What I am asking is if you can treat the return value as a publisher with `Output = Int`, or if you can only treat it as a generic publisher. – jchitel May 03 '20 at 00:33
  • 1
    It works but is relatively useless. Try doing `\(value + 1`) and you'll get an error because your publisher's output type isn't visible. You can fix that with `some Publisher`, which now works... as long as `createPublisher` only attempts to return a single type. – Michael Long Sep 11 '22 at 19:45
1

I had no luck with some Publisher (annoying restriction).

One option is to use AnyPublisher:

func a() -> AnyPublisher<(a: Int, b: String), Never> {
    return Just((a: 1, b: "two")).eraseToAnyPublisher()
}

func b() -> AnyPublisher<String, Never> {
    return a().map(\.b).eraseToAnyPublisher()
}

a().sink(receiveValue: {
    let x = $0 // (a: 1, b: "two)
})

b().sink(receiveValue: {
    let x = $0 // "two"
})

Alternatively, the "Apple way" (what they use in the standard library) seems to be type aliases (or wrapper structs):

enum PublisherUtils {
    typealias A = Just<(a: Int, b: String)>
    typealias B = Publishers.MapKeyPath<A, String>
    // or implement a simple wrapper struct like what Combine does
}

func a() -> PublisherUtils.A {
    return Just((a: 1, b: "two"))
}

func b() -> PublisherUtils.B {
    return a().map(\.b)
}

a().sink(receiveValue: {
    let x = $0 // (a: 1, b: "two)
})

b().sink(receiveValue: {
    let x = $0 // "two"
})

This is the purpose of the Publishers namespace in the Combine framework.

Structs are more opaque than type aliases. Type aliases can result in error messages like Cannot convert Utils.MyTypeAlias (aka 'TheLongUnderlyingTypeOf') to expected type ABC, so the closest you can get to proper opaque types is probably to use a struct, which is essentially what the AnyPublisher is.

Potassium Ion
  • 2,075
  • 1
  • 22
  • 39
0

A really simple trick that helped us reduce most of the verbosity was basically to create a file with the following :

// MARK: Dependencies Extension
typealias SafePublisher<T> = AnyPublisher<T, Never>
typealias IntSafePublisher = SafePublisher<Int>
typealias Int64SafePublisher = SafePublisher<Int64>
typealias Float64SafePublisher = SafePublisher<Float>

// Then for value types, something like

protocol SafePublishing {
    associatedtype SafePublisherType
}

extension SafePublishing {
    typealias SafePublisherType = SafePublisher<Self>
}

struct APublishedType: SafePublishing {
}

// And Magic

var myPublisher: APublishedType.SafePublisherType?

func takeInPublishers(myPubisherVar: APublishedType.SafePublisherType) {
    myPubisherVar
        .sink { myStructType in
            // DO SOMETHING
        }
        .cancel()
}
Peter Suwara
  • 781
  • 10
  • 16