Control flow

Recommended reading: Control flow

The Control flow chapter of the Rust book covers all the standard use cases in a very clear way. People with experience in any other programming language should have no issues with this concept. That's why I won't repeat it again in this chapter and assume you're familiar with the basics.

In this chapter, we'll focus on some of the hidden gems that may not be immediately recognized when reading the book.

Assigning values from an if statement

fn main() {
    let age = 18;
    let is_child = if age < 18 { true } else { false };
    println!("Is child: {is_child}");
}

Notice the lack of ; behind the 'true' and 'false'. This allows us to return the value as a result of the if statement.

Extending the concept of returning from an if statement, we could write something like this. (Not arguing that there are better ways of getting to the same result!)

    fn main() {
    let age = 15;
    let is_teenager = if age < 18 {
        if age >= 10 {
            true
        } else {
            false
        }
    } else {
        false
    };
    println!("Is teenager: {is_teenager}");
}

We can combine this with a function:

fn is_child_a_teenager(age: i32) -> bool {
    age >= 10
}

fn main() {
    let age = 15;
    let is_teenager = if age < 18 {
        is_child_a_teenager(age)
    } else {
        false
    };
    println!("Is teenager: {is_teenager}");
}

To effectively end up with:

fn is_child(age: i32) -> bool {
    age < 18
}

fn is_child_a_teenager(age: i32) -> bool {
    age >= 10
}

fn main() {
    let age = 15;
    let is_teenager = is_child(age) && is_child_a_teenager(age);
    println!("Is teenager: {is_teenager}");
}

Check these examples and pay attention to the (lack of) semicolons ; where we are returning results.

Did you run cargo clippy on the first two examples? Did you see how Clippy can help you write better code?!

There is another type of control flow type in Rust that is commonly used; the match operator.

The match operator

Match operators can be used to match a single value against a variety of patterns. This is best demonstrated with an example:

fn main() {
    let age = 15;

    match age {
        0..=9 => println!("child"),
        10..=17 => println!("teenager"),
        _ => println!("adult"),
    }
}

Match patterns must be exhaustive. If not all cases are explicitly handled, the last statement must be a "catch-all" without conditions.

As with if statements, you can return a value from a match statement.

fn main() {
    let age = 15;

    let description = match age {
        0..=9 => "child",
        10..=17 => "teenager",
        _ => "adult",
    };

    println!("{description}");
}

Notice the ; at the end of the match block!

Match blocks can also be used to replace a set of if...else statements.

fn main() {
    let age = 15;

    let description = match age {
        _ if age < 10 => "child",
        _ if age >= 18 => "adult",
        _ => "teenager",
    };

    println!("{description}");
}

The _ in the above examples are actually variables that we ignore. In Rust variables that are not used can be ignored with an _-prefix. Or just an _, like in these examples. If needed, we can capture the value rather than ignore it:

fn main() {
    let age = 15;

    let description = match age {
        _ if age < 10 => "child".to_string(),
        _ if age >= 18 => "adult".to_string(),
        a => format!("teenager of {} years old", a),
    };

    println!("{description}");
}

If you are - like me - a fan of early returns, you can use match blocks to quickly stop execution. Typically this is used to stop processing in case of a error, but for the sake of demonstration, have a look at this example:

fn age_group(age: i32) -> String {
    let valid_age = match age {
        _ if age < 0 => return "not born yet".to_string(),
        _ if age > 150 => return "seriously?!!".to_string(),
        validated => validated,
    };

    match valid_age {
        _ if age < 10 => "child".to_string(),
        _ if age >= 18 => "adult".to_string(),
        a => format!("teenager of {} years old", a),
    }
}

fn main() {
    let age = 15;
    let description = age_group(age);
    println!("{description}");
}

Notice that valid_age only gets a value assigned when 0 <= age <= 150. The other conditions - with the explicit return - exit the function early with an 'error' condition.

You can do some super powerful stuff with matching on patterns, like matching on parts of an array or struct, but we'll cover that a bit later.

Reference material