Improving the UI of generics

I agree with you, but the problem in my eyes is that it‘s too late to change the default. Nesting today does capture outer generic types (and potentially associated types in protocols). The right default might have been to disable that capture and allow it when explicitly requested by the developer through some attribute or keyword. From my point of view we can now just do the opposite where the most common case will be for protocols in nested in generic types. If you do so, the compiler will raise an issue and let you know that the attribute is required as swift does not support GPs.

Isn‘t that a good compromise to finally allow nesting protocols?

The direction I have in mind is based on introducing existential for generic types (i.e. any Array). Here's a sketch of the space of extensions I envision as possible. We may not necessarily want to support everything I describe here, but it presents a holistic vision of what the type system might look like if we lifted as many limitations as possible.

// extends existential and conforming types
//
// does not support protocol conformances (because it extends conforming types)
// does not support nested types or protocols (because it extends conforming types)
extension Protocol {}

// extends the metatypes of existential and confoming types
//
// does not support protocol conformances (because it extends conforming types)
// does not support nested types or protocols (because it extends conforming types)
extension Protocol.Type {}

// extends the protocol existential only
//
// supports protocol conformances
// supports nested types and protocols - i.e. Protocol.HelperType
extension any Protocol {}

// extends the protocol existential metatype only
// note: there is no distinction between static and instance members here, metatypes are their own metatype
//       this should be recognized by the language when validating protocol conformances
//
// supports protocol conformances
// supports nested types and protocols - i.e. Protocol.Type.HelperType
extension any Protocol.Type {}

// extends conforming types only (including `any Protocol` if it conforms)
//
// i.e. when `any Protocol: Protocol` then `extension Protocol` and `extension some Protocol` are equivalent
// does not support protocol conformances (because it extends conforming types)
// does not support nested types or protocols (because it extends conforming types)
extension some Protocol {}

// extends metatypes of conforming types only (including `any Protocol` if it conforms)
// i.e. when `any Protocol: Protocol` then `extension Protocol.Type` and `extension some Protocol.Type` are equivalent
//
// does not support protocol conformances (because it extends conforming types)
// does not support nested types or protocols (because it extends conforming types)
extension some Protocol.Type {}

// extends the protocol metatype itself
// note: there is no distinction between static and instance members here, metatypes are their own metatype
//       this should be recognized by the language when validating protocol conformances
//
// supports protocol conformances and nested types
// may not reference `Self` or associated types
extension Protocol.Protocol {}

// extends concrete `Array` types as well a hypothetical `any Array` existential
//
// as today, supports protocol conformances and nested types
// if it supports nested protocols in generic types, there would be an implied associated type
// which is implicitly bound in any uses of the protocol in the implementation of this type
extension Array {}

// extends the hypothetical `any Array` existential
//
// supports protocol conformances
// supports nested types and protocols (i.e. Array.HelperType, etc)
// note: because this is extending an existential it does not have a generic context,
//       therefore allowing `Array` to be used directly as a namespace for nested types, protocols, etc
//
// note: this syntax could be supported for non-generic types, but the "existenital" would be equivalent to Self
extension any Array {}

// extends the Array metatype
// note: there is no distinction between static and instance members here, metatypes are their own metatype
//       this should be recognized by the language when validating protocol conformances
//
// supports protocol conformances and nested types
//
// if it supports nested protocols in generic metatypes, there would be an implied associated type
// which is implicitly bound in any uses of the protocol in the implementation of this type
//
// note: static requirements may be fulfilled by static members in the type declaration or 
//       extensions of the concrete type
// note: all members of a metatype extension may fulfill static protocol requirements for 
//       underlying concrete type conformances
extension Array.Type {}

// extends the metatype of the `any Array` existential
// note: there is no distinction between static and instance members here, metatypes are their own metatype
//       this should be recognized by the language when validating protocol conformances
//
// supports protocol conformances of the `any Array` existential metatype
// suppoerts nested types and protocols (i.e. Array.Type.HelperType)
//
// note: this syntax could be supported for non-generic types, but the "existenital" would be equivalent to Self.Type
extension any Array.Type {}

With the ability for metatypes to conform to protocols, we would also be able to constrain them:

func foo<T>() where T.Type: MyProtocol

One interesting thing about this approach is that it is possible to view every type having an associated existential:

  • In the trivial case, an existential is itself an existential
  • Protocol metatypes are associated with the existential of the protocol they define
  • Generic types and metatypes now have an associated existential that erases their type arguments
  • Non-generic concrete types and metatypes are their own "existential"

We could allow access to the associated existential in the type system just as we allow access to a type's metatype without requiring generic constraints. For example:

func foo<T>() -> T.Existential where T.Existential: MyProtocol

The associated existential provides the tag we need to improve our ability to encode higher-kinded types in Swift. There are probably other interesting use cases for it as well.

Interesting, but keep in mind that Protocol.Type is not obtainable in todays swift, you will always get Protocol.Protocol.

Hmm, that is not what I am seeing in a playground:

print(P.Protocol.self) // prints "P.Protocol"
print(P.Type.self) // prints "P.Type"
print(P.self) // prints "P"
print(P.Protocol.self) // this one is new to me that it compiles.

Here is what I mean, I asked Joe long long time ago if there is any good workaround to this problem but I doubt the situation changed since then:

protocol P {}

func whatAmI<T>(_ t: T.Type) {
  print(type(of: t))
}

whatAmI(P.self)              // P.Protocol
whatAmI(P.Type.self)         // P.Type.Protocol
whatAmI(P.Protocol.self)     // P.Protocol.Type
whatAmI(Any.Type.self)       // Any.Type.Protocol
whatAmI(AnyObject.Type.self) // AnyObject.Type.Protocol

Another thing that making existential types explicit would hopefully enable is clearer syntax for existential metatypes, since instead of the .Type/.Protocol weirdness we could say that any (P.Type) is the type of all types conforming to P, and (any P).Type is the type of the existential itself.

1 Like

Wouldn't that be the type of the existential's metatype?

That would be an improvement. I still hope we could move .Type into something generic looking like Type<T> and free up the .Type namespace for custom nested types. This bugged me so many types. I'm sick of writing .Kind types. :smiley:


Actually this fits well with the proposal I was trying to push many times. There we had

  • Type<T>
  • AnyType<T>

Now we could make it even easier into:

  • Type<T>
  • any Type<T>
2 Likes

@anandabits one more example:

P.self is P.Type     // false
P.self is P.Protocol // true

It would be cool to reclaim .Type if we could. My team has a bunch of use cases where .Type is by far the best name for a model. We work around that by skipping nesting and just using a Type suffix. This is unfortunate because we use nesting quite a bit and this breaks the conventions we otherwise follow.

However, I wonder how this approach could support constraints on the metatype (or the "associated existential" I described). The syntax works fine:

func foo<T>() where Metatype<T>: MyProtocol

But the semantics don't really fit with Swift. This syntax implies that Metatype can only have one conformance to any given protocol. We want each metatype to be able to have an independent conformance. Semantically they seem to fit best in Swift's generics model as associated types.

My brain already fades for today, I'll think more on that tomorrow, but if I got you right then you want this constraint:

func foo<T>() where any Metatype<T>: MyProtocol

I still need to think through how any would apply to the design of Type<T> (or whatever we would call it). IIRC @beccadax told me back then that a metatype is like a struct or class, that's why we dropped the Metatype name.

I want to be able to use the metatype and the associated existential in the same ways we can use associated types, the difference being that they are available on all generic types including those without constraints.

In our proposal we would have described such constraint as:

func foo<T>() where AnyType<T>: AnyType<MyProtocol>

What this saying is: "We can use any type T where it's existential metatype is a sub-type of the existential metatype for MyProtocol."

This basically describes indirectly the relationship between T and MyProtocol through their metatypes.

If the translation here is symmetrical then it would be.

func foo<T>() where any Type<T>: any Type<MyProtocol>

Since with any existential types could potentially conform to protocols, the following would mean something different:

func foo<T>() where any Type<T>: MyProtocol

If we could do this, it will fill a huge hole in the constraint system that for example allow you to describe constraints for associated types that await a concrete protocol and not its existential.

I wonder if we should build on the any and some prefix syntax and use a prefix like meta (as a strawman):

  • any T is the associated existential
  • meta T is the metatype of T
  • any meta T is the metatype of the associated existential
  • any P is the existential for the protocol
  • meta P is the protocol metatype
  • any meta P is the metatype of the protocol existential

I realized after posting it that my approach to solving the nesting issue depended on the fact that the name of the existential type matches the name of the protocol (or the unbound name of a generic type for concrete type existentials). This is what made the P.NestedType or Array.NestedType syntax work.

With this prefix approach to existentials that would become (any P).NestedType or (any Array).NestedType which is not what we want when it comes to using types and protocols as namespaces. Maybe one option would be to use some on concrete types:

// We don't know what the type of `Element` is so any static members put in this
// extension are available using `Array.NestedType`, `Array.constant`, etc syntax.
// (as well as being available as `Array<Int>.NestedType`, etc
extension some Array {}

This isn't what I want though. I want to be able to say (in today's spelling) that T.Type: P and proceed to use T.self anywhere a P constraint is present.

Sure that should be possible if existentials will indeed conform to protocols, then I think it would be a great enhancement to also allow metatypes and their existentials to conform to protocols.

I actually like this direction as it avoids the need for a pseudo generic type. It's a little verbose, but I like the trade-off here. Since the syntax itself overlap we can say that we only need the top three forms, but the super-type relations differ as they preserve todays relations.

Lets say T is a non-protocol type and P is a protocol like in your example already.

any T : any T
meta T : any meta T
any meta T : any meta T

any P : any Any
meta P : any meta Any
any meta P : any meta P

IIRC this exactly the same relationship we had in our proposal. (I hope you understand this super-type notation.)

func type<T>(of value: T) -> any meta T

func subtype<T>(of value: T, named name: String) -> (any meta T)?
1 Like

I can't think of a good reason metatypes couldn't fundamentally be plain old generic types. The generics syntax also makes it even clearer (to my eyes) what Meta<any P> vs. any Meta<P> would mean. It'd be difficult to make something that abstracts over existential metatypes and plain old metatypes because they aren't really structurally similar; the former is more a special case of existential types than a proper metatype.

7 Likes

How would this work? static members of a type are instance members of the metatype:

struct MyType {
    static let x = 42
    static func doIt() {}
}
let myMeta: MyType.Type = MyType.self
myMeta.x
myMeta.doIt()

Are you imagining that the static members would be implemented something like a conditional extension like this:

extension Meta where Concrete == MyType {
   let x = 42 // instance stored properties aren't normally allowed in extensions...
   func doIt() {}
}

Even if this approach works wouldn't it be limiting? Now Meta can only have a single conformance to a protocol for all concrete types. My mental model is of metatypes being a lot more first class than this. Each metatype should be able to have its own set of protocol conformances, shouldn't it?

I think you did a correct observation here, even if we had Meta<T>, it would probably be a generic keyword type similar to non-generic Any as keyword type. That implies it won‘t be a citizen of stdlib directly.

My conclusion is:

  • I hope we can move away from T.Type and reclaim .Type
  • With this we could clean up metatypes drastically.
  • We would communicate their behavior more clearly.
  • We would get a new set of constraints we could express in Swift‘s type sytem.