@anandabits one more example:
P.self is P.Type // false
P.self is P.Protocol // true
@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 existentialmeta T
is the metatype of Tany meta T
is the metatype of the associated existentialany P
is the existential for the protocolmeta P
is the protocol metatypeany meta P
is the metatype of the protocol existentialI 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)?
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.
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:
T.Type
and reclaim .Type
If itâs not a normal generic type or typealias I think this syntax would be confusing. If we had type-level functions and used the same angle bracket syntax for those then it would make a little bit more sense. It still isnât clear to me how we would be able to extend metatypes to add protocol conformances to them though. I donât think we would be able to extend the result of a type function.
So you can argue about Collection<.Element == T>
, no? As Joe said, for some any Meta<T>
is visually more appealing than any mata T
.
We arenât taking about supporting Collection<.Element == T>
, we are talking about requiring existentials and opaque types to be prefixed with any
and some
respectively so it would be any Collection<.Element == T>
and some Collection<.Element == T>
. The prefix keywords make it clear that the angle brackets have different meaning than they do for ordinary generic types.
Iâm not trying to fixate on syntax here. Iâm trying to focus on semantics. Ideally the syntax would align with the semantics so Iâm asking questions where I see syntax that doesnât seem to align with the semantics I hope to see.
Okay I understand. So in case you are fine with meta T
then what would you think about typealias Meta<T> = meta T
a small convenience? (Sure this is duplication of the same thing, but that's not my question.)
That would be a way to make the Meta<T>
syntax fit into the semantic model I hope to see. If it didnât conflict with the Any
we already have you could also imagine typealias Any<T> = any T
to access the associated existential of any type.
Given the naming discusses just above the quote, the natural way to do constraints is:
func foo(a: some Collection A, b: some Collection B) -> some Collection C
where A.Element == B.Element, B.Element == C.Element
No need for new weirder syntaxes.
I understand using shorthands (anonymous types) for situations where they are well suited for. But for more complex situations, named types are natural fit. And anonymous types can become unwieldy or difficult when you try to refer to them without having something exact to refer to.
I mean is it really that critical to create new, unnatural syntax for anonymous types that covers every single case of complexity that would be already possible with named types (who require nothing of this)? Is it worth making the parser/compiler that more complex? Having other people look at shorthand code and not understand it? Wouldn't it be ok to say that "anonymous types cover these simple cases as a subset and full functionality is provided with named types"?
Any kind of shorthand we come up with for some in return type position, should be mirrored (where applicable) for parameter position, as well as for any in same places. And we don't want to end up with a jungle of syntax.
The way I would like to see the evolution of any/some is that after MVP:s like the latest SE-0244: Opaque Result Types (reopened) - #22 by Moximillian, the named types would be introduced (at some point): some Collection C
, maybe any Protocol A
, to handle all the complexity with constraints and others, which comes with no requirement for new syntax for setting up constraint rules etc.This gets us full functionality.
Once that is in use and people see actual code, then introduce some well-chosen shorthands for anonymous types, where they make sense. After all, at that point it's mostly just eye-candy, and we can have a little bit patience for one's favorite sugar, right?
EDIT: hmm, if named some types would be used e.g. in stdlib/Foundation, would those names be visible to users of library? If thatâs a problem then maybe initial implementation of named some types could be converted to anonymous by the compiler behind the scenes so that names are not visible to outside of module... Ultimately, since thereâs been discussion about figuring ways to refer to anonymous types too, Iâm not sure such conversion would be worth it though. Youâre not really hiding anything, just making it more difficult to use it?
We do have precedent with regular generics func foo<T: P>()
of having always to name the type. And we have survived quite well without anonymous types there. While some is bit different when used as reverse generic (return type), I think naming is still right place to start. If I understand correctly, the current existential syntax func foo(item: Protocol)
is effectively anonymous type equivalent and people are struggling with that concept and inability to refer to the exact named thing.
In terms of ease of use, hereâs a bit exaggarated example:
Current
func foo(item: Protocol)
â Error: âprotocol Protocol does not conform to protocol Protocolâ
with named any
func foo(item: any Protocol I)
â Error: âexistential I does not conform to protocol Protocolâ