SE-0346: Lightweight same-type requirements for primary associated types

Hello, Swift community.

The review of SE-0346: Lightweight same-type requirements for primary associated types begins now and runs through March 29th, 2022.

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?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • 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/master/process.md

Thank you,

John McCall
Review Manager

26 Likes

What is your evaluation of the proposal?

Strong +1

Is the problem being addressed significant enough to warrant a change to Swift?

Definitely. The ability to return constrained opaque types in a teachable, ergonomic way is very much needed.

Does this proposal fit well with the feel and direction of Swift?

Absolutely.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I've tested a snapshot of the feature with a library that very much benefits from the feature.

11 Likes

Imho this is much better than the original primary-annotation, but I still don't buy the arguments against a more verbose alternative (Collection<Element == String>), which not only has significant benefits, but is also in line with the use of labels in function calls (which is something that I'd like to see for generics as well).
I also don't buy the arguments against generic protocols, and I consider it actively harmful to (hypothetically) add a special syntax for generic protocols. It's one thing to come up with another meaning for the generics syntax, but adding another way to express a generic type would be just confusing.

Therefore, my preference would be to delay this proposal until there is a final decision about labeled generic parameters and generic protocols.

6 Likes

What is your evaluation of the proposal?

Strongly against.

Is the problem being addressed significant enough to warrant a change to Swift?

No. The alternative syntaxes are very little longer, a great deal more explicit about what they're doing, and a great deal less restrictive on the evolution of the language.

Does this proposal fit well with the feel and direction of Swift?

No, it's abusing existing syntaxes and symbols in new contexts where the meanings differ from the established ones. I believe it will actively make the language harder to teach, and both generics and protocols harder to understand.

If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

Rust uses this syntax for generic protocols, and the alternate <.AssociatedType == ConcreteType> syntax to mean what this proposal does, which is a much more logical approach.

How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

I've been involved in all the discussion threads about this, and nobody has said anything to convince me that

  • Swift doesn't want generic protocols,
  • This proposal doesn't cut off that possibility,
  • This syntax isn't a significant distraction that makes it even harder to teach generics and protocols in Swift,
  • All of that is worth the difference between Collection<String> and Collection<.Element == String> or one of the similar alternate & more general syntax proposals
9 Likes

To be clear, I'm not against some syntax existing in Swift for this, only against the syntax that pretends we have generic protocols when we do not.

And I think that any proposal that wants to steal the syntax for generic protocols for another purpose needs to explicitly address how we get something like Rust's Into and TryInto in the future.

2 Likes

It does.

15 Likes

+1. This is a huge improvement.

Yes, definitely.

Yes.

I prefer this interpretation of the angle brackets to that of other languages.

I read the proposal and participated in the discussions.

2 Likes

The main problem that I have with syntax such as Collection<Element == String> is that it requires you to know what the name of the associated type is. When it's a collection it's pretty obvious that it's called Element, but in other cases it is definitely not so clear (without looking at the documentation).

Consider the following two examples:

protocol Vector {
  associatedtype Scalar
  // ...
}

// 1
extension Vector where Scalar == Int {
  // ...
}

// 2
extension Vector<Int> {
  // ...
}

To me, the second option is just as clear and it doesn't require the developer to know that Vector's associated type is called Scalar (which they quite likely wouldn't unless they were implementing it themselves). And I don't think any clarity is lost, because Vector<Int> clearly reads as 'a vector of ints' (to me at least).

However, I am interested in hearing what significant benefits you think the more verbose syntax has, because I can see that my issue with the extra verbosity may be quite small compared to a loss of clarity in certain cases or other issues such as the proposed syntax blocking out potential further directions.

So, feel free to change my mind :)

2 Likes

There is nothing to argue here — it's similar for functions, and those originally even had a special treatment for the first parameter. However, that was changed because of consistency, and now all parameters have labels by default. I'm convinced that is considered to be a good thing by the vast majority of users. Generic parameters are very similar, and as soon as you have more than one, you loose clarity as you would do with unlabeled functions.

Well, how do you know the labels of function parameters — or even the name of the function itself? It's exactly the same problem, so the same solution would work.

That has been brought up by others long ago, but as there is quite a lot of text, I'll jump to directly to the big two advantages (from my perspective):

  • You can restrict all associated types — not only those which have been blessed as "primary"
  • Because of that, you don't suffer the problem of backwards compatibility. You can utilize a library which was created before anyone even thought about primary associated types, and still use the new syntax. With the proposed solution, every framework has to adopt, and especially if you want to support older versions of Swift, this is really painful.
14 Likes

Overall a large +1 for me. Especially combined with the opaque result types will allow me to replace some class structures with a protocol and structs without increasing the cognitive load writing where clauses everywhere.

Maybe I missed it, but I couldn’t see it called out in the design whether the primary associated type declaration fully replaces the regular associatedtype declaration or is something extra you could add. From the examples it’s implied that it replaces it but it might be worth making that explicit in the design.

Looking forward to this!

  • bok
3 Likes

+1

  • Is the problem being addressed significant enough to warrant a change to Swift?

sure

  • Does this proposal fit well with the feel and direction of Swift?

ok

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

N/A

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Read through the proposal and read the comments here. Thought about code I've written that would be cleaner with this syntax and the things it enables that we couldn't do before.

Yeah, I guess my main issue with Element == Int syntax was just that at the moment it never comes up in auto complete (when doing where clauses) so you just need to know (unlike functions). And I realise that my core problem was actually with it not being auto completed currently rather than about the syntax itself. So if the autocomplete in the swift language server adds support for that in the new syntax then I’m all on board with the extra verbosity for clarity.

2 Likes
  • What is your evaluation of the proposal?

We seem to be introducing new functionality via a ‘syntactic sugar’ feature. I think it would be wiser to design the general feature first, then assess any shorthand syntax on its own merits. If we review this now, there will be strong support for the feature purely for the (limited) functionality it opens up, rather than as what it will ultimately be - a shorthand syntax for a more general feature.

Overall, without some key changes I'm against the proposal.

I feel more consideration should be given to:

  • Supporting conformance relationships on associated types, as these feel almost as significant as same-type relationships, and are mostly handwaved away by the proposal as a feature for full-on generics. We should have some kind of inline syntax for these constraints IMO, and I fear this proposal would have us eventually offer 3 ways to write the same thing - full generics, a more powerful <.Element: Constraint> shorthand, and then this. Another reason we should design a more general reverse-generics feature first IMO so we can explore what needs sugaring in practice.

  • How multiple primary types, where we may opt not to specify them, looks. The rules as written would seem to result in backwards-looking types, for example: Collection<Element> is used a lot on examples, but surely we'll want to support Collection<Element, Index> in order for Index to be optionally constrained? This looks backwards compared to the equivalent concrete type, and would put me off this feature as currently designed were it simply syntactic sugar for an existing feature. The proposal should at explore its own effects on the standard library in more detail, rather than putting that off for future proposals once the feature is already in.

  • Is the problem being addressed significant enough to warrant a change to Swift?

Currently, but only because this is syntactic sugar for an unimplemented feature (return type generics / reverse generics). I don't feel this would necessarily warrant a change to Swift were we to have the actual feature this is sugaring in-place already.

  • Does this proposal fit well with the feel and direction of Swift?

No. The proposal changes the way protocols are used, which isn't inherently a bad thing. But sadly there's more to it than that…

The need for reverse ordering of primary associated types, for example Collection<Element, Index> really runs counter to the feel and direction of Swift.

The proposal gives us two syntaxes for declaring associated types, only one of which supports this shorthand.

I feel every associated type will end up being made primary ‘just in case’ without a more general feature for reverse generics in place, fundamentally changing how protocols are designed, simply for a shorthand sugar.

The proposal asks library authors to try to predict how their types will be used in a way they really shouldn't have to, without offering a path for source-compatible corrections if mistakes to ordering are made.

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

C# has actual generic interfaces. They work differently to this and I worry this could lead to confusion as to why you can't conform to a protocol twice in Swift, with different associated types. (Foo: Collection<Int>, Collection<String>).

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Reading and reasoning through the proposal.

9 Likes

Will the following be valid?

extension Sequence<some Error> {}

A bit of a nit, but I think the formal grammar in the proposal has an error:

  • primary-associated-type-list< primary-associated-type | primary-associated-type , primary-associated-type-list >

The shape of this recursion repeats the angle brackets. That is, it disallows Foo<Bar, Baz> and requires Foo<Bar, <Baz>>.

3 Likes

I’m tentatively liking this proposal.

The need to write things like Sequence<String> is real and urgent. Existing approaches present some of Swift’s nastiest sticking points for those not deeply invested in understanding its type system. There’s a progressive disclosure cliff, as it were, in this area of the language.


I wondered a bit about a “named parameter” syntax like Sequence<Element: String>, with no change to protocol declaration syntax. This has two advantages: (1) no need to create this new notion of “primary” associated types, and (2) more obvious correspondence between the different associated types within a protocol. It does bug me in the proposal how much protocol Sequence<Element> looks like a generic protocol, and how much it hides the fact that Element is in fact exactly the same sort of beast as other associated types with the single exception of this convenience at the point of use.

However, the syntactic correspondence between Sequence<String> and Array<String> is compelling for usability reasons. Ultimately, even though it is more concise than Sequence<.Element == String>, the Sequence<Element: String> syntax still places knowledge burden about the protocol’s structure and about type system esoterica on library clients instead of library authors. So…yes, Sequence<String> for usability.


Despite that, I’m a bit uneasy about this new notion of “primary” associated types, and explicitly designating it. Without having thought hard about it at all, I wonder if there isn’t some way to infer from the protocol’s structure itself which types are primary?

For example, consider the declaration of Sequence:

public protocol Sequence {
  associatedtype Element
  associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
  …
}

Iterator depends on Element, but not vice versa. In the (two-node) dependency graph of associated types, Iterator has incoming edges, but Element does not.

One could (I think?) simply move the type constraint and get an equivalent protocol:

public protocol Sequence {
  associatedtype Element where Iterator.Element == Element
  associatedtype Iterator: IteratorProtocol
  …
}

But there is note of authorial intention in the decision to make Iterator’s declaration depend on Element instead of the reverse. Is that sufficient to infer that Element is primary?

Even as I type this out, I see some arguments against it: too much magic, too little syntactic correspondence between the protocol’s declaration and Sequence<Foo>. (Where’d the angle braces come from?) Still, I’d be curious to hear the authors’ own thoughts on the philosophy behind what it means for an associated type to be “primary,” the burden of introducing this additional concept, and in particular why inferring primary-ness is a bad idea. This might help ease my discomfort with introducing yet another entity to Swift’s type system.

6 Likes

I find this syntax a bit misleading cause usually when working with types, the colon means ‘conforms to’:

struct IntWrapper<Value: FixedWidthInteger> // …
function areEqual<T: Equatable>(a: T, b: T) -> Bool // …
2 Likes

Agreed, yet another reason the proposal’s syntax is the one to use.

1 Like

I'm -1 on this proposal as it stands right now.

At the risk of repeating what others already have said, I want to add my two cents here as to why I'm not in favor of this proposal.

As many others I am of the opinion that this proposal does two things in one:

On one side it gives us a powerful new feature, namely the ability to constrain associated types of opaque result types (which later will – hopefully – be generalized to existentials as well). It basically solves the problem that currently there is no way of writing the following (syntax is obviously made-up):

func foo() -> some Sequence where returntype.Element == String {
    // ...
}

I think that nobody claims that we do not need this feature, so this part of the proposal is fine.

However, the proposal comes with a catch: it gives us this feature with a really sugary-feeling syntax that makes the most common use cases possible but excludes some use cases that are more niche. Namely, it will only be possible to constrain associated types that are deemed to be 'primary' by the API designer.

I would much rather see the syntax that uses the names of the associated types as is described in the alternatives suggested and that most people who are currently against this proposal are reaching for:

func foo() -> some Sequence<.Element == String> {
    // ...
}

IMHO, this syntax has very clear advantages:

  • it is possible to constrain every associated type if necessary
  • it could also be used to constrain the associated type to a protocol (e.g. some Sequence<.Element: BinaryInteger>)
  • it is very similar to writing constraints in a where clause
  • it works with protocols that haven't adopted primary associated types yet

If we should choose this syntax, we could still add primary associated types + the sugar syntax alongside it (which then really is sugar and doesn't just feel like it), which would render most arguments against the more general syntax obsolete:

  • No visual clues at the protocol declaration about what associated types are useful.

    there are still primary associated types that help in guiding the API user towards the right types to constrain

  • The use-site may become onerous. For protocols with only one primary associated type, having to specify the name of it is unnecessarily repetitive.

    it will be a rare occasion that you have to use the general syntax (if the API designer has chosen the primary associated type(s) well) – if you have to do it, then it won't be a problem that the use-site becomes a bit onerous perhaps. This is the case with many features that have sugar syntax. Imagine having to write all optionals as Optional<String> instead of String? – it still is useful that the more general syntax exists.

  • This more verbose syntax is not as clear of an improvement over the existing syntax today, because most of the where clause is still explicitly written. This may also encourage users to specify most or all generic constraints in angle brackets at the front of a generic signature instead of in the where clause, violates a core tenet of SE-0081 Move where clause to end of declaration.

    I don't understand the first part of this argument at all. There is no existing syntax for this feature today, at least when it's used for opaque result types (and existentials).
    I also don't think that it is a bad thing when the user can decide if they want to write their generic constraints at the beginning or at the end of a function. Already today there is the choice to write a function like this:

    func foo<S1: Sequence, S2: Sequence>(_ s1: S1, _ s2: S2) {}
    

    But many people still choose to write it like this instead:

    func foo<S1, S2>(_ s1: S1, _ s2: S2) where S1: Sequence, S2: Sequence {}
    

    It depends on what style of programming you like and how long your constraints are and I don't think that pushing users to one of those styles is important here.


All in all I would say that I'd be in favor of this proposal if it actually adds the whole feature that the proposed syntax implicitly sugars. If we have that complete feature then I would be more than happy to also have the sugar alongside it.

11 Likes

@John_McCall writes in https://forums.swift.org/t/pitch-2-light-weight-same-type-requirement-syntax/55081/180:

3 Likes