Ranges and Iteration
Ranges
a..b builds a range value: the integers from a up to but not including
b.
.. binds tighter than a method call, so a range takes a method directly
with no parentheses around it:
echo 1..5.to_list()
echo 1..10.step(3).get_step()
[1, 2, 3, 4]
3
Parenthesise only when the bounds are themselves expressions, since ..
takes primaries: (n * 2)..(n * 3).
A property access and a call are expressions too, so
0..self.sizeand0..items.length()are(0..self).sizeand(0..items).length(). Write0..(self.size)and0..(items.length())when the bound is the property rather than the range.This is the price of the line above it.
..has to bind tighter than.for1..10.step(3)to mean a range that steps, and once it does, there is no way for0..self.step(5)to mean the instance’s ownstepinstead. The parentheses say which one is meant, and the compiler cannot guess.
var r = 1..10
echo r
echo r.lower()
echo r.upper()
echo r.range()
1..10
1
10
9
range() is the distance between the bounds.
A range is a value, not loop syntax. Store it, pass it to a function, return it:
def page(number) {
var size = 20
return (number * size)..((number + 1) * size)
}
echo page(2)
40..60
Counting Down
Write the larger bound first and the range runs backwards:
for i in 10..1 {
echo i
}
10
9
8
7
6
5
4
3
2
The rule is the same in both directions: the first bound is included, the second is not.
Stepping
for i in 1..10.step(3) {
echo i
}
1
4
7
step() gives back a new range carrying that stride. get_step() reads
it; the default is 1.
to_list() materialises a range into a list at stride one, ignoring any
step you set:
echo 1..5.to_list()
[1, 2, 3, 4]
Membership
within() tests against the bounds inclusively on both sides, and it
normalises direction, so 10..1.within(10) and 1..10.within(10) both
answer the same:
var r = 1..10
echo r.within(5)
echo r.within(10)
true
true
That differs from iteration, which excludes the upper bound. When you want
“would the loop visit this number”, compare against lower() and upper()
yourself.
Walking a Range
for with one variable gives you the values, and with two it gives you
the position first and the value second:
for value in 3..6 {
echo value
}
for index, value in 3..6 {
echo '${index}: ${value}'
}
3
4
5
0: 3
1: 4
2: 5
The index counts from zero regardless of where the range starts, which is
what makes it useful: 3..6 yields values 3, 4, 5 at positions 0, 1, 2.
loop() is the callback form. It honours the step and the direction:
25..18.loop(@(i) { print('${i} ') })
print('\n')
25 24 23 22 21 20 19
An iter loop needs no range at all — its three clauses already say
everything a range says, and more, since the step can be any expression:
iter var i = 3; i < 6; i++ {
echo i
}
3
4
5
Use a range with for when the bounds are the interesting part, and iter
when the stepping is.
What Is Iterable
for ... in works on:
- lists, giving index and value
- dictionaries, giving key and value, in insertion order
- strings, giving character position and character
- bytes, giving index and the numeric byte
- ranges, giving position and value
- any class that defines
@key()and@value()
is_iterable() answers the question for any value:
echo is_iterable([1, 2])
echo is_iterable('text')
echo is_iterable(42)
true
true
false
The Iterator Protocol
for is not magic. It desugars into a loop over two method calls.
Given for value in thing, Zuri evaluates thing once, then repeats:
key = thing.@key(key), starting fromnil- stop if
keyisnil value = thing.@value(key)- run the body
So @key(previous) answers “what comes after this one?”, returning nil
when there is nothing left, and @value(key) answers “what is stored
here?”.
Everything built in implements this. So can your own classes, which is what Chapter 6 shows.
Because the iterable is evaluated exactly once, this is safe:
for line in read_the_whole_file() {
echo line
}
The function runs once, not once per line.