Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Generics Manifesto] Existential subtyping as generic constraint #33552

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

nickolas-pohilets
Copy link
Contributor

@nickolas-pohilets nickolas-pohilets commented Aug 19, 2020

Updated 'Generalized supertype constraints' section of GenericsManifest based on https://forums.swift.org/t/existential-subtyping-as-generic-constraint/31584.

I think this addresses an important pain point when using protocols and generics together. Maybe this even should be separated from 'Generalized supertype constraints' and placed in 'Removing unnecessary restrictions' section, rather than 'Minor extensions' section.

Note: This is a copy from #28741, because GitHub does not allow to edit source branch of the pull-request.

@theblixguy theblixguy changed the title Existential subtyping as generic constraint [Generics Manifesto] Existential subtyping as generic constraint Aug 19, 2020
@CodaFi
Copy link
Contributor

CodaFi commented Aug 21, 2020

I’m still confused about what this clarification is trying to accomplish. It seems like you want the ability for certain protocols to conform to themselves. That in hand, you don’t need a new kind of constraint here. Seeing G<T: any P>, my immediate thought is “T is an opaque type sealed behind an interface with requirements at least P”. We already have syntax for that, it’s some P. Perhaps I’m missing something here, but this section is about the ability to express bounds that depend on the substitutions made in a given protocol conformance - the ability to seal an interface’s requirements behind another requirement’s interface.

@nickolas-pohilets
Copy link
Contributor Author

Currently some P is only allowed for function return types, but if it would be allowed for inputs as well, func f(x: some P) would be equivalent for func f<T: P>(x: T). That's a nice syntax sugar, but it does not introduce new semantics.
I'm proposing to introduce new semantics, which is not covered by the some P.

Consider the following example:

protocol P {}
struct S: P {}
let x: S = S()
let y: P = S()
func f(_ a: P) {}
f(x) // OK
f(y) // OK
func g<T: ???>(_ a: T) { f(a) }

Types S and P have something in common. Values of both types can be passed to function f. The question is how can we capture this common property of both types in the signature of the function g?

Let's ask ourselves how types S and P are related with the type of the argument a of the function f.
This relationship is not a protocol conformance, because type P does not conform to protocol P. What we have here is a different kind of relationship.

I'm stating that this relationship is a subtyping. Every type is a subtype of itself. So type P is a subtype of type P. And type S is also a subtype of type P, because type S conforms to protocol P.

So, for the g we need a way to write: func g<T is a subtype of P>(_ a: T).

Next, I'm making an observation that there is already a subtyping constraint in the language.

If I have class C {}, I can write func h<T: C>(_ a: T) and I can call h with T = C. That's a subtyping.

So we already have:

  • T: C - subtyping constraint for base class C.
  • T: P - conformance constraint for protocol P.

That means that : as a relationship operator is already overloaded to mean either subtyping or protocol conformance depending on the right hand side. If right hand side is a type - that's subtyping. If right hand side is a protocol - that's conformance. I'm suggesting to keep this existing overloading as is, and instead introduce a syntax to disambiguate between protocols as types and protocols as protocols.

If there is an ambiguity, I'm suggesting P to mean "protocol P as a set of requirements for a type" and any P as "type of an existential container holding values of types conforming to P`.

Note that in many places, there is no such ambiguity. For example, in func f(_ a: P), P must be a type. So, strictly speaking signature of the function should look like func f(_ a: any P). But for backwards compatibility I'm not suggesting any changes here.

The syntax of any P is not my invention. It was originally suggested in the context of extensions - another example of ambiguity between protocols as types and protocols as protocols:

  • extension P - defines members available on all the types that conform to P - current behaviour.
  • extension (any P) - defined members available on existential container for P.

I think it popped up in the discussion of eliminating AnyHashable, by using any P syntax to declare protocol conformances for existential containers:

extension (any Hashable): Hashable {
    ...
}

But using any P for extensions is out of scope of my suggestion.

@nickolas-pohilets
Copy link
Contributor Author

Also, note that if protocol has static members, <T: P> allows to use them on T, while <T: any P> does not.

Small summary:

<T: P> <T: any P>
Access instance members Yes Yes
Convert value of T to existential container Yes Yes
Pass P as T No Yes
Access static members Yes No

@shahmishal
Copy link
Member

Please update the base branch to main by Oct 5th otherwise the pull request will be closed automatically.

  • How to change the base branch: (Link)
  • More detail about the branch update: (Link)

@nickolas-pohilets nickolas-pohilets changed the base branch from master to main October 5, 2020 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants