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

+1 on both points. The stated rationale for each leaves me entirely comfortable, and I am in full support.

In addition, looking at the new syntax in the updated proposal, I find that I no longer have misgivings about this new notion of “primary” associated types. The first point above — type names in the angle braces always referencing an existing associated type declared below — clears up the impression left by the original proposal that primary types somehow exist in contrast to or apart from normal associated types. My reservations from the original review thread are thus resolved.

No reservations about any of this. Looks great.

6 Likes

+1, this has ended up in a really great place! In combination with the existential version of this feature, generics in Swift are getting a major boost for 5.7.

6 Likes

Agree with both of these revisions outlined in the bulleted points; they are improvements which I hadn’t considered until the prior review period and illustrate the value of this process at its best.

10 Likes

Each entry in the primary associated type list must name an existing associated type declared in the body of the protocol or one of its inherited protocols.

I think this rule really elevates this proposal, although I'm curious if the following is a typo or something I'm missing:

protocol DictionaryProtocol<Key, Value> {
  associatedtype Key : Hashable
  associatedtype Element
  ...
}

Should that be <Key, Element>? I don't see any inherited protocols…

2 Likes

I went ahead and fixed that to say Value; thanks for calling it out.

2 Likes

I am +1 for this. This proposal simplifies the writing of a good bit of code on it's own, but its benefits are almost insignificant compared its power when combined with constrained and implicitly opened existentials.

Making primary associated types refer to explicitly declared associated types removes my biggest reservation against the previous version of the proposal.

3 Likes

Thanks for fixing my silly typo!

This revision made the proposal even better!

The draft PR for adopting these in the stdlib has become considerably simpler in this update.

(Incidentally, I just started a pitch thread to figure out which associated types we'll want to mark as primary in the stdlib.)

5 Likes

+1
These second review covers the concerns I had on the first one. (and thanks for the summary on the changes)

One of the things I feel that will be important to document are some guidelines about when to use primary associated types, which seems that the work being done on the stdlib will help defining :+1:

That said, if there is any feedback I could have here is that I still feel like having a syntax that is as nice as this but works for any associated type would be amazing, specially if the user and the usage code doesn't agree with lib-author on what is important. The alternatives considered mentions Require associated type names which I look with good eyes every-time I read about it.

1 Like

EDIT: The main rule has been clarified in the post below.


I cannot find any examples on protocol inheritance with primary associated type forwarding. Here's a quick example:

protocol P<E> {}

// expected to see an ERROR
protocol Q<E>: P {}

// my expectation for correct syntax
// to keep consistency
protocol Q<E>: P<E> {} 

Can the proposal authors please clarify on the current situation of this particular example?

I expect non-primary associated type to be implicitly inherited by the refining protocol while primary associated types would receive an explicit type.

protocol A<X> {
  associatedtype Y
}

// `B` has no primary associated type itself
protocol B: A<X> {
  associatedtype X
  // associatedtype Y is implicitly inherited here
}

// `C` has no primary associated type itself and it
// has a constraint where `.D == .X`
protocol C: A<D> {
  associatedtype D
}

// `D` has a primary associated type and it's aligned
// with `A.X`
protocol D<X>: A<X> {}

// `E` has a primary associated type but it's constrained
// as `.F == .X`
protocol E<F>: A<F> {}

// `E` has a primary associated type but it does not
// constrain the primary associated type from `A`
protocol F<G>: A<X> {
  associatedtype X
}

I’m not an author, but I can answer some of those.

This is actually ill-formed as of the revision; you have to explicitly declare associatedtype E in the protocol body.

This is not an error. It makes E a primary associated type of Q, as it is of P; otherwise Q would have no primary associated types, even though P does. So associated types are inherited (of course), but whether they are primary is not.

Writing P<E> in the inheritance clause is accepted but in this case redundant, since it’s constraining the first primary associated type of P, namely E, to be equal to E.

4 Likes

Ops, I guess I missed that change. :thinking: Thank you for pointing this out to me again.

I guess since my P and A protocols require explicit associatedtype E and associatedtype X in their bodies the rest of the associated type inheritance is self explanatory and falls under the normal inheritance rules we have today.

protocol P<E> {
  associatedtype E
}

// this is okay then
protocol Q<E>: P {}

So I guess this test case must be ill-formed as well:

Yes, the implementation is still catching up to the revised proposal.

1 Like

+1, very happy to see these changes in second review, I think they make this feature much better and also makes adopting primary associated types in existing codebases significantly less invasive change.

Today it actually produces a redundant requirement warning because it introduces the vacuous requirement 'Self.E == Self.E'. I wonder if we should muffle that.

1 Like

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