Control Flow · about 40 min

Loops: loop, while, break and continue

Why this matters

Computers are useful because they repeat work tirelessly: process every line of a file, retry a connection, iterate a simulation until it settles. Rust has three loop forms, loop, while and for. This lesson covers the first two and the keywords that steer any loop; the next lesson is about for, the one you will use most for walking over data.

loop: repeat forever, until break

fn main() {
    let mut n = 1;
    loop {
        println!("n = {n}");
        n *= 2;
        if n > 20 {
            break;
        }
    }
    println!("stopped at {n}");
}
n = 1
n = 2
n = 4
n = 8
n = 16
stopped at 32

loop repeats its body forever. break leaves the loop immediately and continues after it. Use loop when the exit condition is in the middle of the body or when there are several exits, or for programs that genuinely run forever (servers, event loops).

break with a value

Like if, loop is an expression. break value; ends the loop and makes value the result:

fn main() {
    let mut x: u64 = 1;
    let mut tries = 0;
    let first_big = loop {
        x = x * 7 % 1000;
        tries += 1;
        if x > 900 {
            break x;
        }
    };
    println!("{first_big} after {tries} tries");
}
943 after 15 tries

This is the idiomatic way to "search until found and return the result". (Only loop can break with a value; a while loop might end because its condition became false, and then there would be no value to give.)

while: repeat while a condition holds

fn main() {
    let mut n = 1234;
    let mut digits = 0;
    let mut sum = 0;
    while n > 0 {
        sum += n % 10;
        n /= 10;
        digits += 1;
    }
    println!("{digits} digits, digit sum {sum}");
}
4 digits, digit sum 10

while cond { body } checks the condition before each pass. If it is false at the start, the body never runs. Something in the body must eventually make it false, or the loop never ends.

Underneath, both loops are just jumps:

while n > 0 { body }                   loop { body }

TOP:                                   TOP:
    if !(n > 0) jump END                   body
    body                                   jump TOP
    jump TOP                           (break = jump END)
END:                                   END:

continue: skip to the next pass

continue abandons the rest of the current pass and starts the next one (re-checking the condition, in a while):

fn main() {
    let mut n = 0;
    while n < 10 {
        n += 1;
        if n % 3 != 0 {
            continue;
        }
        println!("{n} is a multiple of 3");
    }
}
3 is a multiple of 3
6 is a multiple of 3
9 is a multiple of 3

continue and break are best used to handle special cases at the top of a loop body, keeping the main work unindented: "skip comments", "stop at the end marker". Notice that n += 1 comes before the continue. Had it been at the bottom, continue would skip it and the loop would spin forever on the same n.

Nested loops and labels

break and continue apply to the innermost loop. To control an outer loop, give it a label, an identifier starting with an apostrophe:

fn main() {
    let target = 42;
    'search: for a in 1..10 {
        for b in 1..10 {
            if a * b == target {
                println!("found {a} * {b}");
                break 'search;
            }
        }
    }
}
found 6 * 7

(for a in 1..10 counts a from 1 to 9; the next lesson explains it.) Without the label, break would only end the inner loop and the search would continue with the next a.

Thinking about loops

Two questions make loops correct:

  1. Does it end? Identify what changes each pass and why it must eventually stop the loop. In the digit-sum loop, n shrinks by a factor of ten each time and must reach 0.
  2. What is true at the top of every pass? This is called the loop invariant. In the digit-sum loop: sum is the sum of the digits already removed from n. When the loop ends with n == 0, the invariant tells you sum is the sum of all digits. Thinking this way catches off-by-one errors before you run anything.

Common mistakes

Infinite loops. Forgetting to update the variable in the condition, or skipping the update with continue. In the exercises, a run that does not finish within 10 seconds is reported as timed out. In a terminal, Ctrl-C stops a runaway program.

Forgetting mut on the loop variable: E0384, cannot assign twice to an immutable variable.

Wanting a value from while:

fn main() {
    let mut n = 0;
    let x = while n < 5 {
        n += 1;
        if n == 3 { break n; }
    };
}
error[E0571]: `break` with value from a `while` loop
 --> main.rs:5:21
  |
3 |     let x = while n < 5 {
  |             ----------- you can't `break` with a value in a `while` loop
4 |         n += 1;
5 |         if n == 3 { break n; }
  |                     ^^^^^^^ can only break with a value inside `loop` or breakable block
  |
help: use `break` on its own without a value inside this `while` loop
  |
5 |         if n == 3 { break; }
  |                     ~~~~~

E0571. Use loop with an explicit condition check when you need a result.

Off-by-one boundaries. while n > 0 versus while n >= 0: with an unsigned n, the second one never becomes false (and decrementing past 0 panics). Trace the first and last pass by hand.

Recap

Going further: compute the greatest common divisor of two numbers with Euclid's algorithm (while b != 0 { let t = b; b = a % b; a = t; }) and convince yourself why it terminates.

Exercises

Exercise 1 · write code

Countdown

Read a non-negative integer n from stdin and use a while loop to print the numbers from n down to 1, one per line, followed by liftoff. For input 3:

3
2
1
liftoff

For input 0, print only liftoff.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: u32 = line.trim().parse().unwrap();
}

Example

input

3

expected output

3
2
1
liftoff

Example

input

0

expected output

liftoff
Hint 1

The variable must change inside the loop, so it needs mut.

Hint 2

while n > 0 { println!("{n}"); n -= 1; }

Reference solution
use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let mut n: u32 = line.trim().parse().unwrap();
    while n > 0 {
        println!("{n}");
        n -= 1;
    }
    println!("liftoff");
}

Exercise 2 · write code

Collatz steps

The Collatz process: if n is even, halve it; if odd, replace it with 3n + 1. Repeat until n is 1.

Read a positive integer from stdin and print how many steps it takes to reach 1, using loop and break with a value: let steps = loop { ... };. Output format:

steps: 8

(for input 6: 6, 3, 10, 5, 16, 8, 4, 2, 1 is 8 steps). Input 1 takes 0 steps. Use u64.

File: main.rs

Starting point

use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let mut n: u64 = line.trim().parse().unwrap();
    let mut count = 0;
}

Example

input

6

expected output

steps: 8

Example

input

1

expected output

steps: 0
Hint 1

Check for 1 at the top of the loop, so input 1 gives 0 steps.

Hint 2

break count; ends the loop and makes count the value of the whole loop expression.

Reference solution
use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let mut n: u64 = line.trim().parse().unwrap();
    let mut count = 0;
    let steps = loop {
        if n == 1 {
            break count;
        }
        n = if n % 2 == 0 { n / 2 } else { 3 * n + 1 };
        count += 1;
    };
    println!("steps: {steps}");
}

Exercise 3 · write code

Skip and stop

Read lines from stdin. Each line is a number, a comment starting with #, an empty line, or the word end. Add up the numbers, skipping comments and empty lines (use continue), and stop reading at end (use break), ignoring anything after it. Print the total:

total: 30

File: main.rs

Starting point

use std::io;

fn main() {
    let mut total: i64 = 0;
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let line = line.trim();
        let n: i64 = line.parse().unwrap();
        total += n;
    }
    println!("total: {total}");
}

Example

input

10
# a comment

20
end
99

expected output

total: 30

Example

input

# only
5

expected output

total: 5
Hint 1

line.starts_with('#') and line.is_empty() test the line; line == "end" compares it.

Hint 2

Put the checks before the parse, which would panic on a comment.

Reference solution
use std::io;

fn main() {
    let mut total: i64 = 0;
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        if line == "end" {
            break;
        }
        let n: i64 = line.parse().unwrap();
        total += n;
    }
    println!("total: {total}");
}

Exercise 4 · quiz

Which loop does break leave?

How many lines does this print?

fn main() {
    'outer: for i in 0..3 {
        for j in 0..3 {
            if j == 1 { continue 'outer; }
            if i == 2 { break 'outer; }
            println!("{i} {j}");
        }
    }
}
Answer
  • 2

For i = 0: j = 0 prints "0 0", then j = 1 jumps to the next i. For i = 1: prints "1 0", then the same. For i = 2: j = 0 hits break 'outer, ending both loops. Two lines. A plain break or continue affects the innermost loop; a label like 'outer lets you name which loop you mean.