I agree there's a bit of an issue with regards to for() loop being syntactically sugared like this, and I feel it's a more fundamental issue. With the for-loop syntax I have a hard time understanding how you even assign a value to the correct tuple index while enumerating the types that compose that tuple without some painful mirroring or other thing like that.
Also based on the Future directions section of the proposal, it seems that adding support for applying the iteration over existing swift flow control checks will need to be added individually. I feel like it should be possible to make the syntax less sweet and a bit more specialized so it makes expansion more coherent.
I don't think that the syntax can be as simple as repeat { }
, not only is this already used in repeat { } while ;
which might render parsing difficult, but given how repeat
is used in other context, wouldn't it make more sense to define a pack iteration block like so:
func allUnwrapOrNil<each Element>(over element: (repeat each Element)?) -> (repeat each Element)? {
let result: (repeat each Element)
repeat each Element {
guard let value = each element else {
return nil
}
each result = value
}
return result
}
The idea is that you have to specify what parameter pack you are iterating over, then inside the block every call concern one element of that pack, if you're using a tuple whose pack expansion comes from the same type pack then you use the syntax each tupleVariable
as the lvalue just like proposed by @ellie20 otherwise you deal with the type as whole.
I'll reuse an example from the first proposal to show how you could get the same result as the print function on variadic tuples with this syntax:
func example<each T: CustomStringConvertible>(packElements value: repeat each T, tuple: (repeat each T)) {
var repeatEachValueComponents: [String] = []
var repeatEachTupleComponents: [String] = []
repeat each T {
repeatEachValueString.append((each value).description)
repeatEachTupleString.append((each tuple).description)
}
print("(", repeatEachValueComponents.joined(", "), ")")
print("(", repeatEachTupleComponents.joined(", "), ")")
var repeatEachValueTupleComponents: [String] = []
repeat each T {
var repeatEachComponents: [String] = []
repeat each T {
repeatEachComponents.append((each tuple).description)
}
repeatEachValueTupleComponents.append("(\(each value), (\(repeatEachComponents.joined(", "))))")
}
print("(", repeatEachValueTupleComponents.joined(", "), ")")
var repeatEachValueEachTupleComponents: [String] = []
repeat each T {
each repeatEachValueEachTupleComponents.append("(\(value), \(tuple))")
}
print("(", repeatEachValueTupleComponents.joined(", "), ")")
}
example(packElements: 1, 2, 3, tuple: (4, 5, 6))
// Prints the following output:
// (1, 2, 3)
// (4, 5, 6)
// ((1, (4, 5, 6)), (2, (4, 5, 6)), (3, (4, 5, 6)))
// ((1, 4), (2, 5), (3, 6))
I think this syntax has the advantage of not adding another sugar on the for
keyword, in addition it puts the pack being iterated over at the forefront of the enumeration rather than all the way at the back of the first line, it also circumscribes the whole code where "each x" will represent a single type of the type pack also allowing assigning values to corresponding tuple expansions.
Another advantage I thought about while writing this is that it does not require you to enumerate the values of the pack expansions at the outset. If a developer needs to enumerate various types and that one of the values is expensive to get but another check would remove the need to check for that value, it's beneficial to be able to choose when the value is extracted in one iteration.
Here is another convoluted example:
protocol SomeProtocol {
associatedtype OtherType
func cheapOperation() -> Bool
func costlyOperation() -> Element?
}
func performCostlyPackIteration<each Element: SomeProtocol>(packElements values: repeat each Element, otherElements otherValues: repeat each Element) -> (repeat each Element.OtherType?) {
let result: (repeat each Element.OtherType?)
repeat each Element {
if !(each values.cheapOperation()) {
each result = nil
continue
}
let costlyResult = otherValues.costlyOperation()
// Do something with the result like use switch { } guards and others.
each result = costlyResult
}
return result
}
The comment in there does a lot of work but the point is that the complexity could go very far and I'm not sure how this could be represented with the for loop syntax while allowing the user to delay the value extraction or even using the pack expansion syntax without making it unreadable.
Under this proposal and the newly added PR #2153, the implementations of Equatable
, Comparable
and Hashable
would look like this:
extension Tuple: Equatable where repeat each Element: Equatable {
static func == (lhs: repeat each Element, rhs: repeat each Element) -> Bool {
repeat each Element {
if each lhs == each rhs {
continue
}
return false
}
return true
}
}
extension Tuple: Hashable where repeat each Element: Hashable {
func hash(into hasher: inout Hasher) {
repeat each Element {
hasher.combine(each hasher)
}
}
}
extension Tuple: Comparable where repeat each Element: Comparable {
static func < (lhs: repeat each Element, rhs: repeat each Element) -> Bool {
repeat each Element {
if each lhs == each rhs {
continue
}
return each lhs < each rhs
}
}
}
I hope I conveyed my misgivings properly and please do tell me if I'm completely off.
Thank you for your kind attention.