Search code examples
swiftiteratorsequenceswift-protocolsfor-in-loop

a constant sequence can iterate using for-in loop, but can't call next() directly?


In the following code, c is a constant sequence (an instance of Countdown), it can iterate through its elements and break when the condition is met, and it can iterate from the start again.

But when I call c.next() directly, I get a compiler error: cannot use mutating member on immutable value.

So, I have two questions:

  1. If I cannot call c.next(), why can it iterate through all the elements in the first place? Isn't it using the next() method internally to iterate through them?
  2. In the second for-in loop in the following code, why is it not counting from 1 where the first iteration left off, instead it counts from the start, which is 3?

struct Countdown: Sequence, IteratorProtocol { 
    // internal state
    var count: Int
    // IteratorProtocol requirement
    mutating func next() -> Int? {
        if count == 0 { return nil } else {
            defer { count -= 1 } 
            return count
        }
    }
}

// a constant sequence
let c = Countdown(count: 3)

// can iterate and break
for i in c { 
    print(i)              // 3, 2
    if i == 2 { break }
}

// iterate again from start (not from 1, why?)
for i in c { print(i) }   // 3, 2, 1

// ⛔️ Error: cannot use mutating member on immutable value.
//          `c` is a `let` constant.
c.next()


Solution

  • You can't call next because next is mutating. next changes the state of the iterator, so that it "moves closer" to the end.

    The for loop doesn't call next directly. It creates a mutable copy of c (var), and calls next on that:

    // The for loop basically does this:
    var copy = c // creates a copy
    var element = copy.next()
    element = copy.next()
    element = copy.next()
    ...
    

    The reason why the second for loop starts from the beginning is because of exactly this. for loops don't actually change the state of the thing you are iterating over. They create a copy, work with the copy, then throws it away.

    One way to avoid this copying behaviour is to make Countdown a class:

    class Countdown: Sequence, IteratorProtocol {
        // internal state
        var count: Int
        // IteratorProtocol requirement
        func next() -> Int? {
            if count == 0 { return nil } else {
                defer { count -= 1 }
                return count
            }
        }
    
        init(from n: Int){
            count = n
        }
    }