RUST: Flow Control in Rust

Rust provides a rich set of flow control constructs — if expressions, while loops, the infinite loop keyword, for loops with ranges and iterators, and the powerful match statement. Each reflects Rust's philosophy of expressiveness without compromising safety or performance.

Flow control determines the path your program takes through its instructions. Rust's flow control constructs are largely familiar to developers from other languages, but Rust adds several key differences: if is an expression that returns a value, loop is a dedicated keyword for infinite loops, and match is a full pattern-matching system that the compiler enforces exhaustively.


The `if` Statement

Rust's if works like most languages, but without parentheses around the condition:

rust
fn if_statement() {
    let temp = 35;

    if temp > 30 {
        println!("really hot outside!");
    } else if temp < 10 {
        println!("really cold!");
    } else {
        println!("temperature is OK");
    }
}

Braces {} are mandatory in Rust, even for single-statement bodies. This is a deliberate design choice to eliminate a class of bugs caused by misleading indentation (the "dangling else" problem in C).


`if` as an Expression

In Rust, if is an expression, not just a statement. This means it evaluates to a value and can be used on the right-hand side of a variable assignment:

rust
let temp = 35;

// if as an expression
let day = if temp > 20 { "sunny" } else { "cloudy" };
println!("today is {}", day); // today is sunny

This eliminates the need for the ternary operator (condition ? a : b) that exists in C, C++, and Java. In Rust, if itself serves that role.

For multi-branch expressions:

rust
let description = if temp > 20 { "hot" } else if temp < 10 { "cold" } else { "OK" };
println!("it is {}", description);

And nested if expressions:

rust
let label = if temp > 20 {
    if temp > 30 { "very hot" } else { "hot" }
} else if temp < 10 {
    "cold"
} else {
    "OK"
};
println!("it is {}", label);

Important: When if is used as an expression, every branch must return the same type. The compiler enforces this.


The `while` Loop

The while loop executes a block repeatedly as long as a condition is true:

rust
fn while_loop_example() {
    let mut x = 1;

    while x < 1000 {
        x *= 2;

        if x == 64 { continue; } // skip the rest of this iteration

        println!("x is {}", x);
    }
}

  • continue skips to the next iteration
  • break exits the loop immediately

text
Execution Flow:
x=1 -> x=2 -> x=4 -> x=8 -> x=16 -> x=32 -> x=64 (skip) -> x=128 -> ... -> x=1024 (exits)


The `loop` Keyword

Rust provides a dedicated loop keyword for infinite loops. This is semantically cleaner than while true {} and enables the compiler to reason better about the code:

rust
fn loop_example() {
    let mut y = 1;

    loop {
        y *= 2;
        println!("y = {}", y);

        if y == 1 << 10 { break; } // break when y reaches 1024
    }
}

`loop` as an Expression

Like if, loop can also return a value using break with a value:

rust
let mut counter = 0;

let result = loop {
    counter += 1;
    if counter == 10 {
        break counter * 2; // returns 20 from the loop
    }
};

println!("result = {}", result); // result = 20

This is unique to Rust and enables patterns where you need to keep trying an operation until it succeeds, and then return the result.


The `for` Loop

Rust's for loop iterates over ranges and iterators. It is the preferred loop in Rust because it is safer (no off-by-one errors) and more expressive:

rust
fn for_loop_example() {
    // Range: 1..11 means 1 inclusive to 11 exclusive (1, 2, ..., 10)
    for x in 1..11 {
        if x == 3 { continue; } // skip 3
        if x == 8 { break; }    // stop at 8
        println!("x = {}", x);
    }
}

Output: 1, 2, 4, 5, 6, 7 (skips 3, stops before 8)

Range Types

SyntaxMeaning
1..101 to 9 (exclusive end)
1..=101 to 10 (inclusive end)
..10from start to 9
1..from 1 to end

Iterating with Enumeration

Use .enumerate() to get both the index and the value:

rust
fn enumerate_example() {
    for (pos, y) in (30..=40).enumerate() {
        println!("{}: {}", pos, y);
    }
    // Output:
    // 0: 30
    // 1: 31
    // ...
    // 10: 40
}

Iterating Over Collections

rust
let fruits = vec!["apple", "banana", "cherry"];

for fruit in &fruits {
    println!("{}", fruit);
}

// With index:
for (i, fruit) in fruits.iter().enumerate() {
    println!("{}: {}", i, fruit);
}


The `match` Statement

match is Rust's most powerful control flow construct. It is like a switch statement, but exhaustive, expressive, and safe:

rust
fn match_example() {
    let country_code = 44;

    let country = match country_code {
        44 => "UK",
        46 => "Sweden",
        7  => "Russia",
        1..=999 => "unknown",  // inclusive range pattern
        _  => "invalid"        // default case (catches everything else)
    };

    println!("country code {} is {}", country_code, country);
    // country code 44 is UK
}

The _ wildcard pattern matches any value not matched by the previous arms. The Rust compiler enforces that your match is exhaustive — you must cover all possible values (or use _).

Match with Multiple Patterns

rust
let x = 5;
let description = match x {
    1 | 2 => "one or two",   // OR pattern
    3..=5 => "three to five", // range pattern
    _ => "other"
};
println!("{}", description); // three to five

Match as an Expression

Like if, match is also an expression that returns a value:

rust
let score = 85;
let grade = match score {
    90..=100 => 'A',
    80..=89  => 'B',
    70..=79  => 'C',
    60..=69  => 'D',
    _        => 'F',
};
println!("Grade: {}", grade); // Grade: B

Match with Guards

You can add conditions to match arms using if guards:

rust
let x = 7;
let description = match x {
    n if n % 2 == 0 => "even",
    n if n % 2 != 0 => "odd",
    _ => "unknown" // needed to satisfy exhaustiveness
};
println!("{} is {}", x, description); // 7 is odd

Match on Enums and Tuples

match is especially powerful when used with enums and tuples (covered in the data structures post):

rust
let point = (0, 5);
match point {
    (0, 0) => println!("origin"),
    (0, y) => println!("on y-axis at {}", y),
    (x, 0) => println!("on x-axis at {}", x),
    (x, y) => println!("at ({}, {})", x, y),
}
// Output: on y-axis at 5


Loop Labels

When you have nested loops, break and continue apply to the innermost loop by default. Rust supports loop labels to specify which loop to break or continue:

rust
'outer: for x in 0..5 {
    for y in 0..5 {
        if x == 2 && y == 2 {
            break 'outer; // exits both loops
        }
        println!("({}, {})", x, y);
    }
}

Labels are prefixed with a single quote ('). This is useful for breaking out of deeply nested loops without needing flag variables.


Choosing the Right Loop

text
Loop Selection Guide:
+---------------------+------------------------------------------+
| Construct           | When to Use                              |
+---------------------+------------------------------------------+
| for x in range      | Known number of iterations or collection |
| while condition     | Unknown iterations, condition-driven     |
| loop                | Infinite loop or retry-until-success     |
| match               | Pattern-based branching on a value       |
| if / else if / else | Simple conditional branching             |
+---------------------+------------------------------------------+

In idiomatic Rust, for with iterators is preferred over while with manual index management. Iterators are safer (no off-by-one errors) and more expressive.


Complete Example

rust
fn main() {
    // if as expression
    let temp = 35;
    let day = if temp > 20 { "sunny" } else { "cloudy" };
    println!("today is {}", day);

    // while with continue
    let mut x = 1;
    while x < 1000 {
        x *= 2;
        if x == 64 { continue; }
        println!("x = {}", x);
    }

    // loop with break-value
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 5 { break counter * 10; }
    };
    println!("loop result = {}", result); // 50

    // for with range and enumerate
    for (pos, y) in (30..=32).enumerate() {
        println!("{}: {}", pos, y);
    }

    // match with ranges and wildcard
    let code = 44;
    let country = match code {
        44 => "UK",
        46 => "Sweden",
        1..=999 => "unknown",
        _ => "invalid"
    };
    println!("code {} is {}", code, country);
}


Conclusion

Rust's flow control constructs are both familiar and uniquely powerful. The if expression eliminates the need for a ternary operator. The loop keyword makes infinite loops intentional and enables returning values from loops. The for loop with ranges and iterators prevents common off-by-one bugs. The match statement provides pattern matching that the compiler guarantees is exhaustive.

Together, these constructs give Rust code a clarity and correctness that is difficult to achieve in C or C++ without extensive discipline.