The proposal isn't quite ready for review because the implementation is incomplete; the type representation syntax works, but the declaration side uses the @_primaryAssociatedType attribute and not the proposed generic parameter list syntax. It goes without saying that this is all behind a feature flag; use -Xfrontend -enable-parametrized-protocol-types if you want to experiment with it. I hope to have the new declaration-side syntax implemented in the coming days so that we can begin the formal review.
I have removed the material from the old pitch about the sugared syntax for extensions of concrete types, like extension Array<String>. We feel this is better pitched in a separate proposal.
This is great! I have convinced myself that this is a very intuitive meaning for MyProtocol<AssociatedType>, and I expect this will do wonders for easing the generics learning curve. I wholeheartedly agree with aligning the protocol declaration syntax and the parameterization syntax rather than adding some sort of annotation on an associatedtype declaration.
I am also optimistic that we will eventually want to be able to provide explicit argument labels for generic parameterizations anyway (e.g. for types which want to have two variadic generic parameters), which would admit a further (intuitive, IMO) syntax here, e.g. Collection<String, Iterator: SomeConcreteIterator>.
IMO biggest potential concern here (given what features people have requested on the forums in the past) is that using this generics-like syntax for same-type constraints and primary associated types essentially cuts off any chance for generic protocols in the language. I'm not particularly concerned by this—in the event that in Swift X we do actually want such a feature, I believe it could be served by allowing protocols nested within (and inheriting) generic contexts, e.g.,
AFAICT this would serve all the purposes that "true" generic protocols would, and IMO has clearer semantics about the identity of different versions of the Generic.Interface protocol.
Correct. However we still have to work out a story for module interfaces, probably using an #if $feature to conditionally emit every protocol with a primary associated type twice, once using the new syntax and once without. It's a bit gross but not a huge deal.
That is also correct. Of course adopting it in the standard library is important, we just felt that this adoption could move in parallel with the compiler work.
To be honest, as someone who has been working on the generics implementation for many years, it is still not clear to me what a hypothetical "generic protocols" feature actually means and whether such a generalization is desirable or even feasible to implement.
So I would expect that this syntax would preserve throwing semantics of the type. Specifically for this to be useful for AsyncSequence (which do not misconstrue my critique... this is VERY important for AsyncSequence... no one really wants to write .eraseToAnyPublisher() over and over...) it needs to preserve the throwyness or non-throwyness of the base type. As of current the implementation does not seem to do so.
Is that a consideration that is going to be accounted for in this proposal?
Here is an example I just ran on nearly top of tree and sadly it seems to fail:
@rethrows
protocol Foo {
@_primaryAssociatedType associatedtype Element
func bar() throws -> Element
}
struct NonthrowingFoo: Foo {
func bar() -> String {
return "bar"
}
}
func testNonthrowing() -> some Foo<String> {
NonthrowingFoo()
}
print(testNonthrowing().bar()) // call can throw but is not marked with 'try'
It is worth noting that this does not require the @rethrows to be there for the behavior to incorrectly propagate.
I'm still apprehensive about this. These are not generics and as such, I'm not convinced that they make the generics system easier to understand.
The problem of adding constraints to opaque types is real, but I think we can do better than this. One same-type constraint, half of which is defined by the protocol author rather than the user, is not an adequate solution.
I'm also not convinced that same-type constraints are worth blessing with special syntax. It makes generics less generic, and feels similar to the mistake we made with existentials. Some of that concern might be alleviated if we had the syntax some Collection<some StringProtocol> available to express a subtype constraint.
But even if it was, I'm still not sure this is the right call. The community has had very mixed feelings about this since it was pitched, and it seems like nothing has really changed. I'm also concerned that we're just "doing things" without a clear or ambitious-enough vision of where we want to go.
I think some Collection<some StringProtocol> as sugar for T : Collection where T.Element : StringProtocol falls out naturally from Doug's opaque parameters pitch and this. It might need some implementation work but I don't see any conceptual problems with this.
That is not allowed currently, since an inheritance clause entry on a concrete type does not desugar to a generic requirement on Self, so there is no base type to apply the same-type requirement to.
You could say that this is equivalent to
struct Lines: Sequence {
typealias Element = String
...
}
But that's more of a stretch, in my opinion. However we could offer this as a fix it if the user writes struct Lines: Sequence<String>.
Looking forward to this! My only hope is that "multiple primary associated types" are prioritized sooner. Would be a shame to not be able to do things like:
Agreed—an example like this would be great to note in the proposal text.
In particular, banning struct Lines: Sequence<String> helps reinforce the notion that these are not generic protocols: a developer coming from e.g. Java can be guided into the realization that only one conformance to a protocol is allowed, by nature of only : Sequence being supported.
What is the performance impact of using this? For example; would it be reasonable to have had the map operation return a some type? If a type is frozen and inlined for performance do we keep that performance or is it obscured by the some-ness?
If the function is inlinable, the opaque type is effectively just sugar and callers will see the concrete underlying type at the SIL level. If the function is not inlinable, callers will manipulate it abstractly.
The underlying concrete type does not need to be frozen for the inlinable optimization to occur; it being frozen is orthogonal to the opaqueness of the function's result.
I was initially quite hesitant about the use of generics syntax to parameterize protocols by their associated types, but I am much more happy about this iteration.
I think my happiness is because of the alignment between how the parameterization is declared and how it’s used. To my mind, it’s basically saying that this is the way in which protocols are parameterized in Swift. Yes, it eliminates room for generic protocols but (as explained in the Generics Manifesto) that feature isn’t really what we’d want to support for very good reasons.
I think this iteration makes it plain, as pointed out by @Jumhyn, that the limitation to one parameter seems arbitrary. Indeed the proposal text offers an example off the bat of SetProtocol<Element>, which makes it pretty glaring that the corresponding hypothetical DictionaryProtocol<Key, Value> isn’t allowed. Not sure that an arbitrary restriction adds to rather than detracts from conceptual simplicity here.
Otherwise I think I’m growing to really like how this is shaping up.
Not to derail the main conversation, but is #if $feature ever going to be pitched for Swift Evolution, or will it remain an undocumented internal feature?
I'm still convinced that this feature is both bad syntax, as well as semantics-wise, due to conflating two orthogonal concepts (generic parameters and associated types) into one syntax:
-1
Previous comments
I can't comment on how feasible generic protocols would be to implement in Swift, but as an avid user of generic traits in Rust I can at least try to give some context as to why one would want them (or something equivalent) in Swift:
It's fairly long, so folding it:
Type-safe, compiler-checked value conversions
Type-safe, compiler-checked value conversions
Swift (like Rust) chose to make type conversions explicit. This is great from a correctness and safety perspective, but tends to be very hindering when trying to write abstract code that's generic over its types. The main reason for this is that Swift lacks a way to abstractly express type convertibility.
Say you have a function that is supposed to calculate the fitness of a given value in respect to a certain "ideal" value:
(This formula is taken from step 7 of the "fitness distance" algorithm defined by the WebRTC spec, which I had to implement recently and where I found myself—yet again—in need of generic protocols.)
Now say that the values you need to compute the fitness for have different types then Double, say Int:
If we were to declare type-specific protocols for every type we possibly might want to convert to, we would be polluting our project's namespace with an immense amount of redundant garbage (and not be gaining much from it semantically, either).
Just do the math: Given N interchangeably convertible types we would need do define N individual protocols of the pattern protocol <…>Convertible { … } and come up with N unique, yet expressive method names to go with them?
And what if this N gets getting bigger and bigger? And what if instead of dealing with concrete types you were working on generic types or methods/functions, and hence no way to have the Swift compiler pick the right explicit protocol for a given generic type T?
This is where generic protocols safe the day!
protocol ConvertibleInto<T> {
func into() -> T
}
But wait, can't we do this already with a protocol with associated types, like so? …
protocol ConvertibleInto {
associatedtype T
func into() -> T
}
Well, it depends. While you could easily implement Foo: CustomStringConvertible in terms of …
struct Foo {
let bar: Int
}
extension Foo: ConvertibleInto {
typealias T = String
func into() -> T {
return …
}
}
… you would get in trouble as soon as you were to decide that it would be nifty to also be able to convert instances of Foo to Data:
// error: redundant conformance of 'Foo' to protocol 'ConvertibleInto':
extension Foo: ConvertibleInto {
// error: invalid redeclaration of 'T':
typealias T = Data
// error: invalid redeclaration of 'into()':
func into() -> T {
return …
}
}
In today's Swift a protocol (regardless of whether it has associated types, or not) can only be conformed to once by any single type. In order to achieve polymorphic semantics of protocol ConvertibleInto we however would need to have a way to allow for multiple conformances per type.
In addition to a hypothetical ConvertibleInto<T> we would probably also want to have access to a corresponding ConstructibleFrom<T> protocol going the other way:
(We will be using extension<…> as a hypothetical syntax for introducing generic arguments into an extension scope.)
Going even further one might want to have the Swift compiler automatically derive conformance of ConstructibleInto<U> for every type U: ConstructibleFrom` with a default implementation like so:
extension<T, U> T: ConvertibleInto<U> where U: ConstructibleFrom<T> {
func into() -> U {
return U(from: self)
}
}
One might also want to have every type T auto-derive conformance to T: ConstructibleFrom<T> like so:
func draw(image: T) where T: ConvertibleInto<CGImage> {
let cgImage: CGImage = image.into()
// …
}
… having it accept images of any type that's convertible to CGImage (e.g. UIImage, NSImage, CGImage, …).
Generic overloading
Generic overloading
With all this talk about multiple conformances of protocols one might wonder "wait, isn't what what Swift has function overloading for? We already can implement variants of a function based in argument and/or return types, why need protocols for that?"
And of course some truth to this sentiment.
Let's assume we wanted to write an efficient and type-safe linear algebra framework for Swift. We would probably end up defining types for scalars, vectors and matrixes, like so:
Notice how each method's return type directly depends on the type of rhs.
This works as long as one is dealing with explicit types, exclusively. But sooner or later one would want to be able to generalize over scalars, vectors and matrixes. (After all from the point of algebra they are just tensors of 0, 1, or 2 dimensions respectively.)
As it turns out there is no way to express overloading in today's Swift from the perspective of protocols. It's a dead spot in Swift's generics type system.
If one had generic protocol at one's disposal however one could express things like this:
Now whenever one needs to be generic over a type U and require it to be multipliable with Vector<T>, one could express it through where Vector<T>: Multiplication<U>.
These are just two of the many use-cases for generic protocols off the top of my head. There are many more.
I don't see how avoiding having to write Collection<.Element == Int> in favor of Collection<Int> is worth effectively shutting the door for any possibility of proper generic protocols ever making it into Swift.
Not being able to express overloading (and abstracting of it in generic contexts) in protocols is one of one of the biggest gaps in Swift today and a daily annoyance for anybody writing generics-heavy code in Swift.
Details hidden because this is a bit of a tangent, but still slightly relevant insofar as it impacts the syntax here
I have always understood the generic protocols as having semantics similar to the (intuitive to myself) semantics of the Generic<T>.Interface example. Just as Generic<Int> and Generic<String> are distinct types related by being parameterizations of the same generic base, Generic<Int>.Interface and Generic<String>.Interface are distinct protocols related by being (indirect) instantiations of the same generic base.
For a hypothetical "true" generic protocol MyGenericProto<T>, types would be able to conform separately to MyGenericProto<Int>, MyGenericProto<String>, etc. etc.
Beyond my intuition about what "generic protocols" mean, though, I have no idea about the ultimate soundness or feasibility of such a system. I myself have not really encountered a spot where I wanted to use a generic protocol, so the using the "obvious" syntax for generic protocols as the syntax for same-type constraints is not that concerning for me.