SE-0398: Allow Generic Types to Abstract Over Packs

Hello Swift community,

The review of SE-0398: Allow Generic Types to Abstract Over Packs begins now and runs through May 8th, 2023.

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 via the forum messaging feature or email. When contacting the review manager directly, please put "SE-0398" in the subject line.

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/main/process.md

Thank you,

Frederick Kellison-Linn
Review Manager

19 Likes

Highly positive! This is a natural follow-on to the parameter packs proposal, and (to me) a necessary part of variadic generics. I'm glad to see it up for review.

I have a few comments on narrow issues, but nothing that should prevent this proposal from being accepted.

In the requirements section, there is this restriction that I don't understand:

i. The inferred requirement is invalid if it contains multiple pack elements captured by expansions at different depths.

I thought that it would be acceptable to have a shape constraint that equates the shapes of packs at different depths, so why would the inferred requirement need to be invalid?

In the section on typealiases, the generic parameter packs are missing. This:

typealias Element = (repeat each S.Element)
typealias Callback = (repeat each S) -> ()
typealias Factory = Other<repeat each S>

should probably be

typealias Element<each S: Sequence> = (repeat each S.Element)
typealias Callback<each S> = (repeat each S) -> ()
typealias Factory<each S> = Other<repeat each S>

The section on classes has this restriction:

While there are no restrictions on non-final classes adopting type parameter packs, for the time being the proposal restricts such classes from being the superclass of another class.

This has the feel of being a limitation on the current implementation (i.e., nobody has implemented this bit yet) vs. a fundamental design issue that needs to be sorted out. Are there unknowns here that could mean that we have this restriction forever, or would somehow be unable to handle inheritance? If not, I'd rather not have the restriction in the proposal itself, and leave it to a compiler release note.

Doug

2 Likes

I feel like proposals should reflect the state of the language as implemented. While override matching in the presence of pack substitution feels like it makes sense and there’s one “obvious” (at least to type checker developers) way it should work, I think it’s worth spelling out the precise behavior in a proposal. It’s a bit more than a release note, but perhaps just barely so.

2 Likes

You can have a same-shape constraint between packs at different lexical depths in the generic signature. The restriction in the proposal is using the term "depth" differently, to describe nested pack expansions.

For example, I can write this:

struct Outer<each T> {
  struct Inner<each U> where repeat each T == each U {}
}

The requirement repeat each T == each U states that each T and each U are exactly the same pack; T and U have the same shape, and each element of T is equivalent to each element of U at the same position.

You can also state that two packs have unified elements using an intermediate scalar type parameter:

struct Outer<each T> {
  struct Inner<each U, Element> where repeat each T == Element, repeat each U == Element {}
}

The requirements repeat each T == Element, repeat each U == Element state that the elements of T and U are all equivalent, but T and U are not required to have the same shape. There's no way to write a requirement that unifies all elements of T and U without an intermediate type parameter, e.g. Element in the above example, but such a requirement is possible theoretically through requirement inference involving nested requirement expansions:

struct Unify<Element, each T> where repeat each T == Element {}

func unifyAllElements<each U, each V>(_: repeat Unify<each U, repeat each V>))

The application of Unify inside of a pack expansion would infer the requirement repeat each T == Element where Element := each U and each T := Pack{repeat each V} for each element of U. This effectively says that all elements of each U and each V are the same type, but without requiring them to have the same shape. Concretely, given each U := Pack{X, Y, Z}, each V := Pack{A, B}, we end up with 6 expanded requirements:

   repeat Unify<each U, repeat each V>, each U := Pack{X, Y, Z}, each V := Pack{A, B}
-> Unify<X, A, B>, Unify<Y, A, B>, Unify<Z, A, B>
-> A == X, B == X, A == Y, B == Y, A == Z, B == Z

This isn't a requirement that's supported in our generics system - each mention of each U or each V is effectively reduced to a scalar type parameter, but we have no such type parameter in the signature of unifyAllElements to reduce these types down to.

So, repeat Unify<each U, repeat each V> has a nested pack expansion, and the proposal is using "depth" to refer to the nesting level of these expansions. The outer expansion that iterates over each U has an expansion depth 0, and the inner expansion that iterates over each V has an expansion depth 1. The inferred requirement ends up capturing each U at expansion depth 0, and each V at expansion depth 1, and equating pack elements at different expansion depths is when we can end up with this funky pack element unification with no type parameter to describe the reduced type.

4 Likes

The language workgroup discussed this proposal and has decided to accept it.

2 Likes

A generic type is variadic if it directly declares a type parameter pack with each , or if it is nested inside of another variadic type. In this proposal, structs, classes, actors and type aliases can be variadic. Enums will be addressed in a follow-up proposal.

Does this mean if I define a variadic generic type that’s a wrapper for enums it won’t work?