Thanks everyone for the feedback! My apologies for getting back to this a little late.
As I posted in that acceptance thread, I don't think it makes sense to warp concrete accepted use cases for identifiers in service of theoretical future language directions like what I'm proposing. It's a problem for any new keyword that it may collide with existing uses as an identifier. For instance, some
is already prominently used as one of the case names for Optional
. It seems to me that any
could still be contextually parsed as part of type grammar, since identifier(identifier)
or identifier identifier
is not otherwise valid type syntax, and types are limited in where they can appear inside expressions. If that isn't palatable, Any
is already a reserved keyword, and Any P
seems fine too.
There are definite advantages to C#'s approach. Because associated types for C# interfaces are represented as generic arguments, interface types always effectively bind all of those interface types, so they're only ever "existential" on Self. The .NET collections API is designed well around the tradeoffs this design provides. It's been personally annoying to myself that we haven't at least implemented Protocol<.AssocType == T>
existentials in Swift yet, since as you laid out well, that would enable the more natural dynamic design for Sequence
and Iterator
you described.
Swift's design is aimed at enabling more a expressive type system to capture more interesting type-level relationships between values. The C# design would become more cumbersome if you tried to implement something like Swift's Collection
hierarchy in it, since you'd need to define a type ICollection<Index, Element>
and carry the index around with you everywhere. The type relationship between collections and indexes is what allows Swift's collections to approach "zero cost" in specialized code, since for instance, you know an Array is always indexed by Ints, and that a String is always indexed by valid code unit offsets represented by String.Index. Although you could express that relationship in C#, it would make ICollection
not very useful as a dynamic interface type, since the Index
generic argument is usually specific to a single collection family, so for instance ICollection<Int, T>
would effectively be a type that can only hold Array
s. By using associated types, Swift allows you to express relationships between Collection
s using only the relevant associated types; you only need to refer to Index
when indexing. With more flexible existential types, you'd also be able to refer to any Collection<.Element == T>
to abstract over collections of a certain element type without confining yourself to a specific index. The goal of associated types is to allow for greater flexibility and expressivity, admittedly at the cost of some shorter-term awkwardness since we're missing so many key features still.
For immutable arguments, these are in fact isomorphic. If the limitations on existentials were removed, then both functions have effectively the same input domain. The differences become interesting if one of the arguments is inout
:
// Takes some collection and modifies it, but doesn't change its type
func foo(a: inout some Collection)
// Takes some collection and modifies it, and may change its type
func foo(a: inout any Collection)
Similarly, for return types, like in your second example:
the two signatures suggest something different to the caller. If you return some Collection
, you're saying you always return values of the same type. If you return any Collection
, you could return different types between different calls.
This would be akin to implicitly declaring that P
has an associated type, since the some Collection
would be chosen by the conforming type. That might be nice sugar for this:
protocol P {
associatedtype ReturnTypeOfFoo
func foo() -> ReturnTypeOfFoo
}
Relatedly, it might also be interesting to explore shorthands for generic wrapper types. Instead of writing something like:
// If we get the call constraints discussed in the "static callables" proposal...
struct MapCollection<C: Collection, Element, Transform: (C.Element) -> Element> {
var underlying: C
var transform: Transform
}
It'd be cool if something like this were possible:
struct MapCollection<T, U, ...> {
var underlying: some Collection where Element == T
var transform: some (T) -> U
}
(I realize this conflicts with the use of some
to declare opaque types on the individual properties themselves; it's a sketch of a possible idea.)
I think it'd be valuable to allow you to use generics syntax as sugar for associated types on protocols, so you could write:
protocol Collection<Element> {
associatedtype Index
}
and that would be as if you'd written:
protocol Collection {
associatedtype Element
associatedtype Index
}
typealias Collection<Element> = Collection<.Element == Element>
I don't think we want generic arguments to completely replace associated types, for the reasons I alluded to above. For something like Collection
, you frequently want to refer to its Element
, but the Index
is usually secondary. This is like the "embracing labels" idea you're proposing, in a sense.
Because of Foundation bridging, this is how we'd have to implement it, alas.
That's a reasonable idea!
Yeah, I'm not a fan of type(of:)
myself either, though I do like the idea of being able to refer to types relative to values in generic signatures. To me, the ideal would be to refer to associated types as members of the values themselves, like:
func foo(a: some Collection, b: some Collection) -> some Collection
where a.Element == b.Element, b.Element == return.Element
(though that still needs a hack to refer to the return value, alas).
Yeah. some Any
and any Any
aren't great, I agree. If we could do it all over again, maybe we should've had Any
be called Value
, and AnyObject
just Object
. I like the idea of using some
and any
also in extensions to help disambiguate the "extend conforming types" case from the "extend the existential type" case. That would also fit well with the future addition of generic extensions, since:
extension some P
would naturally expand to:
extension <T: P> T
Since the property's type is a "reverse generic", it's derived from the container's type, but doesn't require the container to itself be generic. The property's definition is what binds the property type, as in:
extension Int {
var foo: some Any { return "foo" }
}
foo
's opaque return type is bound to String
.
I'm still working through the thread; I'll reply to more posts in a bit. Thanks again for all of your feedback!