Thomas Visser Colorless green ideas sleep Swiftly

Why Combine has so many Publisher types

A question on Twitter prompted me to think about why Combine has so many implementations of the Publisher protocol. There’s one for each operator, at least. The documentation of the Publishers enum lists a whopping 106 implementations. The map operator returns an instance of Publishers.Map, filter returns an instance of Publishers.Filter, and so on.

All of these derived publisher types refer to their upstream with a generic parameter. Take for example the declaration of Publishers.Map:

struct Map<Upstream, Output>: Publisher where Upstream : Publisher

Other operators follow the same pattern. This means that if you create a publisher from an array that you subsequently map and filter, you end up with an instance of Publishers.Filter<Publishers.Map<Publishers.Sequence<[Int], Never>, Boolean>, Boolean>. Except that you don’t. What you’ll find is that mapping and then filtering a Publishers.Sequence will result in a plain Publishers.Sequence.

Combine contains overrides for map and filter (and many other operators) for Publishers.Sequence. Instead of returning a derived publisher that subscribes to the upstream, it creates a new Publishers.Sequence that contains the mapped elements. It is able to do so, because in this special case it can access the stored sequence in the upstream and use them directly. I imagine the map operator for Publishers.Sequence to look something like this:

extension Publishers.Sequence {
    func map<T>(_ transform: (Elements.Element) -> T) -> Publishers.Sequence<[T], Failure> {
        return Publishers.Sequence(sequence: self.sequence.map(transform))
    }
}

The result is a single publisher, instead of a chain of three. I’d call this “fancy compile time chain optimization magic”, but people smarter than me have referred to this as “operator fusion”. This technique is used throughout Combine, not just for Publishers.Sequence.

Having this many concrete publisher types is only feasible because of Swift 5.1’s opaque return type. But even then, types can get in the way. For those cases there’s AnyPublisher, which wraps any publisher to erase its specific type. But now you know that it can close the door on some pretty cool optimizations as well.