SE-0393: Value and Type Parameter Packs

Sorry, I wasn’t clear.

I meant instead of each, can we use unpack.

Even though there are no parsing ambiguities, I still think that we should keep in mind that there is another use of the repeat keyword that does something completely different right now. If I were a beginner and saw either a repeat-while loop or a parameter pack for the first time, I would probably go to google and search for "Swift repeat". Right now, this obviously gives only results about the loop and I could imagine that it stays that way, because much more beginners use these kinds of loops than generics, let alone variadic ones.

4 Likes

It is necessary. Consider the following code (using your suggested syntax and the future direction of variadic generic types):

struct Foo<pack T> {
    let bar: (each T)

    init(bar: each T) {
        self.bar = (each bar)
    }
}

func foo<pack T, pack U>(t: each T, u: each U) -> (Int, (each T, Foo<each U>)) {
    (10, (each t, Foo(bar: each u)))
}

let baz = foo(t: 5, 2.5, u: "Hello", true)

What is the type of baz here? Is it

  1. (Int, (Int, Double, Foo<String>, Foo<Bool>)),
  2. (Int, (Int, Foo<String>), (Double, Foo<Bool>)),
  3. (Int, (Int, Double, Foo<String, Bool>)), or
  4. (Int, (Int, Foo<String, Bool>), (Double, Foo<String, Bool>))?

We could define it to be one of those and all the others couldn't be written in Swift at all.

EDIT:

With the currently proposed syntax all of these could be written like this
struct Foo<each T> {
    let bar: (repeat each T)

    init(bar: repeat each T) {
        self.bar = (repeat each bar)
    }
}

func foo1<each T, each U>(t: repeat each T, u: repeat each U) -> (Int, (repeat each T, repeat Foo<each U>)) {
    (10, (repeat each t, repeat Foo(bar: each u)))
}

func foo2<each T, each U>(t: repeat each T, u: repeat each U) -> (Int, repeat (each T, Foo<each U>)) {
    (10, repeat (each t, Foo(bar: each u)))
}

func foo3<each T, each U>(t: repeat each T, u: repeat each U) -> (Int, (repeat each T, Foo<repeat each U>)) {
    (10, (repeat each t, Foo(bar: repeat each u)))
}

func foo4<each T, each U>(t: repeat each T, u: repeat each U) -> (Int, repeat (each T, Foo<repeat each U>)) {
    (10, repeat (each t, Foo(bar: repeat each u)))
}

let baz1 = foo1(t: 5, 2.5, u: "Hello", true)
// (Int, (Int, Double, Foo<String>, Foo<Bool>))

let baz2 = foo2(t: 5, 2.5, u: "Hello", true)
// (Int, (Int, Foo<String>), (Double, Foo<Bool>))

let baz3 = foo3(t: 5, 2.5, u: "Hello", true)
// (Int, (Int, Double, Foo<String, Bool>))

let baz4 = foo4(t: 5, 2.5, u: "Hello", true)
// (Int, (Int, Foo<String, Bool>), (Double, Foo<String, Bool>))
There are even more possible interpretations of the first code snippet
  1. (Int, (Int, Foo<String>, Foo<Bool>), (Double, Foo<String>, Foo<Bool>)), or
  2. (Int, (Int, Double, Foo<String>), (Int, Double, Foo<Bool>))

They would be written as (Int, repeat (each T, repeat Foo<each U>)) and (Int, repeat (repeat each T, Foo<each U>)) respectively.

2 Likes

Just bikeshedding, I had a hard time digesting this pitch because of the non-obvious use of each and repeat. repeat each Element reads like we're inside a for-loop. I think there were a number of other spellings in the pitch thread that might be preferable.

4 Likes

Also unclear what value each provides:

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

func zip<S>(_ sequence: repeat S) where S: Sequence seems to provide the same type info, since repeat already indicated S is variadic.

Good point, thanks for clarifying that for me.

I wish we didn't have to use repeat, both because it already means something completely different and because the conceptual essence has more to do with expansion, unpacking, variadicity, etc., than with repetition. I do understand the point that it would be source-breaking to use a new keyword. But wouldn't it be a fairly mechanical fix to find conflicting instances of pack, unpack, variadic, or whatever term in a codebase and replace them with a different identifier?

2 Likes

I think it helps readability. While you can argue that types tend to be short, I think at least in expression context each is a valuable cue that tells you what you're expanding over.

You need the keyword at the declaration of the generic parameter at least. These mean different things:

func foo<each A, B>(_: repeat (each A, B))
func foo<A, each B>(_: repeat (A, each B))
func foo<each A, each B>(_: repeat (each A, each B))

You could argue that we should just write repeat (A, B) in all three cases at the usage site, but the declaration has to denote which parameters are packs.

2 Likes

repeat each is not a single production in the grammar; they don't always appear as consecutive tokens. repeat (each T, Int) means something other than (repeat each T, Int). If we substitute T for String, Bool, the first one becomes (String, Int), (Bool, Int) while the second is (String, Bool, Int).

7 Likes

I haven't read through this all with the depth that it certainly deserves, but I'd like to echo some others' sentiment that I have a very hard time parsing the use of repeat and each. Whereas func doSomething<T...>(_ ts: T...) makes perfect sense to me, I have a much harder time developing an intuition at a glance around the keywords in this pitch. I don't have any good alternative suggestions, but I would heartily support trying to find a syntax that reads a little bit more naturally, especially for folks that are just getting started writing some code that uses variadic generics.

9 Likes

FWIW, I find the repeat and each keywords to be fairly descriptive and clarifying. :man_shrugging:

5 Likes

I don't have much to add to the existing discussion, but I'll also say repeat and each have been much easier for me to parse than the previous ... version (and I've written C++ variadic templates that shipped in the Swift compiler). And while I agree that repeat each T definitely looks redundant and it would be nice to support shorthand for that case, the combination of repeat and each in more complicated expressions is the clearest version of pack expansion I've seen yet. repeat (each collection).count makes it very obvious (to me) what the overall "shape" of the repetition is and where the "hole" is that gets substituted with each member of the pack.

The closest feature I can think of to this is, oddly, Python's list comprehensions. Like repeat each x, x for x in xs does seem a bit redundant, but x.count for x in xs immediately makes sense. However, I've never loved the "suffix" nature of the Python syntax; I read x.count before it says what x is. repeat (each x).count is a little clunkier to say out loud, but is easier for me to read as code (and works better for things like code completion too).

23 Likes

I'm not familiar enough with the grammar rules of Swift to know, but does the keyword have to precede the type? There seems to be a lot of pushback on each T but I don't like the suggested pack T either because it reads like the verb form not as "a pack of Ts" to me. I wonder if pack following the constraint is a possibility. Because a T pack reads to me much more as "a pack of Ts" (and is composable for compound types like (T pack, Int) as well).

Does Swift grammar allow for this?

1 Like

This is a great idea, I'd be open to introducing this (but perhaps in a subsequent proposal since it's backward compatible and nicely self-contained).

I'd like to keep the inference around though. We already perform implicit inference for all other requirements. Which part about doing inference for same-shape requirements specifically doesn't feel natural to you?

1 Like

I do love the proposed two-keywords syntax. It is convincingly clear.

Some comments...

I think this syntax is reasonable and should be a must. until I found the description that same-shape requirement is always implicit, I got very confused about same-shape constraint. By using this format of same-shape requirement, it will be much easier to read.
There can be some concern on the expressional power of the each (T, U) syntax, because it cannot express something like shape(T) == shape(U) * 2. But as the proposal explicitly excluding such functionality, then each (T, U) syntax is almost self-evident solution.

A same-type requirement where one side is a type parameter pack and the other type is a scalar type that does not capture any type parameter packs is interpreted as constraining each element of the replacement type pack to the same scalar type:

func variadic<each S: Sequence, T>(_: repeat each S) where (each S).Element == T {}

This is called a same-element requirement.

A valid substitution for the above might replace S with {Array, Set}, and T with Int.

I'm not sure who mentioned this, but there were relating discussion in a previous thread on variadic generics. If it's a function declaration, you can write the same-element requirement this way, but it's verbose in type declaration. For example, if there is a struct Sequences<each S: Sequence, T> where S.Element == T, it is necessary to write Sequences<Set<Int>, Array<Int>, Int>, instead of Sequences<Set<Int>, Array<Int>>. It's not ideal.
The same can be said for same-type-pack requirement. I believe there should be some solution like where <T> or where (each S).Element == some Any,

  • The elements of an array literal, e.g. [repeat each value]

I have large interest in how [repeat each value] behave. How is the type of this array literal resolved? Like [any P]?

Didn’t even notice… I was surprised to see this

func foo<C: BidirectionalCollection>(_ collection: C) -> Set<C.Element?> {
  [collection.first, collection.last]
}
foo([(1, "a"), (2, "b")])

resulting in

/main.swift:4:1: error: type '(Int, String)' cannot conform to 'Hashable'
foo([(1, "a"), (2, "b")])
^
/main.swift:4:1: note: only concrete types such as structs, enums and classes can conform to protocols
foo([(1, "a"), (2, "b")])
^
/main.swift:1:6: note: required by global function 'foo' where 'C.Element' = '(Int, String)'
func foo<C: BidirectionalCollection>(_ collection: C) -> Set<C.Element?> {
     ^

Such inference sounds counter-intuitive for me, since the input is implicitly constrained by output (just as same-shape case). To avoid derailing the review thread, I may prefer the idea of continuing this topic in a separate thread.

I personally accept removing such inference as a breaking change in Swift 6. However, until the change really takes place, I tend to agree that implicit same-shape requirement is “natural” in Swift.

Very strong +1 from me. I have some syntax bikeshedding to add to the already ongoing discussion though ;)

Yes, definitely. The six overloads for tuple equality and 10 overloads for SwiftUI Views should be enough to convince everybody that this is an issue to solve sooner rather than later.

Mostly, yes. Improving the generics system is a long-awaited goal by now and with variadic generics (and maybe parameterized extensions) it will finally be as expressive and powerful as once envisioned in the generics manifesto. The syntax feels very Swift-like even though I have something to say about it at the end of my review.

I have used (and enjoyed) variadic templates in C++, but they get quite messy and are hard to understand as soon as they get a bit more complicated. Parameter packs as proposed here promise to be much clearer in use, as you can see at a glance which parts of your types/expressions are expanded into which outer scope exactly.

I read and participated in this thread as well as the pitch thread and studied the proposal text thoroughly. Additionally, I checked out the feature a little bit using the current snapshot.


I find it rather unpleasant that we have to use repeat as a keyword for this, if we want to use one (and I definitely prefer that), because it doesn't really convey the true meaning it has for me. It would be much nicer to use something like unpack or expand (which should be quite uncommon variable names but maybe not so uncommon function names). Before we definitely settle for repeat we should at least assess the source-breaking impact of some of those other keywords, but if it would be too bad I could live with repeat of course.
In the case of each I'm in the same boat as many others in that I would prefer to use pack for this. IMHO, this would also work if we used repeat and just feels clearer to me, especially at the declaration site.


While experimenting with this feature, I quickly reached the limits of what is possible with this current feature set, so I hope that followup-proposals arrive rather quickly :)

But regardless of that I want to thank the proposal authors already for tackling such a long lasting pain point in the language!

1 Like

I have a quick question. AFAICS, the following code should work under the current proposal already, right?

func foo<each T, R>(f: @escaping (repeat each T) -> R) -> (repeat each T) -> R {
    { (t: repeat each T) -> R in
        let tuple = (repeat each t)
        print(tuple)
        return f(repeat each t)
    }
}

Because, using the most recent snapshot, this currently crashes the compiler:

Assertion failed: (def->getFunction() == &F && "def of root local archetype is in wrong function"), function addTo, file SILInstructions.cpp, line 118.
2 Likes

expand is a great alternative. repeat is too evocative of a while loop or for loop

4 Likes

2 things form my side:

  1. Like others in this thread mentioned, do we really need each? The compiler should be smart enough to infer it. Readability/clarity doesn't seem to improve when using it imo (@Slava_Pestov). Also, we have a lot of other unrelated keywords that we use in our function declarations: at some point lack of brevity starts to damage clarity.

  2. each and repeat are both usually used when referring to a sequence of instructions (in other languages + common language), which makes them a less than ideal pick. I'd go with something like fold

Perhaps something as concise as:

func zip<fold T, fold U>(firsts: T, seconds: U) -> fold (T, U) {
  return fold (firsts, seconds)
}

Also, "folding types" matches nicely what happens in common language, I think.