Skip to content

← Ground Rules: Values, Types, Control Flow step 15 of 24

Easy Primitives

loop, while, for, and break with a value

Count how many Collatz transitions a positive starting number takes to reach 1.

pub fn collatz_steps(start: u64) -> u32

While the current number is greater than one, halve it if it is even; otherwise multiply it by three and add one. Count each transition once. The starting value is not itself a step.

Start Steps
1 0
6 8
27 111

Inputs are positive values whose trajectories reach 1, whose intermediate calculations fit in u64, and whose step counts fit in u32. Zero is outside this contract: repeatedly halving zero never reaches one. No overflow or cycle-handling behavior is requested outside the valid-input domain.

For this exercise, use a loop expression and return the count with break count. A while n != 1 implementation would also express the algorithm correctly, but would not practice returning a value from a loop. Output tests check the count, not which loop syntax you used.

Return a value from a loop

break value ends its target loop and supplies the loop expression's result. This complete example doubles a value until it reaches a threshold:

fn main() {
    let mut value = 1u32;
    let reached = loop {
        if value >= 10 {
            break value;
        }
        value *= 2;
    };
    assert_eq!(reached, 16);
}

A bare break supplies (), the unit value. The starter uses a bare break in the function's final loop expression, but the function promises a u32. That is why it produces E0308, a type mismatch. Supply the count when you break, and keep the loop as the function's final expression.

If several breaks target the same loop, their values must have compatible types. A loop with no break targeting it can diverge and has type !. return exits the containing function rather than supplying a loop result.

Choose the loop that describes the work

  • loop repeats until control flow inside it exits, and supports a result supplied by break.
  • while checks a condition before each iteration. This is useful when the continuation test belongs at the top.
  • for takes items from an IntoIterator value until the iterator returns None, unless the body exits earlier.

while and for evaluate to () when they complete normally. A value-bearing break cannot target either one. A break inside a nested loop can target an enclosing loop by its label; the target determines whether a value is allowed. Labeled blocks can also produce values through labeled breaks.

Iterate over values when you need values

When processing a collection, direct iteration avoids maintaining a separate index. Use enumerate() when you also need the position:

fn main() {
    let values = [2i64, 4, 6];
    let mut total = 0;
    for value in &values {
        total += value;
    }
    assert_eq!(total, 12);

    let mut positions = Vec::new();
    for (index, value) in values.iter().enumerate() {
        positions.push((index, *value));
    }
    assert_eq!(positions, [(0, 2), (1, 4), (2, 6)]);
}

Clippy's needless_range_loop lint can suggest this change when a range variable is used only to index a collection. It does not diagnose every hand-written index loop. Direct iteration makes intent clearer, but it does not prove the surrounding algorithm is correct. Nor does source-level indexing imply a runtime bounds check on every access: optimization may remove checks.

Apply the pattern to Collatz

Check for n == 1 before updating or incrementing the count. This makes the answer for start 1 zero. For other valid states, compute one transition, increase the count, then repeat.

Use n.is_multiple_of(2) for the even test. On the supported Rust toolchain, Clippy's manual_is_multiple_of lint flags the equivalent n % 2 == 0 pattern. The method expresses divisibility directly; it is not a promise of faster machine code.