Opaque type being returned as an existential?

I’m trying to wrap my head around opaque/existential types and ran into this. Am I thinking about it wrong (most likely) or is this a bug in swift?

If there’s a protocol extension that adds a prop/func returning some ___, I’d expect everywhere that’s calling it to be able to get that opaque type back. It seems like if it’s accessed through an existential, then the return type also becomes existential which doesn’t seem right to me.

// Simplified implementation of some of the Combine types
protocol _Publisher {}
struct _Empty: _Publisher {}

protocol FooProtocol {}
extension FooProtocol {
    var publisher: some _Publisher { _Empty() }
}
struct Foo: FooProtocol {}

// Works
let f = Foo()
let fPub: some _Publisher = f.publisher

// Doesn't work
let fp: FooProtocol = Foo()
let fpPub: some _Publisher = fp.publisher
// error: type 'any _Publisher' cannot conform to '_Publisher'

Then I was shown that it works if passed to a function expecting an existential type.

func open(_ p: some _Publisher) {}
open(fp.publisher)

Which surprised me cause I would have thought that function call would be equivalent to this, which doesn't work.

let fpPub: some _Publisher = fp.publisher
open(fpPub)

This is SE-0309: Unlock existentials for all protocols and SE-0352: Implicitly Opened Existentials.

As it says in the latter document, "implicit opening of existential arguments only occurs in calls, so that its effects can be type-erased at the end of the call." Which is why the following does work, and is equivalent to open(fp.publisher):

let fpPub: any _Publisher = fp.publisher
open(fpPub)

As the first proposal describes, Swift now allows you to use any protocol as a type, even if it has Self or associated type requirements, but those members are also subject to covariant type erasure in the same way that return values are when calling functions with implicitly opened existentials. That is to say, Swift no longer stops you from using any FooProtocol as a type, and it even allows you to access (any FooProtocol).publisher, which due to covariant type erasure is a property of type any _Publisher.

You cannot give fpPub a static type of some _Publisher because, as the diagnostic states, the type any _Publisher doesn't conform to the protocol _Publisher—with a few exceptions not relevant here, existential types do not (and in the general case cannot) conform to any protocols.

You cannot "get an opaque type back" when you provide an existential argument to a function such as open or access a property such as publisher on a value of existential type, because an existential type erases the underlying concrete type from the static type system—as the first proposal puts it, "an existential value is akin to a box that can hold any value of any conforming type dynamically at any point in time"—while an opaque type guarantees that it is backed by a unique underlying concrete type.

3 Likes