Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Counter as variable in for-in-loops

When normally using a for-in-loop, the counter (in this case number) is a constant in each iteration:

for number in 1...10 {
    // do something
}

This means I cannot change number in the loop:

for number in 1...10 {
    if number == 5 {
        ++number
    }
}

// doesn't compile, since the prefix operator '++' can't be performed on the constant 'number'

Is there a way to declare number as a variable, without declaring it before the loop, or using a normal for-loop (with initialization, condition and increment)?

like image 631
Marcus Rossel Avatar asked Jan 31 '15 11:01

Marcus Rossel


2 Answers

To understand why i can’t be mutable involves knowing what for…in is shorthand for. for i in 0..<10 is expanded by the compiler to the following:

var g = (0..<10).generate()
while let i = g.next() {
    // use i
}

Every time around the loop, i is a freshly declared variable, the value of unwrapping the next result from calling next on the generator.

Now, that while can be written like this:

while var i = g.next() {
    // here you _can_ increment i:
    if i == 5 { ++i }
}

but of course, it wouldn’t help – g.next() is still going to generate a 5 next time around the loop. The increment in the body was pointless.

Presumably for this reason, for…in doesn’t support the same var syntax for declaring it’s loop counter – it would be very confusing if you didn’t realize how it worked.

(unlike with where, where you can see what is going on – the var functionality is occasionally useful, similarly to how func f(var i) can be).

If what you want is to skip certain iterations of the loop, your better bet (without resorting to C-style for or while) is to use a generator that skips the relevant values:

// iterate over every other integer
for i in 0.stride(to: 10, by: 2) { print(i) }

// skip a specific number
for i in (0..<10).filter({ $0 != 5 }) { print(i) }

let a = ["one","two","three","four"]

// ok so this one’s a bit convoluted...
let everyOther = a.enumerate().filter { $0.0 % 2 == 0 }.map { $0.1 }.lazy

for s in everyOther {
    print(s)
}
like image 192
Airspeed Velocity Avatar answered Sep 29 '22 16:09

Airspeed Velocity


The answer is "no", and that's a good thing. Otherwise, a grossly confusing behavior like this would be possible:

for number in 1...10 {
    if number == 5 {
        // This does not work
        number = 5000
    }
    println(number)
}

Imagine the confusion of someone looking at the number 5000 in the output of a loop that is supposedly bound to a range of 1 though 10, inclusive.

Moreover, what would Swift pick as the next value of 5000? Should it stop? Should it continue to the next number in the range before the assignment? Should it throw an exception on out-of-range assignment? All three choices have some validity to them, so there is no clear winner.

To avoid situations like that, Swift designers made loop variables in range loops immutable.

like image 26
Sergey Kalinichenko Avatar answered Sep 29 '22 16:09

Sergey Kalinichenko