SE-0393: Value and Type Parameter Packs

I think it would have to be otherwise we introduce yet another dimension for ranking where instead of a witness the solver would prefer variadic overload in some contexts, I vaguely remember that we have a similar issue already.

About alternatives: I'm only now reading about this topic and so it's possible that what I write has been taken into consideration and discarded, but couldn't using .. instead of ... solve the ambiguity problem while keeping the readability of the construct intact?

I personally dislike each as a pack declaration keyword. The only sense that it makes is when read in the context of an expansion such as repeat each T.

However did I miss it, why does the expansion cannot omit the each in such case and just let us write repeat T where T is already known to be a pack?!

If that reduction could be valid then each makes not that much sense. Personally I would be rather precise and call it pack.

func foo<pack T>(_: repeat T) -> T {}

repeat each T is way too mouthful and feels unnecessary to me.

Is there any difference between each T and T within the same context?

So let me repeat the questions, no pun intended. Why are we forced to replicate the each keyword everywhere? If there's no strong reason for that, why does it have to be each and not pack which is equally short and precise to the point?

To me the following would mean the same:

func bar<pack T>(_: repeat T)
func bar(_: repeat some pack T)

It does fit better with some than each does.

11 Likes

I love this functionality, really looking forward to being able to use it. The proposal is much clearer than the pitch, but I must confess I am still confused about all the terminology, especially since some of the concepts are merely abstract and don't seem to exist in the Swift world in some sense.

I think it would be tremendously helpful if there could be examples for each of the concepts under Here are the key concepts introduced by this proposal -- Maybe it's just me but I don't really understand the terms even after reading and mostly understanding the entire proposal, those descriptions are just too abstract.

And then about naming:

I agree with the above that repeat each seems superfluous, and a bit misleading. It's not actually a pack of "each T", right? It's a pack of T, which happens to be an "each", but we already know that. Isn't it a bit unusual to repeat the "declaring" keyword at the use site? Compare it to @escaping, inout, where X: MyProtocol etc. We use them in the parameter list, but not in the body or the return type, since we have already established what the things are.

And since we call these things packs, why not make that the keyword? pack T makes a lot more sense than repeat T, let alone repeat each T, especially since there is no actual repetition. Or go with many T or multiple T.

In short, something like

func < <variadic Element: Comparable>(lhs: { Element }, rhs: { Element }) -> Bool
func < <variadic Element: Comparable>(lhs: (pack Element), rhs: (pack Element)) -> Bool

seem clearer than

func < <each Element: Comparable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool

Personally I would strongly prefer using curly braces though, { T } is immediately obvious to anyone that has seen a mathematical set. It also allows us to physically enclose the thing that is packed, which is nice when it's a more complex expression.

7 Likes

Just a couple more cents on the naming. Referring to the variadic entity I as a non-native English speaker would refer to as "T is a parameter pack", but never would I use each anywhere or say something like "this is an each T". What is an EACH, it has barely any strong meaning besides in some numerical or counting context? pack seems like a more fitting keyword here. Feel free to disagree, but please provide a strong reason for that particular naming choice besides when reading repeat each T.

The following example from the future direction does not read well. I understand the intention, but it feels unnecessary to me.

func variadic<each T>(t: repeat each T) {
  let tt: repeat each T = repeat each t
}

I'd rather reduce it to something like this:

func variadic<pack T>(t: repeat T) {
  let tt: repeat T = repeat t // or even just `let tt: repeat T = t`
}

On top of that. Can't repeat be extended in the future to allow repeating non-variadic packs?

func foo(n: repeat Int) // or `repeat pack Int`
17 Likes

I share the same sentiment that I find each really hard to parse and agree that pack reads better. Furthermore, the future syntax for iterating packs also strikes me as very hard to read.

func allEmpty<each T>(_ array: repeat [each T]) -> Bool {
  for a in repeat each array {
    guard a.isEmpty else { return false }
  }

  return true
}

With a normal for loop it is almost like a normal English sentence but with repeat each it becomes hard to construct a sentence for this. For the for loop it seems like we could just have a for a in array and that should be able to "just" work?

Another thing that I didn't see mentioned is how this works with some and any? I was kinda expecting something like this to work.

func foo(
  firsts first: repeat each some Any,
  seconds second: repeat each some Any
) {
}

// or
func foo(
  firsts first: repeat some each Any,
  seconds second: repeat some each Any
) {
}

Edit:

Actually thinking about the some case some more. I would love to write just this:

func foo(
  firsts first: some pack Any,
  seconds second: some pack Any
) {
}
// If we elide some in the future this becomes even shorter
func foo(
  firsts first: pack Any,
  seconds second: pack Any
) {
}
7 Likes

Having two keywords that have to be next to each other seems quite verbose and not so swifty. Also not beginner or non-native English friendly.

It just seems verbose and complicated for doing what the programmer wants to do, and is closer to what the compiler is doing.

Also how will this work with replacing existing functions with multiple overloads of different arities? Eg if SwiftUI decides to replace its many functions that need this. Seems like a way to disambiguate is needed.
How will that work with backwards compatibility if they will be ambiguous. If you're targeting a lower iOS version than the version of SwiftUI that introduces these new functions?

8 Likes

This proposal does not include a spelling for same-shape requirements in the surface language; same-shape requirements are always inferred, and an explicit same-shape requirement syntax is a future direction.

I’ve got a (possibly) viable syntax for explicit same-shape requirement:

func foo<each (T, U)>(t: repeat each T, u: repeat each U) {
    let tup: (repeat (each T, each U)) = /* whatever */
}

Here we “group” each T and U into a pair (with tuple-like syntax), so we know at the first glance that each T and each U have exactly the same number of elements. This is said:

each (T, U) := each T, each U where shape(T) == shape(U)

I’m also worried about implicit inference of same-shape requirement because it doesn’t clearly represent user’s intention and the rules are not so natural for Swift. If possible, we should give an error and guide users to use explicit requirements from the very beginning.

3 Likes

No, .. is a valid custom operator, so it does not help with the ambiguity problem. The value of the repeat keyword is that we know syntactically that an expression is a pack expansion. Not needing operator lookup or overload resolution to determine whether an expression is a pack expansion is deliberate and necessary for type checking performance, diagnostics, code completion, etc. Personally, I also believe that repeat is more clarifying than any of the operator alternatives.

7 Likes

Overall I like the direction the syntax has gone in. I agree with others that each T as a declaration mechanism for packs is a little odd, but I don't know that I dislike it enough to suggest burning another keyword like pack. Maybe repeat T would be better than each T as the declaration syntax?

I strongly agree with the following rationale from the proposal:

However, the repeat keyword is explicit signal that the pattern is repeated under substitution, and requiring the each keyword for pack references indicates which types or values will be subsituted in the expansion.

Both the bounds of the expansion and the underlying types/values participating in the substitution are highly salient and should IMO be visible at a glance without digging into the declaration of all the identifiers within the expression.

That said, I also agree that repeat each T is a bit of a mouthful... to the extent that we think each is useful to indicate the types/values being substituted, I wonder if each in this position isn't carrying its weight and would benefit from a shorthand that drops each. If you're only expanding a pack directly, you would be allowed to write repeat T. And if the declaration syntax was changed to repeat then e.g. the zip declaration could look like:

func zip<repeat S>(_ sequence: repeat S) where each S: Sequence

Of course, we can always add this shorthand syntax in the future. I don't think I would ever want repeat each T to be illegal.

Last thought: we aligned on borrowing and consuming parameter modifiers—ought repeat perhaps be repeating in parameter position?

3 Likes

I don't get this argument, the alternative is to "burn" repeat and/or each, and will we really have a better place to use the keyword pack than for packs?

I don't really see why it would have to be seen as shorthand at all. That would depend on how we see each. In my mind it's analogous to the angle brackets around generic parameters:

func first<T>(elements: [T]) -> T?

Here <> around T indicates that it's a generic parameter, and they don't have to be repeated every time we refer to T. In fact, it's very similar in the sense that <T> is not a type, it's an indication that T is a placeholder for a type that can change between different use sites for this function.

I haven't completely wrapped my head around the meaning of each, but it seems to me that it is trying to be a bit like some, but I wonder if there shouldn't be a more explicit way to write it, like this, analogous to normal generics:

func count<variadic T>(elements: { T }) -> Int

// Or if we absolutely must have a keyword:
func count<variadic T>(elements: pack T) -> Int
1 Like

When it isn't just repeat each T, but something like repeat (each T, Bool), marking the components that are expanded differently improves the legibility a lot. I think Freddy is suggesting that we could keep that while allowing the simple case of repeat each T to be shortened to repeat T.

8 Likes

Yeah sure, that's sort of what I meant by "it depends on how we see each". I don't like the idea of being able to skip each at will, I would prefer, if possible, to get rid of each in that position (along the lines of the <> in <T>) altogether.

But again, I haven't completely understood its meaning yet. Will try to find more examples.

1 Like

In particular, this makes a huge difference in the expression syntax, where you'll have stuff like repeat (each collection).first a lot.

2 Likes

Can we use pack and unpack here?

Mind game: does this work?

func test<each T>() -> () -> repeat each T {
    repeat { each T }
}
1 Like

For pack expansions, it's critical that we use a real keyword and not a contextual keyword, because you can write any arbitrary expression as the repetition pattern. Adding a new, non-contextual keyword is a source breaking change. Repurposing repeat was opportunistic, but it also perfectly matches the programming model because pack expansions lower down into loops over each pack element, repeating the pattern at each iteration.

repeat { /*code with some pack elements*/ } is a pack expansion with a closure pattern. We cannot use {} as a value pack literal syntax because {} is already a closure literal, which is totally valid to write in pack expansion patterns.

5 Likes

I think what I meant is this (repeating the closure):

func test<each T>() -> repeat () -> each T {
    repeat { each T }
}

But even this won't work because the return type cannot be a pack of closures; it needs to be a tuple of closures, so like this:

func test<each T>() -> repeat (() -> each T) {
    (repeat { each T })
}

I was trying to come with something that would be confusing with the repeat-while loop, but ended up confusing myself. And the required parenthesis in the last incarnation disambiguates it from a repeat-while loop.

I hate to pile into the bikeshed, nevertheless:

It seems odd to use each inside the generic angle brackets, where it's a reference not to 'each' scalar type, but to the pack itself. On the other hand, each makes perfect sense as syntax for the pack expansion, where the pack will be expanded to a list of each scalar type from the pack. And since that meaning is marked by each, I'm not sure that repeat is necessary at all.

I would therefore suggest this syntax:

func < <pack Element: Comparable>(lhs: (each Element), rhs: (each Element)) -> Bool

and

func zip<pack S>(_ sequence: each S) where each S: Sequence

In other words, each alone (without repeat) would signal a pack expansion type. It's unambiguous (unlike '...'), and much harder to miss than a single subtle sigil like '*'.

This would reduce verbosity and get rid of the clumsy syntax repeat each P, which when read as an English command makes it sound like you're repeating each individual P type some unspecified number of times, as opposed to repeating the expansion type. We already use repeat in a completely different context (loops) with a completely different meaning. More importantly, I don't think repeat is the right concept here: it's not a matter of repetition, but (in my mental model at least) of expanding the 'variadicness' that's inherent in the pack (i.e. a list of scalar types) into a corresponding list of scalar types. So under the syntax I propose, you'd have:

func makePairs<pack First, pack Second>(
  firsts first: each First,
  seconds second: each Second
) -> (Pair<each First, each Second>) {
  return (Pair(each first, each second))
}

I haven't worked through the whole proposal considering this proposed syntax, so it's very possible I'm missing some good reason this won't work or is less clear than what the proposal envisions.

4 Likes

Ah sorry! I thought you were asking about using {} for pack references. Yes, pack expansions with closure patterns are valid under this proposal, though they do need to appear in a context that accepts a comma-separated list of things.

The while keyword and condition distinguish pack expansions with closure patterns from repeat-while loops. I don't think there's any parsing ambiguity.