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

Viewed 84

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()

2 Answers

Because each for loop is creating a new iterator by calling the makeIterator() function of your sequence, which is defined by the standard library in a conditional expression of Sequence, but only when the conforming sequence also conforms to IteratorProtcol, as yours does.

Desugaring the for loops reveals the problem:

let c = Countdown(from: 3)

var iterator1 = c.makeIterator()
while let i = iterator1.next() { 
    print(i)              // 3, 2
    if i == 2 { break }
}

var iterator2 = c.makeIterator()
while let i = iterator2.next() { print(i) }   // 3, 2, 1

There are two separate iterators made, one per for loop. Each one "starts fresh" by virtue of originating from a copy of c (which has never been mutated).

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
    }
}
Related