Introduce Any<P> as a better way of writing existentials

I've tried to say this before, but hopefully this post is clearer.

I definitely support reconsidering the syntax of existential types, but I would not like to make any incremental changes to existentials without holistically considering where we'll end up. In particular, there are at least three possibilities we should consider that will affect our syntax choices:

  1. “Partial” protocol types, i.e. existentials whose protocol declares requirements not available on an instance of the existential type. It seems likely that the current “associated type or self parameter” restriction on existentials will be lifted at some point, and when that happens, we'll be faced with situations like this:

    protocol Equatable { 
      static func == (Self, Self) -> Bool
    }
    
    func test(x: Equatable, y: Equatable) -> Bool {
      x == y // ERROR!
    }
    

    where some part of the API of the declared API of the protocol in question (in this case, the whole API!) is completely unavailable on instances of that protocol type. I don't think it's possible to overstate how weird it is going to be for ordinary users that they can declare a type like Equatable and then not be able to use the declared API at all. And in fact I think it will be more damaging when only a small fraction of the API is missing on the existential because users who don't understand generics yet will happily code their way down the “easy” path of using existentials until they find themselves blocked by partiality, when it would have been more appropriate to use the protocol as a generic constraint.

  2. Constrained existentials, e.g. a Collection whose Elements have type Int. It seems obvious to me that we're going to get this feature someday, and that it will involve a where clause, e.g.

    func first(x: Collection where Element == Int) -> Int? { x.first }
    
  3. Existentials that conform to their corresponding protocol. Today, no protocol type (existential) is self-conforming, but it would be very useful (and avoid a tedious forwarding layer in many cases) if a protocol could be declared to be self-conforming, e.g.,

    public protocol Drawable: Self { func draw() }
    

    The explicit statement “: Self” is important, because self-conformance is a guarantee to clients—it determines whether they can use the existential as a generic parameter constrained to that protocol—and adding certain kinds of requirements (init and static members, and anything that makes the protocol “partial”—see 1. above) necessarily make self-conformance impossible. We wouldn't want maintainers of a self-conforming protocol to inadvertently break that guarantee.

I have the following goals for a new syntax:

  • When naming an existential type, possible partiality should be evident. For me, “Any<Equatable>” doesn't meet that bar.
  • The syntax should accommodate constraints without being overly verbose. Adding Any<…> doesn't automatically lead us to an obvious place to add constraints, and constraints are already going to add where.

Therefore, I propose we chart a path to this future state:

  • Self-conforming existentials are explicitly declared so, per Drawable above. Since they are never partial, you can name them without a “where” clause.
  • Partial existentials are spelled with a where clause. An unconstrained partial existential uses the empty where condition, “_”. For me, “Equatable where _” clearly indicates that some part of the declared API may be missing because some constraints may be missing.

If we made it an error to use a partial existential without a where clause, we'd need syntax for explicitly declaring non-self-conforming-but-non-partial protocols, so protocol authors could avoid inadvertently breaking client source by adding an init or static requirement. Therefore, IMO using a partial existential type without a “where” clause should generate a warning and a fixit: ”partial protocol type should be spelled with an empty where clause; do you want to add it?” That's a nice conclusion because it's probably the same warning we'd want as part of transitioning to the new scheme. I also like that it introduces the searchable term "partial protocol type" that we can clearly define.

Given all this, I think the near-term steps are:

  1. Add the syntax P where _ as a synonym for the existential type P.
  2. At some point appropriate to the release process, add the warning/fixit described above.
9 Likes