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

Hello, Swift community.

The first review of SE-0346 has come to an end. The Core Team has reviewed the feedback received during the review and come to the following conclusions:

  • We are convinced that succinctly laying constraints on associated types is an important problem worth solving in the language.
  • We think that generic argument syntax is the right syntax to use for the common case of constraining so-called "primary" associated types. The ability to combine this syntax with opaque type parameters (e.g. Collection<some Equatable> ) makes this a surprisingly powerful way of expressing constraints.
  • We are comfortable with the current limitation of this expressive power to primary associated types. It would be good to add a syntax for arbitrary constraints in the near future, but using the generic-argument syntax for this purpose seems right, and a more general feature will necessarily need to use a more general syntax. It is acceptable, even good, for proposals to focus on the most important cases first as long as they don't preclude generality in the future.
  • We do not think that "generic protocols" are the right way to model arbitrary relationships between multiple types, and so we are not concerned about taking the generic-argument syntax away from that future direction.
  • We understand the reservations of library authors who are maintaining source distributions that must work with older tools, but we cannot accept this as an unlimited constraint on language evolution.
  • We agree with the feedback from the community that the list of primary associated types on a protocol should always name existing associated types, rather than being treated as declaring them itself. While this syntax intentionally looks like a generic parameter list, the protocol is not generic over these types; requiring the associated types to be explicitly declared reinforces that distinction. Along the same lines, it should not be possible to declare protocol constraints on associated types in this list; that should be done in a where clause or on an associatedtype declaration.With that said, we don't find the documentation argument for doing this convincing. As a general rule, it needs to be possible to add documentation to generic parameters in order to properly document generic types.
  • We agree with the feedback from the community that constrained uses of a protocol with multiple primary associated types should have to give all of them, rather than being allowed to give only a prefix and leave the rest unconstrained. In addition to being consistent with other generic argument lists in the language, this will also allow the future introduction of default constraints on primary associated types.

The authors have agreed to revise the proposal, and a second review of SE-0346 begins now and runs until April 12th, 2022. This round of review is focused solely on the final two points covered above, about the declaration of primary associated types and whether it should be permitted to give fewer types than declared; all other aspects of SE-0346 are considered accepted .

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager. When emailing the review manager directly, please keep the proposal link at the top of the message.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

https://github.com/apple/swift-evolution/blob/main/process.md

Thank you for helping to make Swift a better language.

John McCall
Review Manager

26 Likes

+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