Why does this iterator fail to update correctly?

This iterator got stuck after the first iteration for me:

Definition:

struct MyInterval
    trange :: Tuple{Number,Number}
end

function Base.iterate(m::MyInterval, state=m.trange[1])
    if state >= m.trange[2]
        nothing
    else
        mnew = MyInterval(m.trange .+ (0.1, 0))
        (mnew, state+0.1)
    end
end

Test:

M = MyInterval((3,4))
for m in Base.Iterators.take(M,4)
    println(m)
end

Output:

julia> 
MyInterval((3.1, 4))
MyInterval((3.1, 4))
MyInterval((3.1, 4))
MyInterval((3.1, 4))

Modifying the iterator update as follows makes it work correctly:

function Base.iterate(m::MyInterval, state=m.trange[1])
    if state >= m.trange[2]
        nothing
    else
        mnew = MyInterval((state+0.1, m.trange[2]))
        (mnew, state+0.1)
    end
end

This differs only in one line:

old: mnew = MyInterval(m.trange .+ (0.1, 0))
new: mnew = MyInterval((state+0.1, m.trange[2]))

Your code returns mnew, but this doesn’t change the object passed to the next call to iterate. The iterator object always stays the same for the whole loop, only state changes. The first return value is not a new iterator, but is instead the next element in the collection.

The iterable instance isn’t typically updated, it usually just holds the minimum information needed to set up the iteration. I can’t tell what your intended output is, so I’ll make one up. Let’s say I want to count upwards from 4 to 7. I just need a struct that contains the first and last integer, and an instance can hold 4 and 7.

iterate returns a tuple of the iteration item and iteration state, or it returns nothing when iteration is over. The items are the values I want: 4 5 6 7. So iterate would return (4, 1), (5, 2), (6, 3), (7, 4), nothing.

The state is what is used to keep track of how far the iteration goes. iterate does not take in the items, it only takes in the iterable instance and the previous state. In the example above, the previous state determines the result tuple with (iter.first + state, state + 1), though iter.last should be used to check if the iteration should end.

The state could have been implemented with different values for a different algorithm.

@stevengj I’m not understanding how that explains the difference between the version that works and the one that doesn’t.

In both cases my code returns mnew:

old: mnew = MyInterval(m.trange .+ (0.1, 0))
new: mnew = MyInterval((state+0.1, m.trange[2]))

In the first case, you use the value m.trange to create mnew, and m.trange never changes (in your example it is always 3), because m always refers to the original MyInterval instance.

In the second case, you use the value state instead of m.trange. The state changes with every iteration.

1 Like

Thanks to everyone for explaining. This took me a while to wrap my head around.