Making a Class Iterable
Any class can be walked with for ... in. It takes two decorated methods
and no other ceremony: no interface to declare, no iterator object to
return, and no state kept between passes.
This section covers the protocol, the two shapes it usually takes, what happens when a piece is missing, and the rules that keep a custom iterator well behaved.
The Protocol
for value in thing is not magic. Zuri evaluates thing once, then
repeats three steps:
key = thing.@key(key), starting fromnil- stop if
keyisnil value = thing.@value(key), and run the body
So the two methods answer two separate questions:
@key(previous)— “given the last position, what is the next one?” It receivesnilon the first call and must returnnilwhen there is nothing left.@value(key)— “what is stored at this position?”
The loop never stores a cursor of its own. The key is the cursor, which
is why @key gets the previous one back each time.
A Counting Example
class Countdown {
@new(from) {
self.from = from
}
@key(previous) {
if previous == nil {
return self.from
}
if previous <= 1 {
return nil
}
return previous - 1
}
@value(key) {
return key * 10
}
}
for value in Countdown(3) {
echo value
}
30
20
10
Trace it once and the protocol stops being abstract. @key(nil) returned
3, so the first value is @value(3), which is 30. @key(3) returned
2, then @key(2) returned 1, then @key(1) returned nil and the
loop ended.
Note that the key and the value are genuinely different things here: the key counts down from three, the value is ten times it. Two variables in the loop give you both:
for key, value in Countdown(3) {
echo '${key} -> ${value}'
}
3 -> 30
2 -> 20
1 -> 10
Wrapping a Collection
The more common case is a class holding a list, where the key is an index. The shape is always the same three checks:
class Stack {
@new() {
self.items = []
}
push(value) {
self.items.append(value)
return self
}
@key(previous) {
if self.items.is_empty() {
return nil
}
if previous == nil {
return 0
}
if previous >= self.items.length() - 1 {
return nil
}
return previous + 1
}
@value(key) {
return self.items[key]
}
}
var stack = Stack().push('a').push('b').push('c')
for value in stack {
echo value
}
for index, value in stack {
echo '${index}: ${value}'
}
a
b
c
0: a
1: b
2: c
Each of the three checks in @key earns its place:
The empty check comes first. Without it, @key(nil) would return 0
for an empty stack and @value(0) would index past the end.
previous == nil starts the walk, and 0 is the first index.
previous >= length() - 1 ends it. Using >= rather than == means a
collection that shrank mid-loop still terminates.
An empty collection iterates zero times rather than failing:
for value in Stack() {
echo 'never printed'
}
echo 'done'
done
What You Get for Free
Defining both methods makes is_iterable() answer true, and makes the
class usable everywhere for is:
echo is_iterable(Stack().push('a'))
echo is_iterable(Countdown(1))
true
true
Both Are Required
for calls @key and then @value. Defining only one produces an error
naming the one that is missing:
class OnlyKey {
@key(previous) {
return previous == nil ? 0 : nil
}
}
catch {
for value in OnlyKey() {
echo value
}
} as e {
echo '${e.type}: ${e.message}'
}
PropertyError: undefined property '@value' on instance of 'OnlyKey'
A class with neither fails on @key instead:
class Plain {}
catch {
for value in Plain() {
echo value
}
} as e {
echo '${e.type}: ${e.message}'
}
PropertyError: undefined property '@key' on instance of 'Plain'
Rules Worth Knowing
A nil key ends the loop, always. That means a collection whose keys
could legitimately be nil cannot use them as keys. Use indices and look
the real key up in @value.
The instance is evaluated once. for x in build_thing() calls
build_thing() a single time, before the first pass, so @key and
@value are always called on the same object.
Neither method should mutate. They are called once per pass, in a loop
you do not control, and a @key with a side effect is a loop whose
behaviour depends on how many times something asked for the next key.
Iteration order is whatever @key says. There is no requirement to
count upwards, or to visit everything; Countdown walks backwards, and a
class could just as well skip, filter or repeat.