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)
}
}