SE-0346 (second review): Lightweight same-type requirements for primary associated types

Yeah, I feel like people will reflexively want to write things like protocol P<E>: Q<E>, and since it’s harmless, we probably ought to let them.

7 Likes

Let me bring "generic protocols" up again for one question. So since the associated type is still required in the protocol body regardless of the primary associated type projection in the angle brackets, does this imply that "generic protocols" remain not blocked syntax wise inside the angle brackets?

//         generic type T
//         v
protocol P<T, U> {
//            ^
//            projected primary associated type U
  associatedtype U
}

I understand that existing protocols with primary associated types would break if they would get another type parameter in the angle brackets, but this is unlikely to ever happen even if we would get generic protocols later on.

This question is not about wether or not we add that feature, but if something like this would still be possible syntax wise. The requirement of the explicit associated type in the body seems like the turning point of the whole conversation we had about it during the pitch phase.

2 Likes

As there can be more than one primary associated type, I would find using list inside angle brackets for two completely different features a bit too subtle, and consequently confusing.

I thought core team was more interested in having parenthesis syntax or something like that for generic protocols, if they would be implemented some day in the future…

I know it may be a bit excessive but I’m in favor of annotating the associated type as ‘primary’ apart from including it in the generic-parameter-like list of the protocol. I think the difference between the plain associated types and the primary ones is too subtle and can be missed, especially if the protocol has a lot of associated types.

I’m not aware of any such behavior in Swift where distinct declarations are interlinked only because they share the same name. Although the generic-parameter-like declaration is not exactly a declaration on its own, someone who doesn’t keep up that much with Swift may be perplexed as to the existence of a generic parameter ID and an associated type of the same name. Despite being inconsistent, this behavior would be actively harmful since there is no particular indication of the angle-bracket syntax not being a generic parameter, which is what angle-bracket syntax is used for in other places.

Most people won’t be significantly impacted by this change. Library authors already engage in verbosity (e.g. with explicit access-control modifiers) so an added keyword won’t be the end of the world. In fact, this feature may help add friction to making an associated type primary, which changes the API surface substantially. Likewise, consumers of the API will likely only look at documentation where they’ll see the most crucial part: the primary associated types in angle-bracket syntax. Contributors, however, often skip documentation and may not understand the connection between two declarations that are lexically separated. Even if they read the documentation, it may not actually clarify that a particular associated is primary, since API consumers will likely only see the primary associated types in angle-bracket form. What’s worse, folks who are interested in the implementation of a particular library are not contributors, so they’re less likely to know the latest Swift syntax, especially if they’re not that acquainted with Swift and come from languages where the angle-bracket syntax has a different meaning. I understand that looking at generic codes and protocols isn’t the most beginner-friendly think to do, but their ubiquity in Swift may lead to this scenario.

Overall, I don’t think that a particularly complex syntax is necessary for connecting an associated type with its angle-bracket counterpart. But the simple act of adding ‘primary’ to the associated type itself, makes the feature more explicit for folks who will go through the code, whether contributors or enthusiasts, thus lowering Swift’s learning curve and ensuring consistency within the language.

1 Like

I’m not sure I understand this perspective. That’s exactly how associated types have always worked in all supported circumstances? That is, conforming types declare a type or type alias with the same name and it becomes the associated type (rather than shadowing it). Protocols that refine other protocols declare associated types with the same name to add constraints. This design is simply being extended to parameterization of the original protocol itself.

I’d argue, actually, that adding new syntax to the language not used elsewhere decreases consistency rather than increases it, while increasing the learning curve rather than decreasing it.

For the author, enforcing an extra annotation doesn’t prevent error but adds another reason why code might not compile—it offloads to the human a requirement to match up two declarations which the compiler does not even require for disambiguation, where mismatches are vastly more likely to be an oversight in bookkeeping than to reflect an unintentionally included parameterization. Such a design would also rather significantly increase the number of mechanical changes required to support prior versions of Swift.

As to the reader, I don’t see any intrinsic reason why the sigil primary would cause someone who would otherwise think that there are two declarations with the same name to think otherwise. Nothing about “primary” means “it’s the same thing as what’s in the angle brackets.”

2 Likes

That's a matter of taste. This is also easily teachable and fixable if we were to say that "true" generic type parameter must always prefixed / suffixed. IMO that restriction would be purely synthetic and actually not necessary.

In the end you can view the generic type parameter on a protocol declaration to be linked to a primary associated type or not. If it's not linked then it remains a pure generic type parameter otherwise it's a primary associated type.

It is debatable wether or not the user has to know if a specific generic type parater is actually a generic type or a primary time at call site (e.g. some P<Int, String>). We can leave this question for the future.

Right, but I don't want to derail this review thread into a discussion around a strawman syntax for a generic feature that uses something else than angle brackets. My original question is a simple YES or NO question. The required explicit associated type changed my mind in a more positive way. If the user will need a constraint a non-primary associated type, he/she will be able to use the generalized syntax, which is already discussed in another thread. I'm at least somewhat happy to see that the revised design for the proposed feature still leaves room for generic protocols which could use the angle brackets, at least according to my intuition.

Let’s say you merge some commit from elsewhere and it removes an associated type declaration that was used as primary type in brackets. I would much rather have compiler complain that an associated type is missing for the primary in brackets than compiler thinking that ”oh, this protocol has a generic now”, and then either proceeding compile or producing very cryptic errors. That design feels flawed. But yeah, this is pretty much off topic, so no more…

2 Likes

Fair point, but again it is debatable how much weight such cases would have for a design of that feature or if we can consider this as an acceptable user error. Once again, I'm just seeking for a simple answer to my question if we could theoretically fit generic type parameter into the same angle brackets type list.

This touches on something that I brought up during the pitch phase: Swift already has an established term for "the things between the angle brackets", and that term is type parameter. Calling this feature "primary associated types" is making it harder to understand than it has to be. Why not simply call them "protocol parameters"? That’s how people are going to explain them anyway.

(Quick! What’s the thing that you use to parametrize a protocol? Is it A) a protocol parameter, or B) a primary associated type?)

2 Likes

Tiny personal nit pick: protocol type parameter

I think the primary associated type here is intending to convey semantic meaning which is more detailed and informative than just calling it protocol type parameter.

Same way like any word can be called ”a word”, but there’s additional value to distinguish verbs and nouns, for example.

3 Likes

Personally, I've thought that "parameterizing associated type" would be a fitting term.

1 Like

And my point is that it is actually less informative. The associated types that you want to make available for parametrization may well be the most important associated types, but the distinguishing feature of making them parameters is that they become parameters, not that they are important. Calling them "primary associated types" is putting the cart before the horse.

I revised the implementation for these two changes: Revise SE-0346 implementation for core team decision by slavapestov · Pull Request #42158 · apple/swift · GitHub

5 Likes

That would be slightly confusing with the generic associated type, which is another highly anticipated generic feature.

2 Likes

I didn’t phrase that very well.

A protocol is a declaration specifically for creating a common interface across types and thus implementing common functionality. To create a common interface, protocols have (non-functional) requirements that must be satisfied by conforming types, which another way of saying that these types need to choose an implementation. Whether conforming types choose the default inferred requirement implementation or create their own implementation is a different question. There is thus a clear relationship and therefore distinction between protocols and their conforming types.

However, when talking about protocols on their own, shadowing is generally prohibited. When it is allowed, shadowing is done explicitly, like with the override keyword. Relating types, in general, uses explicit annotation, such as with typealias and where clauses (in associated types or after the protocol name). Similarly, concrete types follow the same approach of no/explicit shadowing.

Not to mention that the name in angle brackets doesn’t act so much as a declaration but rather as a marker for primary associated types; the actual declaration uses the associatedtype keyword. It would be fine if primary associated types where declared solely in angle brackets and interpreted by conforming types as requirements. But that’s not what is done here, because this reinterpretation of marking and not shadowing happens within the protocol (not between a protocol and a concrete type).


I see your point, but this is already a novel feature; I don’t think a new keyword is what changes the learning difficulty. It may actually help with searchability (if someone doesn’t search “protocols angle brackets,” but “primary associated type”).

Could you elaborate on why that would be the case?

True, but it does signify that “this isn’t a regular associated type.” Thus, folks coming from languages with generic protocols will have a clue that this isn’t the same feature in Swift.

Actually, the override keyword, when used with protocols, is an internal undocumented feature implemented for ABI-related reasons that has never gone through Swift Evolution:

It is not a part of the official language, which pointedly does not require any keyword for restating a protocol requirement. (Also, note that overriding is not shadowing; indeed, they are opposites. A shadowing requirement (for ABI purposes) in a refining protocol must be annotated with @_nonoverride.)

I have a pretty good hunch that what people will search for is "generic protocol" in Swift, and that the top results will tell them exactly what these protocols are and in what ways they're not generic.

With your proposed amendment, one would need to make changes to both the declaration of the protocol and the declaration of the associated type in order to convert from future Swift to current Swift, since both the angle brackets and primary would be rejected by today's Swift compiler.

For a protocol with a single primary associated type, this would represent double the number of edits; for a protocol with two primary associated types, triple; etc.

1 Like

Huh, I always assumed override to be an official part of the language. Its lack of an underscore prefix and its adoption by standard library types reminds be of @rethrows.

Agreed, “shadowing” is not a good term for subsuming a requirement in a refining protocol.

Yes, perhaps, but I think that having a keyword attached makes things easier. Of course, this can’t be done for every single feature. However, features that belong in the not-so-advanced category, where users would be expected to be experts, but are not too ubiquitous either, where the feature would be explored in introductory Swift tutorials, should have good searchability. An anecdotal example of this happening was with closures in UIKit: I saw the more lightweight closure syntax after autocompleting, but didn’t know why that was happening. In UIKit, trailing closures don’t appear that much (compared to Collection operators, SwiftUI, etc.), so they weren’t covered in many tutorials (or at least those I watched). So for a long time I thought trailing closures was a UIKit-only thing. Now, yes, UIKit is just a framework, but my point is that trailing closures weren’t that common, but when they showed up, I didn’t have a keyword or an attribute to search for. Otherwise, users may see primary associated types show up more and more, but they may have to depend on randomly stumbling across a tutorial.

OK, I thought maybe you implied a change in the swift-interface-file generation by the compiler.

I understand this concern but new features and constantly being added, while Swift manages to remain mostly source-compatible (e.g. not changing the behavior of #file yet, albeit not being clearly defined). If we examine how each library author that wants to support older tooling will add new features, I think we are missing the point. The way I see it, larger libraries that need to work with many contributors have such concerns, but, consequently, they also have the engineers to commit to that, in this case, committing to double the effort of adding primary associated types.

This review is complete, and the Core Team has decided to accept SE-0346. I'd like to thank everyone who participated in this review (whether in this thread or previously) for your contributions.

John McCall
Review Manager