Control Flow · about 35 min

if, else and Conditions as Expressions

Why this matters

Up to now every program ran the same statements in the same order regardless of input. Real programs make decisions: reject bad input, choose a message, take a shortcut. if is the basic decision-maker. In Rust it is also an expression that produces a value, which lets you write decisions compactly and makes the compiler check that every path produces something sensible.

The basic form

fn main() {
    let temperature = 31;
    if temperature > 30 {
        println!("hot");
    } else if temperature < 10 {
        println!("cold");
    } else {
        println!("pleasant");
    }
    println!("done");
}
hot
done

The condition must be a bool. Rust has no "truthy" values:

fn main() {
    let count = 3;
    if count {
        println!("non-zero");
    }
}
error[E0308]: mismatched types
 --> main.rs:3:8
  |
3 |     if count {
  |        ^^^^^ expected `bool`, found integer

Write the comparison you mean: if count != 0.

What the CPU does

An if compiles to a comparison and a conditional jump:

        compare temperature with 30
        jump-if-not-greater  to ELSE_IF
        ... code for "hot" ...
        jump to END
ELSE_IF:
        compare temperature with 10
        jump-if-not-less     to ELSE
        ... code for "cold" ...
        jump to END
ELSE:
        ... code for "pleasant" ...
END:
        ... println!("done") ...

The CPU executes instructions in sequence until a jump tells it to continue elsewhere. Every decision, loop and function call you write ends up as jumps like these.

if is an expression

Because if is an expression, it has a value: the value of whichever block ran.

fn main() {
    let n = 7;
    let parity = if n % 2 == 0 { "even" } else { "odd" };
    println!("{n} is {parity}");

    let fee = if n >= 5 { 0 } else { 2 } + 1;
    println!("fee {fee}");
}
7 is odd
fee 1

This replaces the ternary operator cond ? a : b that C and Java have; Rust does not need one. Note the ; after the closing brace when an if is part of a let: the let statement ends there.

It also makes functions read cleanly. The body of this function is a single expression:

fn max(a: i32, b: i32) -> i32 {
    if a > b { a } else { b }
}

fn main() {
    println!("{}", max(3, 9));
}
9

Both branches must agree

An if used as a value must produce the same type from every branch, because the variable it initializes has a single type:

fn main() {
    let n = 3;
    let label = if n % 2 == 0 { "even" } else { 1 };
    println!("{label}");
}
error[E0308]: `if` and `else` have incompatible types
 --> main.rs:3:49
  |
3 |     let label = if n % 2 == 0 { "even" } else { 1 };
  |                                 ------          ^ expected `&str`, found integer
  |                                 |
  |                                 expected because of this

And it must have an else: without one, the if produces nothing when the condition is false.

fn main() {
    let n = 3;
    let label = if n > 0 { "positive" };
    println!("{label}");
}
error[E0317]: `if` may be missing an `else` clause
 --> main.rs:3:17
  |
3 |     let label = if n > 0 { "positive" };
  |                 ^^^^^^^^^^^----------^^
  |                 |          |
  |                 |          found here
  |                 expected `&str`, found `()`
  |
  = note: `if` expressions without `else` evaluate to `()`
  = help: consider adding an `else` block that evaluates to the expected type

E0317: an if without else has type () when the condition is false, which does not match &str. The compiler checks every path. This is a pattern you will see throughout Rust: things that would be a run-time "undefined" or "null" in other languages are caught at compile time because the compiler insists every case produces a proper value.

An if used as a statement (no value wanted) can omit else; then both branches have type () anyway.

Writing good conditions

Combine tests with &&, || and !, and remember short-circuiting from the previous module:

fn main() {
    let divisor = 0;
    let total = 10;
    if divisor != 0 && total / divisor > 2 {
        println!("big ratio");
    } else {
        println!("no ratio");
    }
}
no ratio

Because divisor != 0 is false, the division is never evaluated, so there is no divide-by-zero panic.

Tips:

Common mistakes

Using = instead of ==. if x = 5 is an error in Rust (E0308: expected bool, found ()), whereas in C it silently assigns. Good news: Rust catches it.

A semicolon inside a value branch. let x = if c { 1; } else { 2 }; makes the first branch (), giving E0308 incompatible types. Value-producing blocks end without a semicolon.

Forgetting the ; after a let ... = if ... { } else { };. The compiler says "expected ;" at the end of the if.

Overlapping or misordered ranges in else if chains: the compiler cannot catch these; your tests must.

Recap

Going further: rewrite a chain of three if/else if statements that each call println! into one let message = if ... expression followed by a single println!. Which version is easier to change later?

Exercises

Exercise 1 · write code

Positive, negative or zero

Read one integer from stdin and print positive, negative or zero.

Examples: input 5 prints positive; input -3 prints negative; input 0 prints zero.

File: main.rs

Starting point

use std::io;

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

Example

input

5

expected output

positive

Example

input

-3

expected output

negative
Hint 1

Use if n > 0 { ... } else if n < 0 { ... } else { ... }.

Reference solution
use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let n: i64 = line.trim().parse().unwrap();
    if n > 0 {
        println!("positive");
    } else if n < 0 {
        println!("negative");
    } else {
        println!("zero");
    }
}

Exercise 2 · write code

if as a value

Write fn abs_diff(a: i32, b: i32) -> i32 that returns the distance between a and b (always zero or positive), with the body being a single if/else expression and no return keyword. Keep the given main, which prints:

7
7
0

File: main.rs

Starting point

fn abs_diff(a: i32, b: i32) -> i32 {
    a - b
}

fn main() {
    println!("{}", abs_diff(10, 3));
    println!("{}", abs_diff(3, 10));
    println!("{}", abs_diff(4, 4));
}

Example

expected output

7
7
0
Hint 1

if a > b { a - b } else { b - a } is an expression whose value is one of the two branches.

Reference solution
fn abs_diff(a: i32, b: i32) -> i32 {
    if a > b { a - b } else { b - a }
}

fn main() {
    println!("{}", abs_diff(10, 3));
    println!("{}", abs_diff(3, 10));
    println!("{}", abs_diff(4, 4));
}

Exercise 3 · quiz

Does it compile?

Does this compile?

fn main() {
    let n = 3;
    let label = if n % 2 == 0 { "even" } else { 1 };
    println!("{label}");
}
Answer
  • No: both branches of an if expression must have the same type

Every variable has one type known at compile time. An if used as a value can produce either branch, so both must have the same type, or the compiler cannot know what label is. rustc reports E0308 "if and else have incompatible types" and points at both branches.

Exercise 4 · write code

Letter grades

Read a score from 0 to 100 from stdin and print the letter grade: 90 and above A, 80-89 B, 70-79 C, 60-69 D, below 60 F. If the score is above 100 or below 0, print invalid instead.

Compute the grade with a single if / else if chain used as an expression bound with let, then print it with one println!.

File: main.rs

Starting point

use std::io;

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

Example

input

95

expected output

A

Example

input

80

expected output

B
Hint 1

Check the invalid case first, then go from the highest grade down: each else if only runs when every earlier test was false.

Hint 2

The whole chain is let grade = if ... { "A" } else if ... { "B" } ... else { "F" }; with a semicolon after the last brace.

Reference solution
use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).unwrap();
    let score: i32 = line.trim().parse().unwrap();
    let grade = if score < 0 || score > 100 {
        "invalid"
    } else if score >= 90 {
        "A"
    } else if score >= 80 {
        "B"
    } else if score >= 70 {
        "C"
    } else if score >= 60 {
        "D"
    } else {
        "F"
    };
    println!("{grade}");
}