Opaque result types

I'd like to clarify this slightly, because the details here matter and explain why I think existentials add a lot of undesirable complexity.

Dynamically, it's true that the returned value is something that conforms to P, exactly the same way that it might be true if the declaration was func existential() -> Any. You can try to down-cast this thing to some actual P via existential() as? SomeP, for example. It goes a little further, because (today) if P has requirements, you can use those too. But statically, it's not really a P, because you don't have access to all of P's interface.

struct Y : P {}

extension P {
    func f(_ x: Self) {
        assert(type(of: x) == type(of: self)) // valid assumption
    }
}

existential().f(existential()) // error member 'f' cannot be used on value of protocol type 'P'; use a generic constraint instead

The compiler can't allow that call because you could be passing a Y to f when Self is X.

Because we don't currently support “generalized existentials” this problem mostly hides in the margins: you have to write a generic function or use Self in an extension to see it. But if we generalize existentials it will become much more glaring, because basic features of a protocol's API will be unavailable on its existentials. For example, an existential Collection of Ints can't be indexed, even though indexing is a fundamental part of the Collection API.

So, I'm sure many people will disagree with me, but personally I'm not convinced that generalized existentials are a good thing for the language overall. In contrast, opaque result types solve a lot of real problems without introducing more of this “it's P but it's not a P“ craziness.

5 Likes