Photo by Teigan Rodger on Unsplash

Conditionals and recursion

The main topic of this post is the if statement. You can use it to execute different code depending on the state of the program. But first I want to introduce one new operator: modulus.

Modulus

Suppose the run time of a movie is 105 minutes. You might want to know how long that is in hours. Division returns a floating-point number. You need to use floating point to get decimals:

fn main() {
    let minutes: f64 = 105.0;
    let hours: f64 = minutes / 60.0;
    println!("The movie lasts {} hours", hours);
}

This prints out The movie lasts 1.75 hours.

But we don’t write hours with decimal points. We could use rounding or truncating. Rounding returns the integer number of hours, with proper rounding. Truncating chops off the decimals.

Rounding:

let mut hours: usize = (minutes/60.0).round() as usize;

This prints out The movie lasts 2 hours. Not good enough.

Truncation:

let hours: usize = (minutes/60.0).trunc() as usize;

This prints out The movie lasts 1 hours. That won’t do either.

To get the rest, you could subtract off one hour in minutes:

fn main() {
    let minutes: f64 = 105.0;
    let hours: usize = (minutes / 60.0).trunc() as usize;
    let rest: usize = minutes as usize - hours * 60;
    println!("The movie lasts {} hours and {} minutes", hours, rest);
}

Now we get what we wanted, as this prints out The movie lasts 1 hours and 45 minutes. But it’s rather clunky and not that easy to read.

An alternative is to use the modulus operator ,% , which divides two numbers and returns the rest.

fn main() {
    let minutes: f64 = 105.0;
    let hours: usize = (minutes / 60.0).trunc() as usize;
    let rest: usize = minutes as usize % 60;
    println!("The movie lasts {} hours and {} minutes", hours, rest);
}

This gives the right answer, The movie lasts 1 hours and 45 minutes and is a lot more elegant.

The modulus operator is more useful than it seems. For example, you can check whether one number is divisible by another. If x % y is zero, then x is divisible by y.

Also, you can extract the right-most digit or digits from a number. For example, x % 10 yields the right-most digit of x (in base 10). Dividing by 100: x % 100 yields the last two digits.

Boolean expressions

A boolean expression is an expression that is either true or false. The following examples use the operator ==. It compares two operands and produces true if they are equal and false otherwise.

true and false are special values that belong to the type boolean; they are not strings.

The == operator is one of the relational operators; the others are:

x != y     // x is not equal to y
x > y      // x is greater than y
x < y      // x is smaller than y
x >= y     // x is greater than or equal to y
x <= y     // x is smaller than or equal to y

These operations should be familiar to you from mathematics. The symbols Rust uses are different from the mathematical symbols. A common error is to use a single equal sign (=) instead of a double equal sign ( == ). Remember that = is an assignment operator and == is a relational operator.

Logical operators

There are three logical operators : && for ”and”, || meaning ”or”, and ! meaning ”not”. The semantics (meaning) of these operators is much like their meaning in English. For example, x > 0 && x < 10 is true if, and only if, x is greater than 0 and also less than 10.

n % 2 == 0 || n % 3 == 0 is true if either or both of the conditions is true, that is, if the number is divisible by either 2 or 3 or both.

Finally, the ! operator negates a boolean expression, so !( x > y ) is true if x > y is false, that is, if x is less than or equal to y.

Conditional execution

To write useful programs, we almost always need to check conditions. Then we can change the behaviour of the program based on those conditions. Conditional statements give us this ability. The simplest form is the if statement:

fn main() {
    let x: isize = 5;
    if x > 0 {
        println!("x is positive!");
    }
}

We call the boolean expression after if the condition. If it is true, the block of code inside the {} runs, if not, nothing happens.

if statements have the same structure as functions: a header followed by a body. We call statements like this compound statements.

There is no limit on the number of statements that can appear in the body, but there has to be at least one. It is useful to have a body with no statements sometimes (usually as a place keeper for code you haven’t written yet). In that case, you can use a todo macro instead, which does nothing:

fn main() {
    let x: isize = 5;
    if x > 0 {
        todo!();
    }
}

The todo! will always panic. If you forget to replace it with real code, you will get a reminder as soon as you run this with an x that is greater than 0:

thread 'main' panicked at 'not yet implemented', src/main.rs:4:9

Alternative execution

A second form of the if statement is “alternative execution”. If there are two possibilities the condition determines which one runs. The syntax looks like this:

fn main() {
    let x: isize = 5;
    if x % 2 == 0 {
        println!("x is even");
    } else {
        println!("x is odd");
    }
}

If the rest when dividing x by 2 is 0, then we know that x is even, and the program displays an appropriate message. If the condition is false, the second set of statements runs. Since the condition must be true or false, exactly one of the alternatives will run. We call the alternatives branches. They are branches in the flow of execution.

Chained conditionals

Sometimes there are more than two possibilities and we need more than two branches. One way to express a computation like that is a chained conditional:

extern crate rand;
use rand::prelude::*;

fn main() {
    let x: isize = 5;
    let mut rng = thread_rng();
    let y: isize = rng.gen_range(-10, 10);
    if x < y {
        println!("x is less than y");
    } else if x > y {
        println!("x is greater than y");
    } else {
        println!("x and y are equal");
    }
}

Again, exactly one branch will run. There is no limit on the number of else if statements. If there is an else clause, it has to be at the end, but there doesn’t have to be one.

Rust checks each condition in order. If the first is false, it checks the next, and so on. If one of them is true, the corresponding branch runs and the statement ends. Even if more than one condition is true, only the first true branch runs.

Nested conditionals

We can also nest one conditional within another. We could have written the example in the previous section like this:

extern crate rand;
use rand::prelude::*;

fn main() {
    let x: isize = 5;
    let mut rng = thread_rng();
    let y: isize = rng.gen_range(-10, 10);
    if x == y {
        println!("x and y are equal");
    } else {
        if x < y {
            println!("x is less than y");
        } else {
            println!("x is greater than y");
        }
    }
}

The outer conditional contains two branches. The first branch contains a simple statement. The second branch contains another if statement, which has two branches of its own. The branches are both simple statements in our example. They could have been conditional statements as well.

The indentation of the statements makes the structure clear. But nested conditionals are more difficult to read. It is a good idea to avoid them when you can, and with a little effort and afterthought, you almost always can.

Logical operators provide a way to simplify nested conditional statements. For example, we can rewrite the following code using a single conditional:

extern crate rand;
use rand::prelude::*;

fn main() {
    let mut rng = thread_rng();
    let x: isize = rng.gen_range(-20, 20);
    if 0 < x {
        if x < 10 {
            println!("x is a positive single-digit number");
        }
    }
}

The println! statement runs only if we make it past both conditionals. That means we can get the same effect with the && operator:

extern crate rand;
use rand::prelude::*;

fn main() {
    let mut rng = thread_rng();
    let x: isize = rng.gen_range(-20, 20);
    if 0 < x && x < 10 {
        println!("x is a positive single-digit number");
    }
}

Recursion

It is legal for one function to call another. It is also legal for a function to call itself. It may not be obvious why that is a good thing, but it turns out to be one of the most magical things a program can do. For example, look at the following function:

fn main() {
    countdown(3);
}

fn countdown(n: usize) {
    if n <= 0 {
        println!("Blastoff!");
    } else {
        println!("{}", n);
        countdown(n - 1);
    }
}

If n is 0 or negative, it outputs the word, “Blastoff!” Otherwise, it outputs n and then calls a itself — passing n - 1 as an argument. Here is what happens if we call this function like this:

countdown(3);

	The execution of `countdown` begins with `n = 3`, and since `n` is greater than 0, it outputs the value 3, and then calls itself:

		The execution of `countdown` begins with `n = 2`, and since `n` is greater than 0, it outputs the value 2, and then calls itself...

			The execution of `countdown` begins with `n = 1`, and since `n` is greater than 0, it outputs the value 1, and then calls itself...

				The execution of `countdown` begins with `n = 0`, and since `n` is not greater than 0, it outputs the word,Blastoff!” and then returns.

			The `countdown` that got `n=1` returns.
		The `countdown` that got `n=2` returns.
	The `countdown` that got `n=3` returns.

And then you’re back in main. So, the total output looks like this:

3
2
1
Blastoff!

A function that calls itself is recursive. We call the process of executing it recursion. As another example, we can write a function that prints a string n times:

fn main() {
    print_n_times("Spam!", 3);
}

fn print_n_times(the_string: &str, n: usize) {
    if n <= 0 {
        return;
    }
    println!("{}", the_string);
    print_n_times(the_string, n - 1);
}

If n <= 0 the return statement exits the function. The flow of execution immediately returns to the caller. The remaining lines of the function don’t run.

The rest of the function is like countdown. It displays the_string and then calls itself to display the_string another n - 1 times. So the number of lines of output is 1 + n - 1, which adds up to n.

For simple examples like this, it is easier to use a for loop. But we will see examples later that are hard to write with a for loop and easy to write with recursion. So it is good to start early.

The creators of Rust encourages iteration over recursion. The main reasons for this are that

  • recursion reduces portability, not all compilers handle recursion in the same way
  • recursions can cause performance issues, especially a feature called “tail calls”
  • recursions can make debugging more difficult. This is especially true if we use tail call optimization. Tail call optimization overwrites stack values

There are crates, like tramp, designed to make recursion more powerful in Rust. But it might be a better idea to be careful, and favour looping in your own code, at least for the time being.

If you find the last couple of paragraphs confusing, don’t worry about it. Most people would. No big deal, stick to looping.

Infinite recursion

If a recursion never reaches a base case, it goes on making recursive calls forever. This means that the program never terminates. We call this infinite recursion , and it is generally not a good idea. Although it is often used for interactive programs. These programs need to wait an indeterminate time for an unknown number of user inputs.

Here is a minimal program with an infinite recursion:

fn main(){
    recurse();
}

fn recurse() {
    recurse();
}

Compiling it finishes with a warning, that the function cannot return without recursing. And we get a helpful suggestion to use a loop instead:

warning: function cannot return without recursing
  --> src/main.rs:16:1
   |
16 | fn recurse() {
   | ^^^^^^^^^^^^ cannot return without recursing
17 |     recurse();
   |     --------- recursive call site
   |
   = note: `#[warn(unconditional_recursion)]` on by default
   = help: a `loop` may express intention better if this is on purpose

    Finished dev [unoptimized + debuginfo] target(s) in 0.83s

It is no surprise that things end in a bad way when we launch the application that uses our function:

thread 'main' has overflowed its stack
fatal runtime error: stack overflow
[1]    49071 abort      cargo run

If you encounter an infinite recursion by accident, review your function. Confirm that there is a base case that does not make a recursive call. And if there is a base case, check to make sure you are guaranteed to reach it.

Keyboard input

The programs we have written so far accept no input from the user. They do the same thing every time.

Rust provides a built-in function called io::stdin(). It will stop the program and wait for the user to type something. When the user presses Return or Enter, the program resumes. The function then returns what the user typed as a string.

Before getting input from the user, it is a good idea to print a prompt telling the user what to type. Use println! for that, it is the easiest way.

Debugging

When a syntax or runtime error occurs, the error message contains a lot of information. It can be overwhelming. The most useful parts are usually:

  • What kind of error it was
  • Where the error occurred.

Syntax errors are usually easy to find, but there are a few gotchas. Whitespace errors can be tricky because spaces and tabs are invisible and we tend to ignore them.

Rusts error messages are known, indeed renowned, for being fantastically helpful. Nine times out of ten they will actually suggest the very thing that fixes the problem!

Runtime errors are less helpful, but pretty good all things considered. Suppose you are trying to print a list of all the aliens you’ve encountered the last week. In Rust, you might write something like this:

fn main() {
    let aliens = ["Klaatu", "Q", "Spock", "Worf"];
    for i in 0..5 {
        println!("{}", aliens[i]);
    }
}

When you run this program, you get this output:

Klaatu
Q
Spock
Worf
thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 4', src/main.rs:4:24
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The error message indicates line 4, where you are trying to access elements in your array. But there’s nothing wrong on that line. The real problem is in the range you use on line 3. The range end (non-inclusive) should be 4, not 5. That way the loop will end when i is 3. Now it tries to use the number 4 to index into the array, and that wont work. Arrays are zero indexed, so the index starts on 0 and stops at 3 for this array.

Take the time to read error messages carefully, but don’t assume that everything they say is correct.

About the author

For the last three decades, I've worked with a variety of technologies. I'm currently focused on fullstack development. On my day to day job, I'm a senior teacher and course developer at a higher vocational school in Malmoe, Sweden. I'm also an occasional tech speaker and a mentor. Do you want to know more? Visit my website!