OK, after letting this gestate over the past few days, I think I’m ready to propose something radical:
- Instead of creating second kind of associated type, let’s eliminate the
associatedtype
keyword entirely.
- Let’s call this feature “generic protocols”.
Rationale
There are exactly two things you can do with a generic type:
- Instantiate a concrete type from it.
- Instantiate a quantified type via
where
.
Concretization of generic types happens all over the place in normal Swift code. The most obvious example is when instantiating a value of a generic type:
struct S<T> { }
let s = S<Int>() // constructs S<Int> from S<T>
This is the example @regexident draws on when talking about invoking generic types like functions. But this isn’t the only place where such an operation occurs. Because of inheritance, generic classes can also be concretized when subtyping:
class Parent<T> { }
class Sub: Parent<Int> { } // constructs Parent<Int> from Parent<T> and extends it
Like generic classes, protocols with associated types can also be concretized by subtyping:
protocol P {
associatedtype T
}
struct S: P { // constructs type 'P where T == Int' and subtypes it
typealias T = Int
}
So why does this have a different name and syntax from generic structs? We could call such a protocol a “generic protocol” and spell it the same way with no loss of expressivity:
protocol P<T> { }
struct S: P<Int> { }
And then we can discuss syntax enhancements that apply to all forms of generics:
class Parent<First, Second> { }
class Sub: Parent<.Second = Int, .First = String> { }
protocol Collection<Element, Index, Iterator, SubSequence> { }
struct Array<T>: Collection<.Element = T, .Index = Int, .Iterator = IndexingIterator<Self>, .SubSequence = ArraySlice> { }
As well as new functionality such as generalized existentials:
// Existential of generic struct
var arr: any Array = [1, 2, 3]
arr = ["one", "two", "three"]
// Existential of generic protocol
var coll: any Collection = Set(["a", "b", "c"])
coll = 1..<10
But what about “real” generic protocols?
The Generics Manifesto describes two different things that commonly go by the name “generic protocols”:
- Generalized existentials.
- Multiple conformance.
Even without multiple conformance, the language already prohibits conforming to a protocol more than once:
protocol P {
associatedtype T
}
struct S { }
extension S: P {
typealias T = Int
}
extension S: P { // error: redundant conformance of 'S' to protocol 'P'
typealias T = String
}
Concretizations of generic protocols are themselves subtypes of the generic protocol, and this restriction is a specific instance of Swift’s more general prohibition against subtyping a supertype via more than one path. For example, Swift eschews the diamond inheritance problem by prohibiting multiple inheritance in classes.
The manifesto contains an example where have historically asked for the ability to conform to a
protocol more than once:
protocol ConstructibleFromValue<T> {
init(_ value: T)
}
struct Real { ... }
extension Real : ConstructibleFromValue<Float> {
init(_ value: Float) { ... }
}
extension Real : ConstructibleFromValue<Double> {
init(_ value: Double) { ... }
}
To be honest, I’m not clear what the programmer expects to do with these extensions. Everything I can think of is either solved by conditional conformances (which arrived after the manifesto was written), or are compatible with associated-types-as-generics.