Control flow is a set of statements that organize the sequence of execution for each statement. This is the most important thing you must learn about structured programming: How to control the logical path of your program.

Basic concepts

The control flow is a metaphor. The program is compared to a river. The river always flow downhill. Sometimes the river is split into two or tree branches and sometimes branches join back together. Very rarely we use an artificial lake and pump the water uphill. To pump more water we may use multiple pumps. This is very similar to a program logic that may work in parallel to do something hard.

Three important concepts to learn here:

  • The decision, sometimes known as conditional statement;
  • The repetition, sometimes known as loop;
  • The exception, sometimes known as error;

Apart from these some computer languages have a multi-path statement. For example “switch” or “case”.

Decision as statement

In Rust this statement is based on if … else …  keywords. We have two blocks of code: When a condition is true then first block is executed. When the condition is false the second block of code is executed. The second block is optional but the first block is mandatory.

Example:

// the decision statement
let a = 10;
let b = 10;

if a == b {
  println!("a and b are equal");
}
else {
  println!("a and b are not equal");
};

Decision as expression

In many languages the decision is only a statement. In Rust the if  is actually an expression that can have a result. The result is given by the value of the last expression in the block. This is specific to Rust and looks a little bit strange for programmer. Sometimes may be useful to think like this especially when there is no ternary operator like “?” used in other languages.

Example:

fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

    println!("The value of number is: {}", number);
}

Cascade decisions

In Python we have “elsif” keyword and in Julia we have “elseif” keyword to create a multi-deck decision. Here in Rust we do not have a special keyword. Instead we can use else if that are two keywords. This is possible in Rust due to lack of any symbol required after the “else”. In Python after “else” we must use “:” but in Rust we can use a block or a statement.

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

Repetition statement

In Rust we have several kind of repetition statements. Each type is best for something but the main idea is the same. We repeat a block of code several times. The number of times can be controlled by a variable that is called “control variable” and a condition or just a condition.

Repetition statement can be nested on one or several levels. Having nested repetitive statements can create performance issues therefore we must include in the loop as few statements as possible. The loop control variable need to be incremented inside the loop this can also take time.

The infinite loop

This is the most simple repetition statement. We use only keyword “loop” and { … } to create a repetitive block. This will be repeated forever and the program will get stack in the loop. You can stop using CTRL+C. This is actually a logical error. Good programmers must avoid infinite loops at all cost.

fn main() {
    loop {
        println!("press ctrl+c to stop me!");
    }
}

The exit condition

To avoid an infinite loop we must have an exit point. This can be created using “break” statement inside a condition statement “if”. We create a condition to stop the loop. This condition must sometimes become true or else we can’t exit the loop even if we have a break somewhere.

In the next example we create a control variable “i” that is declared outside of the loop and start with 1. Then we increment this variable with 1 every loop iteration. We use the relation operator “>” with if statement you have just learned before and break when i > 3. This will print exactly 3 times and then the loop will stop and program will normally exit.

fn main() {
    let mut i = 1;
    loop {
        println!("Variable i is now {}!",i);
        if i > 3 { 
           break;
        } else {
           i += 1;
        } //end if
    } // end loop
} // end main
Note: In this example you can see comments ate ending the blocks. This is a sage code good practice rule. We add comments to the end of block when the indentation level is greater then 2. This will end the curly brackets nightmare.

The While loop

This is a repetitive block of code that is executed as long as one condition is true. When the condition become false the program continue with next statement after the loop block end. Now the problem is to create a condition expression that will become false. If the condition never become false we again can have an infinite loop.

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number = number - 1;
    } // end while

    println!("LIFTOFF!!!");
} // end main

The For loop

This kind of loop is created using keyword “for” and it has two usages: One is to iterate a specific number of times over a range of numbers. Second is to iterate over a collection of items: array, vector, string or map/dictionary. The range in Rust is created using a range expression with .. operator. (1..10) will create one number and will assign it to “i” to create the control variable.

Example:

fn main() {
    // next loop will generate numbers: 1,2,3,4,5,6,7,8,9 
    // the lust number i will not be generated that is the Range definition
    for i in (1..10) {
        println!("{}!", i); 
    }//end for
    println!("done!");
} //end main

The iteration

Now at last we can present the most useful loop that is actually the “iterative” statement. This will scan all items into a collection. The peculiar thing is that we have to specify function “iter()” to get an “iterator” that is a function that can be used for each iteration to fatch the next element.

fn main() {
    let a = [10, 20, 30, 40, 50];
    // next we iterate over all members of array "a"
    for element in a.iter() {
        println!("the value is: {}", element);
    } // end for
} // end main

Pattern match

There is one more control statement that is very important in Rust. This will replace “switch” or “case” that is available in Julia and Level. The idea of this statement is simple. We create a structure to check several conditions. We execute one statement for witch the condition is true. The beauty of Rust is that this condition is verified such way that all cases are covered. If we do not cover all cases a compiler error will be generated.

Example:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Observation 1: The match has a special syntax symbol: “=>” this is like a “result” and is not used in any other place in the language. The function result type has a similar symbol “->”. I have no idea why the symbol is different. It could be the same in my opinion.

Observation 2: The symbol “::” is used here to extract element members from Coin. In my opinion we could use “.”. For some reason I do not like this symbol “::”. In Level language we use “.” for all membership operations.

Observation 3: The last element into enumeration has a coma after it. “Quarter,”. In my opinion this is not necessary. I have look twice in the manual and this is the syntax. I can’t complain but looks bad.

Error handling

Some computer experts do not consider error handling part of control flow. In my opinion this is just another path of the program. Is the black path or the unwanted path. This path has to be treated and tested like the normal path. It is an important part of your code.

Rust groups errors into two major categories: recoverable and unrecoverable errors. Recoverable errors are situations in which it’s reasonable to report the problem to the user and retry the operation, like a file not found error. Unrecoverable errors are always symptoms of bugs, like trying to access a location beyond the end of an array.

Panic

We use panic to create an unrecoverable error and stop program execution.

fn main() {
    panic!("crash and burn");
}

Result

We do not have a try block like in Java. Instead we use a special type that is the “Result” type.  In Rust we do not have a classic approach but a more exotic one. To understand how is done you must unintended the Result type the match patterns and the new operator “?”.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

 Note: Result is an enumeration and a generic type.

In Rust library some functions may return a Result. To use these functions we need to handle the errors. We may decide to continue the program or to create a meaningful message and exit the program. Or we may decide to do nothing and let the error propagate out of current scope.

Observe that Result start with capital letter. Normally any function return a result but only some functions return a Result. That is a difference between the two. To function that return a Result may be successful but may be in error that need to be handled.

We handle the errors using pattern matching we have just learned recently.

Here is how is done:

use std::fs::File;

fn main() {
    // we try to open the file and capture the result in handler f of type Result
    let f = File::open("hello.txt");

    // now we look at f that is of type result and handle the error 
    let f = match f {
        Ok(file) => file,
        Err(error) => {
            panic!("There was a problem opening the file: {:?}", error)
        },
    }; // end match
} //end main

It looks very complicated? Read the comments in the example maybe this will help. The good news is there are some tricks already implemented that makes the program much more easy to read and follow. Most of the time we do not handle the errors using match but using these tricks I will explain next:

The tricks

First trick is to do nothing at all by propagating the error to next level. This is using operator “?”. In the case of error we do not know what to do so we put the question mark and continue programming. If an error is present this will be propagated and program will probably stop working.

use std::fs::File;

fn main() {
    let f = File::open("hello.txt")?;
}

Do you see the question mark after the expression open()?. That is all now the error is unhandled but propagated.

The shortcut

The second trick is more neat. We use a built-in helper method for Result type. This method can be call with dot operator “.” after the expression that can return a type Result. We just mention .expect(“error message”) or even better we use .unwrap(). That’s it a simple function call will resolve our expected error issue. Now the error is “handled”

Using expect:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Error: File not found!");
}

Using unwrap:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}

 

That is about it. Of course this is a short skimming through. For more details look into manual.