I feel like we are talking past each other. What I was trying to suggest was an alt-syntax that still housed the primary associated types inside the angle brackets. I thought one of your issues was that would become ambiguous if generic protocols existed in the future, so I amended by saying we could prefix the associatedtype declarations with the 'associatedtype' keyword inside the angle brackets.
So rather than (quoting your example):
protocol P<A> {
^~~ generic type parameter list, not primary assoc
primary associatedtype B
// non-primary
associatedtype C
}
You would hypothetically declare it as
protocol P<A, associatedtype B> {
// non-primary
associatedtype C
}
Ah, that makes more sense now. While I understand what you mean, I don't think this is a good approach unless we would start explicitly annotating the primary associated types inside the declaration angle brackets today.
protocol P<associatedtype A>
Unless we come up with another short form for that, I don't think this is the syntax we all want to see as there will be several protocols with multiple primary associated types with potentially longer names.
Requiring to write associatedtype before each primary associated type inside the angle brackets or just having a single associatedtype in the list feels strange to me. Also one more thing. If protocols had a generic type parameter, we may want to apply it as a default type on a potentially primary associated type.
The version with a marker seems to be more straight forward:
protocol P<A> {
primary associatedtype B = A
}
extension SomeType: P<X> {} // SomeType.P<X>.B == X
extension SomeType: P<Y> {
typealias P<Y>.B = Foo // default overriden
}
exntesion SomeType: P<Z, Bar> {} // SomeType.P<Z>.B == Bar
Generally there is a strong expectation that declaration order inside type definitions doesn't matter. There are exceptions (like @frozen structs) but those are special cases most users aren't aware of.
This is the main reason why we don't have comparable synthesis for structs, but do for enums where ordering matters in other ways (raw values and case iteration).
On the other hand, it's well understood that the order of the generic placeholders on a type has meaning, and this understanding should transfer directly to primary associated types.
Wouldn't that still apply to my suggestion though? We can document and teach that the order of primary associated type exposure is reflected by their top down declaration order on the sugared use-side?
// proposed
protocol DictionaryProtocol<Key: Hashable, Value> {
...
}
// my suggestion that might be less discoverable at a glance
// but it keeps some known syntax integrity and future extensibility intact
protocol DictionaryProtocol {
// declaration order for `primary associatedtype` needs to be documented
// as it becomes important, however it's only important for the
// sugared use-side only
primary associatedtype Key: Hashable
primary associatedtype Value
...
}
// nothing changes for the use-side
some DictionaryProtocol<SomeKey, SomeValue>
It does feel like the primary markers would only enable a few compiler checks:
if there is a primary marker, allow the sugared syntax P<Something>
link the order from the generic type parameter list with every primary associated types in their restrictive order
By the end of the day, we're talking only about sugar syntax for a same-type constraint. The general feature as John previously mentioned still does not rely on any kind of order of associated types (primary or not).
Sure, we can document anything and hope people read the docs. But should tread carefully where a design violates a usually-safe assumption that you can reorder declarations. Reorganization of source code is a common thing to do and this would introduce a new way in which doing so could break code. @frozen is an exception to this because that is a "user beware" feature for when you are intentionally making your ABI fragile to gain performance. This feature is not like that.
Considering the difficult migration for libraries (refs 1, 2), keeping @_primaryAssociatedType in one form or another would help (and then order would matter, yes, and this would be very clearly understood):
// New syntax
protocol MySequence<Element> { }
protocol DictionaryProtocol<Key: Hashable, Element> { }
// Alternative syntax for libraries with compatibility constraints
protocol MySequence {
#if compiler(>=...)
@primaryAssociatedType associatedtype Element
#else
associatedtype Element
#endif
}
protocol DictionaryProtocol {
#if compiler(>=...)
// Order matters: deal with it
@primaryAssociatedType associatedtype Key: Hashable
@primaryAssociatedType associatedtype Element
#else
associatedtype Key: Hashable
associatedtype Element
#endif
}
The acceptance notes for SE-0035 (Introduce existential any) has postponed the discussion about the transition path. Maybe the transition path for this pitch will be postponed as well. But postponing does not mean ignoring, right?
And if each transition path needs its own pitch and proposal, well this puts the transition path in great danger, since very few people understand the need for those transition paths, and among them even less have the ability and time to complete a proposal and its implementation.
While I understand your point, I would like to throw the question into the room if reorganizing associated types vertically inside an evolving protocol is really a concern that we should solve in a way that does not require documenting the 'sugared' call-side order? Does that concern really outweigh the trade-off for keeping syntactical spare space for another possible feature?
// proposed syntax
protocol P<A, B> { ... }
// yes it's probably unlikely that someone updates this to
protocol P<B, A> { ... }
// my suggestion
protocol P {
primary associatedtype A
primary associatedtype B
}
// possible:
// * but is that really that likely to happen without being caught very quickly?
// * does this really needs to be solved differently especially with such trade-offs?
protocol P {
primary associatedtype B
primary associatedtype A
}
Again, we're talking about sugar code for a non-existing general feature. If that existed, here's what the above concerns could potentially look like:
// the non-sugared general feature does not have this issue
func foo() -> <T: P> T where T.A == ConcreteA, T.B == ConcreteB
// only the sugar version from the current proposal has the order issue
func foo() -> some P<ConcreteA, ConcreteB>
// if someone was to re-order `primary associatedtpe A` and `primary associatedtype B`
// this will likely instantly break and `ConcreteA` will land in `B`, while `ConcreteB` will become `A`
// 👆is that such a big deal?
In that spirit: Does solving the 'order' problem for sugar syntax justifies burning an entire different future feature, which then would possibly require some unusual workaround syntax? (I'm talking about the pitched alternative syntax for generic protocols in this thread protocol Q(...) {}.)
Could you define what you mean by "parameterized protocols" or link to a definition elsewhere? I keep seeing it thrown around without any clear direction on what it actually means. For example, is the type relationship parametric or ad-hoc? If the latter, why are we so fixated on using parametric syntax?
Just going to throw a +1 on this. A big part of my support for the angle bracket syntax at the point of use is thanks to the alignment with the syntax at the point of definition. I would consider it a pretty big detriment if the 'compromise' position broke this alignment.
I gave a thorough overview of what "parameterized/generic protocols" are and why they are desirable (i.e how they fill a large gap found in Swift today) in an earlier comment to this thread.
This is a very selective framing. Let’s review what the manifesto actually says:
Unlikely
Features in this category have been requested at various times, but they don't fit well with Swift's generics system [...]
Generic protocols
One of the most commonly requested features is the ability to parameterize protocols themselves. For example, a protocol that indicates that the Self type can be constructed from some specified type T:
Most of the requests for this feature actually want a different feature. They tend to use a parameterized Sequence as an example, e.g.,
protocol Sequence<Element> { ... }
func foo(strings: Sequence<String>) { /// works on any sequence containing Strings
// ...
}
The actual requested feature here is the ability to say "Any type that conforms to Sequence whose Element type is String"
The pitched feature directly addresses what the manifesto identifies as the most common desired use case for P<T> syntax, albeit in a slightly different way than envisioned (the manifesto prefers generalized existentials). The manifesto is quite clear in preferring this direction over parameterized protocols.
Also, as far as I can recall, nobody has yet shown an example of a “generic protocol” where parameterized protocol syntax is manifestly better than the mooted “multi-self protocol” concept. (And yes, I did just review @regexident’s examples.)
On the contrary, I haven’t seen a strong defense of the assertion that generic protocols are inferior to or necessitate multi-Self protocols. I challenged this earlier but was ignored.
That’s an artificially high standard. If cases like constraining collections are indeed far more common, which seems highly likely, multi-Self protocols don’t need to be better than parameterized protocols, they merely need to be good enough.
On the contrary, introducing the topic of multi-Self protocols has created an impossibly high standard for proponents of generic protocols. Instead of simply explaining their utility, perhaps by way of comparison to Rust which already has them, they are now forced to defend the introduction of an esoteric rank-N type class systems—one that, again, Rust is able to hide from the language user.
This made things click for me. I wonder if this association could be made clearer somehow, maybe through naming. I don’t feel like "primary associated type" really captures this notion.
There is zero interest on the Core Team in reserving P<Int> for “generic protocols”. I can say that with confidence because we’ve had multiple conversations about it. To give you an idea of the tenor of those discussions, they’ve all turned into discussions about whether it’s acceptable Evolution procedure to just rule something out by fiat or if we formally need to go through the proposal process for a negative feature.
Arguments against using this syntax include:
There are other reasonable and available spellings for that concept if we decide we want it, such as (T,U) : Convertible. Thus we’re not talking about ruling the feature out completely; we’re talking about the practical consequences of using this particular spelling.
The generic spelling inappropriately elevates one parameter over the other when in fact there’s no relationship.
The second parameter would behave unlike an ordinary associated type, so it ought to be declared differently, and it certainly needs to be used differently. For example, since there is no functional dependency, Self has no unique member of that name. Using member syntax to declare the “associated type” would be misleading. Using member syntax to name it would immediately break down in any context where multiple conformances are known.
The generic spelling is familiar and widely used, and it would induce programmers to use the feature when in fact they ought to have a functional dependency, as they would with an ordinary protocol.
Additionally, aside from the syntax, there are fundamental soundness problems with the feature in terms of the type-level computation it accidentally enables. It’s roughly analogous to an automaton going from one stack to two. If memory serves, folks in the Haskell world were exploring the research potential and expressive power of lifting some of the restrictions on type class instances when suddenly they realized that everything they were doing was already enabled by multi-parameter type classes, just with less convenient syntax. So it’s not something we should do casually.
We can certainly include a more conceptual definition of "primary associated type" in the proposal to capture this phenomenon with conforming types. As far as the terminology itself, I don't think any of us proposal authors have been able to come up with a better name for the concept, but we'd welcome other suggestions!
I feel like there’s a significant disconnect between what people are asking for and what the Core Team is hearing. People are asking for a language-level abstraction over writing out a bunch of (e.g.) ConvertibleFrom* types. They’re not asking for types with multiple Selfs, and I don’t think there has been a thorough argument that proves that would indeed be necessary achieve what they’re asking for.
People aren’t asking for a new kind of associated type. They are asking for a factory for protocols. In other words, they’re asking for a type constructor T -> U -> V. To substitute real names, Int -> ConstructibleFrom<T> -> ConstructibleFrom<Int>. ConstructibleFrom<Int>.Self has a dependency on ConstructibleFrom<T>, and thus transitively on T.