Parameter packs are an exciting new feature in Swift, but they quickly become very unintuitive when it comes to trying to express list operations on them.
Currently, repetition patterns with packs are limited to expressions. This does not allow for short-circuiting with break or continue statements, so the pattern expression will always be evaluated once for every element in the pack. The only way to stop evaluation would be to mark the function/closure containing the pattern expression throwing, and catch the error in a do/catch block to return, which is unnatural for Swift users.
I have been working together with Holly on a new Pack Iteration proposal, which will enable iterating over each element in a value pack and bind each value to a local variable using a familiar for-in syntax!
Cool. It's especially nice that, when iterating a tuple of 2 identical packs, it is able to determine that the types of each pair of elements are the same.
I have a couple of questions.
Q: Is it possible to give types in the pack a name?
func iterate<each Element>(over element: repeat each Element) {
for element in repeat each element {
// Can I give the type of 'element' a name here?
}
}
Q: Have you given any thought to expanding this to existentials? There is a great deal of symmetry between:
func iterate<each Element: P>(element: repeat each Element) {
for x in repeat each element {
// the type of 'x' is (effectively) 'some P',
// where the underlying type changes with each iteration.
}
}
And
func iterate(elements: some Sequence<any P>) {
for x in elements {
// the type of 'x' is 'any P' -- which we can unbox within the loop body to 'some P'.
// Again, the underlying type changes with each iteration.
}
}
func allUnwrapOrNil<each Element>(over element: repeat (each Element)?) -> (repeat each Element)? {
let result: (repeat each Element)
for (value, resultTarget) in repeat (each element, each result) {
guard let value else {
return nil
}
resultTarget = value
}
return result
}
This can be just a bikeshedding, but I think generalized repeat syntax instead of for ... in can be a good alternative.
func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool {
repeat {
guard each lhs == each rhs else { return false }
}
return true
}
This way, current repeat each element can be considered 'expression' version of repeat statement, which makes much consistency with if and switch.
You can use the same trick to “lift” the local type to a named type parameter that works with existentials; define a generic function (local or top-level) that takes a ‘T’ with the requirements you want (which must be satisfied by the requirements of the existential/pack element); then you call the function with this value, and now inside the function you refer to ‘T’.
To nitpick, I believe repeat {...} is ambiguous with a repeat expression where the pattern is a closure (that's not something we support right now, but it can be understood as a simple transformation where captured pack references are lifted to closure parameters passed in on each iteration). However, allowing repeat on statements, so you'd write repeat if let ... and so on, is something @hborla and I have discussed before.
It would make pack iteration look almost like control structures of languages with first-class generators, such as Icon. It's an expressive and consistent model but I worry it might be a bridge too far for programmers who are accustomed to imperative 'for' loops though!
I’m glad to see this rough edge being addressed, it was one of the confusing things when playing around with parameter packs for the first time and led to unnecessary redirection. I’m all for this improvement.
I feel like it's a bit awkward to write for element in repeat each element { ... }.
Maybe there should be a shorthand that looks like this? for each element { ... }
It seems like this specific example is a bit misleading (we might want to change it😅). To clarify, in for element in repeat each element { ... }, the element after the for keyword is a local variable that we're binding the ith element of the pack element (after repeat each keyword, which is of type each Element) to.
The main idea behind this proposal is to be able to bind values from a pack expansion expression to a local variable. We need the repeat each <pack> syntax in the for-in to specify that the source of our loop is a pack expansion type. Next, we absolutely need to include the name of our local variable after the for keyword so we can apply statements to it.
I understand that; repeat each /* name */ is a pack expansion expression, and you would need to name the local variable; in the shorthand each element of the pack would shadow the name of the pack.
I’ll admit I haven’t been keeping up with the progress on variadic generics and pack expansions.
However, from a purely syntactic perspective, looking at this example in the proposal:
func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool {
for left, right in repeat (each lhs, each rhs) {
guard left == right else { return false }
}
return true
}
It seems to me that there should be parentheses around “left, right”. As in:
for (left, right) in repeat (each lhs, each rhs) {
...
}
I don’t know exactly what’s happening “behind the scenes” to make this all work, but from the perspective of a programmer reading or writing the code, it sure looks like the thing being iterated over has the shape of “a collection of 2-tuples”.
Thus, each element should be a 2-tuple, and naming the parts left and right looks like standard tuple destructuring. Moreover, if one didn’t want to destructure, it seems like it should be possible to write:
Aye, but it can be annoying to have to do that. For instance, within the loop body, I may want to write things like:
let value: ElementType
if thing {
value = ...
} else {
value = ...
}
// or:
var values = [ElementType]()
There are tricks, but it doesn't feel as harmonious when you're forced to adapt your regular coding style to gaps in the language. This kind of code works in other generic contexts, but in these contexts you need to use a more awkward formulation due to not being able to give the element type a name.
The inability to introduce local generic types has been a hole in the language for a long time, and it significantly affects the ergonomics of both variadic generics and existentials. That is why, a long time ago, I suggested that this feature would actually be a good first step towards introducing variadic generics, and I mention it at every opportunity since then :
So as nice as this feature is (and it is very nice), I kind of do need to express some slight disappointment that we're not truly tackling local generic types while we do it. Instead, we're carrying on the trend of making generic type parameters unutterable - and in doing so, needlessly restricting the kinds of patterns developers are able to express.
I'm fully okay with that being a future direction, but I think we should acknowledge that pack iteration is not "finished" until we can name the generic types bound on each iteration of the loop.
We'll still need a way to give them a name, though. Otherwise we're still in the situation where we have unutterable types that only have meaning in single expressions when the compiler can infer some underlying type to bind them to:
for element: some P in repeat each element {
// This will work:
var array = [element] // Type of 'array' is Array<some P>
// This won't:
var array = Array<some P>() // This type is meaningless. Doesn't compile.
array.append(element)
}
I also can't refer to associated types of the element's type, because I cannot utter it:
func doSomething<each C: Collection>(_ packOfCollections: repeat each C) {
for collection: some Collection in repeat each packOfCollections {
let i: (some Collection).Index // Meaningless type.
}
}
Again, this kind of code works in other contexts where generic parameters are declared, but we need to give a name to the element's type so that we are able to refer to it within the loop body.
We've considered syntaxes like this in the past:
for<T> element: T in repeat each packOfElements {
var array = Array<T>() // Now we can refer to 'T'!
}
But that's a matter of bikeshedding. There are other possibilities for where to put the angle brackets that might look nicer than that. It's the functionality that I care about more than anything, and that it feels consistent with the rest of the language -- you should be able to copy and paste generic code in to the loop body, and assuming the names line up, it should just work.
I think this suggestion overall makes sense, but it seems like there's a semantic issue: in the existing conventions for parameter packs introduced with SE-0393 we follow the pattern of using plural form for naming parameter packs. So in this example, we should really rename the parameter pack to elements. So in for each elements version, the local variable name elements will not make sense, unless you will name the parameter pack element (as this proposal mistakenly does), which contradicts the precedent set by the proposal.
@xAlien95 is right, my recommendation (and what I wrote deliberately in the examples of SE-0393) is to use the singular naming convention for parameter packs. The note on naming convention (linked above) that I wrote still applies even though the syntax has since changed!
This is a great addition to the current parameter pack feature. I understand the reasoning behind the proposed syntax for element in repeat each element { ... } is to try to fit for...in loop in the expansion of the parameter pack repeat each element.
However, it does look odds with for element in repeat each element { ... }. Instead of introducing this special for...in loop, how about introducing a special for...each loop for this purpose like below: for element = each element { ... }
For pairs: for (element1, element2) = each (element1, element2) { ... }
Thank you! Since for element in repeat each element will probably be the most common use case of this feature, I think adding for each element syntactic sugar actually makes sense, and we can add your suggestion to the future directions of the pack iteration proposal
Back in the review of SE-0393, I noted that I was sad not to have pack iteration in the initial incarnation of variadic generics, so I'm thrilled to see it here. This is one of those interesting features where the expression in the language is so clear and obvious, and yet the fact that it works feels almost magical.
Your pitch is clear and straightforward, and I'm looking forward to getting to play with an implementation.