Even if we make the syntax for naming type arguments more lightweight, and bring the declaration closer to the matching value's declaration, I think there's still value to not having to name things when the thing being named doesn't have much of an identity of its own. There's still cognitive overhead to having to juggle those names.
What you just said and extraction of the generic type that is not fixed on the conforming type. That last thing is a huge pain with current generics feature set. PATs provide only one fixed type on the conforming type while I have a generic and static API which requires a non-fixed generic type which PATs don't allow.
Can you give a more concrete example?
Not at the moment, as I would need to find that part of the API architecture that I wanted to design with GPs and abstract it so I can post it here, but that might take some time. I'll try to find it in the next couple days.
Rust explicitly communicates that in most use-cases the user likely want PATs, so does Swift already, but Rust does not abandon GPs right away as they are still useful, so why can't Swift do the same? Is it the implementation complexity for the compiler, or is it just a resistance to support that particular feature?
This is how I feel as well. The syntax Joe posted upthread models what is going on much better than generic syntax. This avoids the arbitrary choice of who gets to be Self
which would always be a frustrating and annoying choice to have to make.
Absolutely! I’m not suggesting otherwise, only that it would be cool to be able to lighten up the syntax required when we do need the names and that it avoids the need for type(of:)
and return
.
I still think the Protocol<.AssociatedType == T>
syntax is desirable. It makes the most sense for existentials and if we do it there we should be consistent and also support it in opaque types.
I'm not suggesting we abandon the functionality, I'm saying that I don't (personally!) think that "generic protocols" are the best way of exposing multi-parameter protocol functionality.
If we go in this direction maybe it helps solve the problem of nesting protocols in types. Generic parameters of the type would become an associated type in the protocol. Uses of the protocol in the body of the type would have an implied same-type constraint:
class MyViewController<Model>: UIViewController {
protocol Delegate { ... }
var delegate: Delegate? // Delegate<.Model == Model>?
protocol P { ... }
// implied where T.Model == Model constraint
func doSomething<T: P>(with value: T) { ... }
}
Why wouldn't it work for any?
func foo (_ c: any Collection C) where C.Element == Int
In the above example c
could be a different type each time, but we know it is a collection of Ints.
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.
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.
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.
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>