Is there any advantage in using AnyPublisher with Swift 5.7?

Hello,

I am playing around with swift 5.7 and I am curious, since we can now do any Publisher, is there any advantage of using eraseToAnyPublisher and AnyPublisher type at all?

Huh – I didn't know that was a thing. Hope we'll see it for AsyncSequence soon, too.

Regarding your question: I don't think so, no. If anything, you may get some slight performance benefit, especially if you also use some Publisher where you can.

yeah I was thinking the same, but wanted to check if there was a "catch" before I start changing code everywhere :slight_smile:

Essentially, with any Publisher what was previously implemented at the library level – in the form of a Type Eraser – is now done at the language level. That type erasure brings some small performance overhead either way – no idea if the language level one is better or worse, but I'd be very surprised if worse. With some Publisher, the types are actually swapped out for the fully qualified type names at compile time – so there's no runtime overhead.

4 Likes

There is some restriction to some, it cannot be a return type from a Protocol requirement which is usually the case where I used to use AnyPublisher, as part of public API.
Still, using any feels more natural than the wrapper type erased type.

1 Like

Yeah, that's where I found myself reaching for AnyPublisher a lot, too. But there's also a bunch of places where I accepted AnyPublisher as a method parameter. Those should all be replaceable with some Publisher now. (Although not 100% sure about protocols – would work too I'd imagine).

1 Like

We found that any Publisher<Output, Failure> is not quite usable with Combine as designed today. Because Combine's operators return concrete types with generics holding onto "upstream" publishers, most operations are not callable directly on the any existential, since the existential does not actually conform to Publisher.

Basically:

let p: any Publisher<Int, Never> = Just(42)
p.map { $0 + 1 } 🛑

:stop_sign: Member 'map' cannot be used on value of type 'any Publisher<Int, Never>'; consider using a generic constraint instead

This may be a limitation of opening existentials that will be fixed in the future, or it may be a limitation of Combine's design.

To chain additional operations onto an any Publisher you must write extra code to first open the existential to some Publisher<Output, Failure>. And because of this, I imagine AnyPublisher will be the more convenient tool to reach for for some time!

That said, as @tcldr mentioned, if you can use some Publisher, you should be just fine...but there are many times where any Publisher/AnyPublisher are required, like when different branches of a function produce different publisher types. In these cases AnyPublisher may be more convenient to use.

23 Likes

Oh dear. That's a blocker. Good to know, thank you.

You can still return some Publisher in these cases, as long as you eraseToAnyPublisher on all branches. That way the concrete type is AnyPublisher.

Swift is in this weird state right now where the language implements type erasure in some cases, but in other cases you have to resort to manual type erasure. Yet the two forms of type erasure can be combined. :sweat_smile:

4 Likes

That sounds so counter intuitive :expressionless:
I knew there was some catch (pun unintended)

1 Like

At least now the errors "P doesn't conform to P" are the now slightly more understandable, "any P does not conform to P." Maybe allowing any P to conform to P will become possible in the future, but will require a lot of compiler code gen. :sweat_smile:

Unfortunately, it’s not possible to do this in the general case. The classic example is Equatable. Equatable.== is only defined on Self; it is meaningless to ask whether two unrelated types are equal. But if the singular any Equatable type itself conformed to Equatable, you could do something like (42 as any Equatable) == ("Hello" as any Equatable), and the compiler would not be able to synthesize a meaningful implementation of (any Equatable).==.

2 Likes

I'm not sure Equatable is the best example—as the linked comment notes, there's a straightforward and sound implementation: two any Equatable values are equal if they are of the same type and the underlying == implementation for that type returns true. The standard library already implements this for AnyHashable (which is of course Equatable).

A better example (IMO) is a protocol whose corresponding existential type fundamentally cannot fulfill its own static guarantees. For example:

protocol P {
  associatedtype A
  func f() -> A
  func g(_: A)
}

Here, for any P to conform to P soundly, we need to pick a type for A. If P only required f, then could pick the supertype of all possible values for A, i.e., Any. If g were the only requirement, we could pick the subtype of all possible values for A (which doesn't really exist in Swift, but philosophically would be Never. But with both requirements there's no choice for (any P).A that can statically fulfill the protocol requirements. If we try to use a value of type any P from a generic context, we would have to fail at runtime for some values.

5 Likes

If one could extend any P, would parameterized extensions allow someone to write such a conformance when A is known? e.g.

extension <T> any P : P where A == T { }

I'm not sure I totally understand what this would mean—I believe that with parameterized extensions the generic parameter is meant to be something that gets used in the type being extended.

With primary associated types and constrained existentials you might be able to do something like:

extension<T> any P<T>: P {
  typealias A = T
  ...
}

Is that what you were imagining? I'm not certain this would be theoretically sound, or even whether constrained existentials would be able to conform differently for different constraints like this.

Yes, that's basically what I was thinking of.

AnyHashable, being a concrete type in and of itself, is required to define the result of comparing any two AnyHashable values for equality. One can argue that this behavior is also reasonable tor any Equatable, but you can’t generalize that logic to all protocol requirements involving the Self type.

2 Likes

Ah yes, I see—I agree there's absolutely no reasonable general rule for the compiler to synthesize such a conformance even for existentials that could soundly conform to their corresponding protocols, given the right implementation.

2 Likes

Wasn’t one of the points of eraseToAnyPublisher to hide the real type?

x as? UnderlyingPublisher works but x.eraseToAnyPublisher() as? UnderlyingPublisher does not.

With any Publisher, the cast would succeed, wouldn’t it?

This is such a disappointment, I wanted to ditch all the manual erasure noise

Maybe in AsyncSequence we will be able to get a good api?

1 Like