Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lecture 8 - Control flow: branching and looping

A decision diamond with two exits and a path looping back

Announcements




Learning Objectives

  • Write if / else if / else branches and early returns
  • Use while, for, loop, break, and continue
  • Use for loops with ranges (.. and ..=)
  • Use .iter() and .iter().enumerate() to loop over an array
  • Create and work with fixed-size arrays
  • Choose appropriate loop types based on use case requirements

Three rules for branching with if

You probably already do these two:

  1. Indent each block. Rust doesn't need the whitespace to run, but people need it to read. Your editor will mostly take care of it. (Unless you're doing the all-on-one-line trick)
  2. Use else if rather than an if nested inside an else
if x == 7 {
    let z = 100;
} else {
    let z = 200;
}

or

let z = if x == 7 {100} else {200};

This one takes some explaining:

  1. Keep nesting shallow. When it makes sense, return early, or move a branch into its own function

Checking inputs with nested if

#![allow(unused)]
fn main() {
/// Returns the score as a percentage, or -1.0 if the inputs don't make sense.
fn percent(points: f64, max_points: f64) -> f64 {
    if max_points > 0.0 {
        if points >= 0.0 {
            if points <= max_points {
                points / max_points * 100.0
            } else {
                println!("Invaid");
                -1.0
            }
        } else {
            println!("Invalid");
            -1.0
        }
    } else {
        println!("Invalid");
        -1.0
    }
}
}

Same checks, returning early

/// Returns the score as a percentage, or -1.0 if the inputs don't make sense.
fn percent(points: f64, max_points: f64) -> f64 {
    if max_points <= 0.0 {
        println!("Max points must be positive");
        return -1.0;
    }
    if points < 0.0 {
        println!("Points can't be negative");
        return -1.0;
    }
    if points > max_points {
        println!("More points than the max");
        return -1.0;
    }
    points / max_points * 100.0
}

fn main() {
    println!("{}", percent(45.0, 50.0));
    println!("{}", percent(55.0, 50.0));
}

Each check gets its own if, with its message right under it. Once you're past all of them, the inputs are good.

Your turn: clean this up

#![allow(unused)]
fn main() {
// This is technically valid but TERRIBLE code please DO NOT DO THIS
let x = 4;
let result = if x > 0 {
    if x < 10 {
        let temp = x * x;
        let bonus = if temp > 10 { 5 } else { 2 };
        temp + bonus
    } else {
        let factor = x / 2;
        if factor > 3 {
            factor * 3
        } else {
            factor + 1
        }
    }
} else {
    0
};
println!("Result: {}", result);
}

T/P/S - How would you rewrite this so it's easier to read?

One way: return early, like the percent example.

#![allow(unused)]
fn main() {
fn result_for(x: i32) -> i32 {
    if x <= 0 {
        return 0;
    }
    if x < 10 {
        let temp = x * x;
        let bonus = if temp > 10 { 5 } else { 2 };
        return temp + bonus;
    }
    let factor = x / 2;
    if factor > 3 { factor * 3 } else { factor + 1 }
}
}

Another way: give each branch its own function, so the top-level function is just a short else if chain.

#![allow(unused)]
fn main() {
fn small_result(x: i32) -> i32 {
    let temp = x * x;
    let bonus = if temp > 10 { 5 } else { 2 };
    temp + bonus
}

fn large_result(x: i32) -> i32 {
    let factor = x / 2;
    if factor > 3 { factor * 3 } else { factor + 1 }
}

fn result_for(x: i32) -> i32 {
    if x <= 0 {
        0
    } else if x < 10 {
        small_result(x)
    } else {
        large_result(x)
    }
}
}

Looping

In P1CP2 week you'll write programs that keep asking until they know something

In CP2, one person picks a secret number, and the other asks questions until they have it.

Every strategy you write for checkpoint 2 is a loop. Here's one that comes with the project:

/// Guess at random until the guess happens to be right.
pub fn random(keeper: &mut SecretKeeper, min: u32, max: u32) -> u32 {
    loop {
        let guess = random_range(min..max);
        if keeper.ask_if_equal(guess) {
            return guess;
        }
    }
}
  • loop runs forever, like while True: in Python
  • The only way out is to leave: here return ends the whole function
  • Don't worry about &mut yet. keeper is the one who knows the number and answers yes or no

The same loop, written another way

A loop is an expression, so it can hand back a value. break with a value leaves the loop and gives that value to whatever is waiting for it:

pub fn random(keeper: &mut SecretKeeper, min: u32, max: u32) -> u32 {
    let answer = loop {
        let guess = random_range(min..max);
        if keeper.ask_if_equal(guess) {
            break guess;
        }
    };
    answer
}
  • return leaves the whole function
  • break leaves just the loop, so it's handy when there's more to do after it

You can break out of for and while loops too, but without a value. Can you guess why?

for loops and ranges

A range is start..end, e.g. 1..5 or we can write (1..5)

The index will vary as:

Unless you use the notation (start..=end), in which case the index will vary as

Let's watch it in a for loop:

#![allow(unused)]
fn main() {
for i in (1..5) {
    println!("{}",i);
};
}

Or inclusive:

#![allow(unused)]
fn main() {
// inclusive range
for i in (1..=5) {
    println!("{}",i);
};
}

Or you can get fancy:

#![allow(unused)]
fn main() {
// every other element 
for i in (1..5).step_by(2) {
    println!("{}",i);
};

println!("And now for the reverse");
for i in (1..5).step_by(2).rev() {
    println!("{}",i)
};
}

Try printing over your loop indices early on to make sure it's doing what you want it to do!

Arrays in Rust

  • Arrays in Rust are of fixed length (we'll learn about more flexible Vec later)
  • All elements of the same type (unlike tuples)
  • You cannot add or remove elements from an array (but you can change their values)
  • Arrays are 0-indexed and elements are arr[i]

What will this return?

#![allow(unused)]
fn main() {
let mut arr = [1,7,2,5,2];
arr[1] = 13;
println!("{} {}",arr[0],arr[1]);
println!("{}",arr.len());
}

Three ways to loop over an array

#![allow(unused)]
fn main() {
let fruits = ["apple", "banana", "orange"];

// 1. Count through the positions
for i in 0..fruits.len() {
    println!("fruits[{}] = {}", i, fruits[i]);
}

// 2. Take the values one at a time (`for fruit in fruits` works too)
for fruit in fruits.iter() {
    println!("{}", fruit);
}

// 3. Get the position and the value together
for (i, fruit) in fruits.iter().enumerate() {
    println!("fruits[{}] = {}", i, fruit);
}
}
You needUse
The positionfor i in 0..fruits.len()
The valuefor fruit in fruits.iter() or for fruit in fruits
Bothfor (i, fruit) in fruits.iter().enumerate()
To change the elementsfor i in 0..fruits.len(), then fruits[i] = ...

Don't worry yet about what .iter() and .enumerate() are doing under the hood. For now, .iter() hands you the items one at a time, and .enumerate() pairs each one with its position as a tuple.

for fruit in fruits and for fruit in fruits.iter() do the same job for now. There is a difference, and we'll get to it when we learn about ownership.

You'll sometimes see &fruit written in that last loop. We'll get to what & means later.

Common array operations

#![allow(unused)]
fn main() {
// create array of given length and fill it with a specific value
// note the semicolon vs the comma!
let arr2 = [15;3];
for x in arr2 {
    print!("{} ",x);
}
println!();
}
#![allow(unused)]
fn main() {
// you can still infer or annotate types
let arr2 : [u8;3] = [15;3];
}

Arrays come with useful built-in methods:

#![allow(unused)]
fn main() {
let mut scores = [85, 92, 78, 96, 88];

// Get the length
println!("Number of scores: {}", scores.len());

// How to print an array
println!("{:?}", scores);

// There's also sort, min, clamp, truncate... you can look them up!
}

Remember: {:?} is "debug" formatting

Let's pause here for some review (skip for time)

Take a minute with a partner to review functions from last lecture:

  1. What's wrong with this function signature?

    #![allow(unused)]
    fn main() {
    fn calculate_area(width, height) -> f64 {
    }
  2. What's wrong with this function?

    #![allow(unused)]
    fn main() {
     fn mystery(x: i32) -> i32 {
         let result = x * 2;
         result + 1;
     }
    }
  3. What are two different ways you can fix this so it compiles?

    fn main() {
        let x = 4;
        let y = 4.5;
        let z = x + y;
        println!("{}",z);
    }

while loops

While loops continue as long as a condition remains true (very similar to Python)

#![allow(unused)]
fn main() {
let mut number = 3;

while number != 0 {
    println!("{number}!");
    number -= 1;
}
println!("LIFTOFF!!!");
}

Using continue to jump to the next iteration

Think/pair/share - what is this going to print?

#![allow(unused)]
fn main() {
let mut x = 1;

let result = loop {
    if x == 3 {
        x = x+1;
        continue; 
    }
    println!("X is {}", x);
    x = x + 1;
    if x==6 {
        break x*2;
    }
};

println!("Result is {}", result);
}

FYI: you can label loops

break and continue apply to the innermost loop. A label like 'outer: lets you target an outer one instead:

#![allow(unused)]
fn main() {
'outer: for x in 1..=4 {
    for y in 1..=3 {
        if x * y == 6 {
            break 'outer;   // leaves both loops
        }
    }
}
}

You won't need this in this course. If you find yourself reaching for it, there is usually a clearer way to write the loop.

Which loop would you use?

T/P/S - Pick for, while, or loop for each of these games:

  1. Deal 5 cards to each of 4 players
  2. Roll a die until you get a 6
  3. Blackjack: keep drawing cards until your hand is 17 or over
  4. Hangman: keep taking guesses if you have lives left and the word isn't solved
Some possible answers:
  1. for, twice: you know up front it's 4 players and 5 cards
  2. loop, with a break when you roll a 6. You can't know how many rolls it will take
  3. while hand < 17: one condition decides when to stop
  4. while lives > 0 && !solved: still one condition, built from two with &&

Aren't these all kind of the same?

#![allow(unused)]
fn main() {
for i in 0..3 {
    println!("{i}");
}
}

How could you write this as a while loop? And that while as a loop?

#![allow(unused)]
fn main() {
let mut i = 0;
while i < 3 {
    println!("{i}");
    i += 1;
}
}
#![allow(unused)]
fn main() {
let mut i = 0;
loop {
    if i >= 3 {
        break;
    }
    println!("{i}");
    i += 1;
}
}

All three print 0 1 2.

Pick the one that's most concise and expresses what you mean:

  • for: "go through each of these"
  • while: "keep going as long as this is true"
  • loop: "keep going until something inside tells me to stop"

When for fits, it's the safest (there's no i += 1 to forget)

Activity time

Activity 8: loops, functions, and variables review

  • Work alone or with one partner, and put both names on one sheet
  • Paper only, no laptops
  • Start with whatever part you feel least sure about
  • We'll go over answers at the start of Wednesday's class

To think about til Wednesday: which one is faster?

Two ways to find the nth prime (the 4th prime is 7):

A. Check each number. Count up from 2. For each number, try dividing it by every smaller number. Stop once you've found n primes.

B. Cross out multiples. Write down every number up to 100,000. Cross out every multiple of 2, then every multiple of 3, then of the next number that isn't crossed out, and so on. Then count through what's left.

  • How would you write each of these as loops?
  • Which would you bet is faster?
  • How could you find out for sure? How would you count the steps?
  • Does the answer depend on n?

That's where we start on Wednesday.