Lecture 8 - Control flow: branching and looping
Announcements
Learning Objectives
- Write
if/else if/elsebranches and early returns - Use
while,for,loop,break, andcontinue - Use
forloops 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:
- 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)
- Use
else ifrather than anifnested inside anelse
if x == 7 {
let z = 100;
} else {
let z = 200;
}
or
let z = if x == 7 {100} else {200};
This one takes some explaining:
- 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;
}
}
}
loopruns forever, likewhile True:in Python- The only way out is to leave: here
returnends the whole function - Don't worry about
&mutyet.keeperis 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
}
returnleaves the whole functionbreakleaves 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
Veclater) - 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 need | Use |
|---|---|
| The position | for i in 0..fruits.len() |
| The value | for fruit in fruits.iter() or for fruit in fruits |
| Both | for (i, fruit) in fruits.iter().enumerate() |
| To change the elements | for 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:
-
What's wrong with this function signature?
#![allow(unused)] fn main() { fn calculate_area(width, height) -> f64 { } -
What's wrong with this function?
#![allow(unused)] fn main() { fn mystery(x: i32) -> i32 { let result = x * 2; result + 1; } } -
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:
- Deal 5 cards to each of 4 players
- Roll a die until you get a 6
- Blackjack: keep drawing cards until your hand is 17 or over
- Hangman: keep taking guesses if you have lives left and the word isn't solved
for, twice: you know up front it's 4 players and 5 cardsloop, with abreakwhen you roll a 6. You can't know how many rolls it will takewhile hand < 17: one condition decides when to stopwhile 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.