Groovy from the ground up

Loops and ranges

Repeating work four ways — for-in over a range or list, each over a list, times for a count, while for an unknown number of passes — and how to leave early.

Part 6 of 12

Beginner15 min readBack to Courses

Ranges, and for-in

1..5 is a range: every whole number from one to five, both ends included, and 1..<5 stops before five. A range can be walked, collected, turned into a list, and used as a switch case — it is a value, not a loop. That is why (1..5).contains(3) is a question you can ask without writing a loop, and why printing a range shows [1, 2, 3, 4, 5] rather than the two dots.

for (n in 1..5) walks it, one pass per number. The same syntax walks a list: for (name in names). When the number of passes is known and each pass needs the item, this is the plainest loop there is and it reads like the sentence it is.

each, and times

list.each { ... } hands every item to a closure. The block is the body, and the current item arrives as it unless you name it yourself. It reads well for "do this to everything", and it is the method the next chapter builds on. It also answers the collection it walked rather than the block’s last value, so a line that assigns the result of each gets the original list back.

3.times { ... } runs a block three times, counting from zero, and eachWithIndex gives you both the item and its position. These are not two languages: each and times are ordinary methods with a closure argument, which is why they can be chained and why they cannot be steered with break.

while, and leaving early

while (fuel > 0) checks before each pass and is the right loop when the count is not known at the start — something burns down until it runs out. The body has to change whatever the condition watches, or the loop never ends. A while that never ends shows up as a Run that never returns, and the fix is always the same: find the value the condition watches and change it inside the body.

break leaves the loop, continue skips to the next pass, and both work in for and while. They do not work inside each: the block is a closure, not a loop body, so a break there is refused at compile time with "break statement is only allowed inside loops or switches". When a loop must exit early, use for.

Four loops, one exit

for (i in 1..3) {
    println 'pass ' + i
}

def totals = [2, 4, 6]
totals.each { println it * 10 }

2.times { println 'again' }

def n = 0
while (n < 3) {
    n = n + 1
}
println 'n = ' + n

for (c in ['a', 'b', 'c']) {
    if (c == 'b') continue
    print c
}
println()

A range is a value

1..5 is an IntRange, an object with methods: sum(), collect { }, contains(3), toList(). That is why it can be a switch case and why (1..10).sum() needs no loop. for (n in 1..5) walks it with an iterator, and each walks it with a closure; both visit the same numbers.

1..<5 is the same object with the right end excluded. The half-open form is the one to reach for when the end is a length: 0..<list.size() visits every valid index exactly once, while 0..list.size() visits one too many and throws.

Loops and closures are different tools

for and while are statements. They can break, continue and return from the enclosing method, and the compiler knows they are loops. each, times and eachWithIndex are methods that call a closure; the closure is a separate piece of code, so break and continue have nothing to attach to.

That difference decides which one to use. If the loop must stop early, or skip based on a condition, use for. If the job is to do something to every item, each reads better and chains with the collection methods. times is for a count with no collection behind it.

Three loop mistakes: 1..list.size() is one past the last index, so use 0..<list.size(); break inside each is a compile error, because the block is not a loop; and removing items from a list while each walks it skips the next item. Collect the removals and apply them after the loop.

Try these next

  • Print the sum of 1..100 with sum() and again with a while loop.
  • Walk a list with eachWithIndex and number the lines from 1.
  • Write a nested loop that prints a 3 by 3 grid of products.

Count, walk, repeat, drain

Run it and match each block to the output. Then change the first range to `1..<3` and see what stops happening, and make the `while` loop count up from 0 to 3 instead of down from 3.

Output
Click "Run" to execute your code

A table, a running total, a numbered list

Run it and check one row of products by hand. Then make the table go to 5 instead of 3, and change `eachWithIndex` so the count starts at 0.

Output
Click "Run" to execute your code

A loop that will not end is not a mystery: the condition is still true after the body, so something in the body was supposed to change it. Print that value inside the loop and the answer is on the screen.

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What is the difference between 1..3 and 1..<3?