Improving the UI of generics

I think that would be fatal as this makes the generic rules behave differently. I understand what you want to achieve but I would like to avoid such strange type inference. Instead I think it would be better to provide a general attribute for nested types to make them non-generic when they are nested in generic types. This would solve your issue and also other namespacing workarounds that exist today in generic context.

class Generic<T> {
  struct AlsoGeneric {} // as it captures T

  @nonGeneric
  struct NonGeneric {} // capturing generic types disabled

  @nonGeneric
  protocol Delegate: AnyObject {
    associatedtype T
  }

  weak var delegate: (any Delegate<.T == T>)?
}

Generic<Int>.AlsoGeneric != Generic<Bool>.AlsoGeneric
Generic<Int>.NonGeneric == Generic<Bool>.NonGeneric
Generic<Int>.Delegate == Generic<Bool>.Delegate

I have other ideas around namespacing more generally that I intend to write up and post eventually. I posted this suggestion for a couple reasons. It does the thing most people intuitively want using the syntax that is often requested. It may also be a way to introduce nested protocols sooner than would otherwise be possible. (One reason I haven't pitched my namespace ideas yet is that I don't think they would go anywhere in the near term anyway)

I think there's a general issue here where there's a mismatch between Swift's design and how users want to use declaration namespaces. Although Swift's nesting semantics treat nested types as also having nested generic contexts, which is part of the motivation for the restrictions on nesting protocols, types inside protocols, and so on, it's clear that a lot of the time developers just want to nest as a namespacing tool without also nesting the generic environment. It'd be interesting to explore that problem holistically.

6 Likes

I‘m looking forward to read that one day, no stress though. ;)

What do you think about an attribute that will disable the capturing outer generic/associated types? It can allow nesting protocols and nesting in protocols, we just need to require this attribute all the time as we don‘t support a feature set we talked upthread that would behave like other types nested in generic types.

Also I think it can solve the issue you once posted where you would like to share the same static member on a generic type. Here you could also disable generics and allow sharing it no matter what the generic type parameters are.

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.