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)
}
Use eraseToAnyPublisher() to expose an instance of AnyPublisher to the downstream subscriber, rather than this publisher's actual type. This form of type erasure preserves abstraction across API boundaries, such as different modules.
AnyPublisher is a concrete implementation of Publisher that has no significant properties of its own, and passes through elements and completion values from its upstream publisher. Use AnyPublisher to wrap a publisher whose type has details you don't want to expose across API boundaries, such as different modules.
Overview. The Combine framework provides a declarative Swift API for processing values over time. These values can represent many kinds of asynchronous events. Combine declares publishers to expose values that can change over time, and subscribers to receive those values from the publishers.
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 } }
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.
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() }
}
Publisher
typeYou 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)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With