2.5 Flow control

Our program starts off by running the first line in the code block attached to the main function. It then carries on to the next line in that same function—ultimately executing anything that needs executing, as well as ultimately evaluating anything that needs evaluation. When the program reaches the end of the code block attached to the main function, the program is over—and exits.

Let’s have a look at a simple program printing two messages:

fn main()
{ // Beginning of main function’s code block
    println!("Hello, world!"); // The first line of code being executed
    println!("Hey!"); // The second instruction to be executed
} // Here the program ends

Executing line by line—only being abe to create on outcome of the program every time you run it—is slightly boring. We need a way of controlling the path our code will take, depending on certain conditions.

If-then

Using if, we can control the flow of the code depending on conditions. Do remember that any code shown in these sections absent of a main functions requires one to be run.

Let’s control the flow:

let x = 32;

if x == 32
{
    println!("x is 32!");
}

This code first defines a variable binding named x, which has the value of thirty-two. It then checks to see whether or not x is exactly equal to the number thirty-two using an if. Since x is equal to thirty-two, it goes on to the then part of the expression—which happen to be a code block. If—an only if—an expression inside an if is evaluated to true, the whole code block attached to that if is executed and went over.

For the reason stated above, this code will print:

x is 32!

Considering whenever the program reaches the if, it will run the code block attached to it—x is in fact thirty-two.

Equality

To check for equality inside ifs, one uses the equality operator ‘==’, rather than using just a single equality sign. A single equality sign is solely used to bind values inside let statements. For that reason, the code in the previous example says something along the lines of “execute the following codeblock—if and only if—x is equal to the value of thirty-two”.

Inequality

At times it’s easier to check for the opposite—to see if the variale we’re checking with is equal to anything but a certain value. This may be done using the inequality operator ‘!=’, is not equal to:

let x = 32;

if x != 32
{
    println!("x is not 32.");
}

In this somewhat rare case, nothing will be printed because x clearly is equal to thirty-two.

If-then-else

Most of the time you’re going to want to also execute a piece of code when the condition following the if does not evaluate to true. At times like thse, we use an else, following the code block for the if:

let x = 32;

if x != 32
{
    println!("x is not 32.");
}
else
{
    println("x is indeed 32.");
}

This means that should x be happen to be equal to thirty-two, which it is, it won’t enter the code block attached to the if, but rather will enter the code block attached to the else. The reason this happens is because x is thirty-two—the first if expression won’t accept it, but the else will accept it should the if not accept it.

For that reason, this prints:

x is indeed 32.

Else-if

An if may be appended to an else, in order to create more paths for the code to flow down; in order to create more paths in which the code can wander down:

let age = 3;

if age == 1
{
    println!("You’re one year of age.");
}
else if age == 2
{
    println!("You’re two years of age.");
}
else if age == 3
{
    println!("You’re three years of age.");
}
else
{
    println!("You’re neither one, two or three years of age.");
}

This code checks to see if age is one—if not—it executes the following else which executes and if, repeating the process quite some times until it finds an if that matches its expression. If neither one of the values inside the if structure matches, the final else will be executed. In this case, this will be printed:

You’re three years of age.

It begins at the top and evaluates every single if or else if it passes by, ending up skipping the rest of the structure upon locating one that corresponds to true upon the expression inside it being evaluated.

Greater than and less than

There are four more operators other than == and != you can use in order to check for different kinds of equality—all with the possibility of being used inside ifs. Let’s consider a mechanic from a classical guess’n game:

let secret_number = 5;
let guess = 3; // Whatever the user may have guessed

if guess > secret_number // Guess is greater than secret number
{
    println!("Guess is too big.");
}
else if guess < secret_number // Guess is smaller than secret number
{
    println("Guess is too small.");
}
else // Guess is the same as secret number
{
    println("Congratulations!");
}

In this example we use greater than and less than—> and < respectively—to compare the numbers for our guess and for our secret number with one another.

Greater than or equal to and less than or equal to

There are times the need to check whether something is greater than, or even equal to something—as well as its opposite, checking whether something is less than or equal to:

let x = 5;

if x >= 5
{
    println!("x is either equal to or greater than five.");
}

if x <= 5
{
    println!("x is either equal to or less than five.");
}

Using the output from the above code, a clever person could figure out that the value of x is indeed exactly five, but I probably would not.

x is either equal to or greater than five.
x is either equal to or less than five.

That’s clever, isn’t it?

Match

When you’ve got a huge set of values that you’d like to go over, it can be nice to have a quicker way of writing multiple paths the code can take—using a more simplified manner.

The Rust match is somewhat similar to that of switch used in some other languages, although it’s a whole lot more powerful thanks to the wonders of Rust’s pattern matching system. Using patterns with match as well as let is discussed in-depth over at the Patterns section of this tome.

Here’s an example:

let age = 3;

match age
{
    0 => println!("Just born!"), 
    1 => println!("Little baby!"), 
    2 => println!("Somewhat annoying!"), 
    3 => println!("Stop being annoying! Although still kind of cute."), 
    4 => println!("Quite cute, quite funny."), 
    5 => println!("Getting there!"),
    _ => println!("I do not know how old you are."),
}

Upon using a match, Rust goes over all values specified and evaluates one that corresponds with the variable provided—age, which is set to the value of three.

Due to age being three, this will print whatever is found inside that arm of the match expression:

Stop being annoying! Although still kind of cute.

The different paths the code may take are defined by using a small arrow defined using an equal sign and a greater than sign =>. Rust will choose whatever expression lies after an arrow that matches the binding given to the match.

Default case

You may be used to other languages having a default case if it fails to locate a path to take. If in this case age had been greater than 5 or less than 0, the program would not have known what to execute. Since we have the _, it does—whatever comes after the => of this is what’ll be executing upon not locating another path to take. It’s required that we provide match with a _, otherwise it will present us with an error:

error[E0004]: non-exhaustive patterns: _ not covered
--> src\main.rs:5:8
  |
5 |     match age
  |           ^^^ pattern _ not covered

This means that all possible paths Rust’s compiler may choose to take must be covered. The _ is a pattern used to check for any value. If you put the default case somewhere in the middle of a match, we will also be presented with an error:

error[E0001]: unreachable pattern
--> src\main.rs:9:3
  |
9 |             2 => println!("Two")
  |             ^ this is an unreachable pattern

So keep your default cases at the very bottom. All paths need to be covered—and no path is allowed to be unreachable.

Code block inside match

You can put an entire code block inside a match, in order to be able to execute multiple lines of code upon a path being taken. Consider this example:

let beard_length = 5;

match beard_length
{
    3 => println!("Nothing hap’n here."), 
    4 => println!("I’m a friend of ‘3’."), 
    5 => {
        println!("I like both 3, ");
        println!("as well as 4.");
    }
    _ => {} // This arm does absolutely nothing
}

That being said, matches can be quite useful at times. They’re like enhanced versions of ifs—amazingly so since match-like structures in other languages usually are quite useless. Once you’ll get into the section on Patterns you’ll see how useful matches actually are.

Exercises

  • Print out various different messages depending on some variable bindings describing a person—gender, weight, height, length of hair and beard color. Accomplish this using either ifs or a match. You get extra points for using a matches combined with functions to achieve this.

Moreover

Flow control is required to make one’s application actually do something. Without flow control present, we could really only make one thing happen at any run of the application. We need something to control the flow—flow control you’re loved!