Control Flow
Learning Objectives
- You know how to work with conditional statements, pattern matching, and loops in Rust.
Rust comes with commonly used control flow functionality such as conditional statements, pattern matching, and loops. We’ve already used these in Dart and Gleam, but as Rust has some nuances, we’ll visit them again.
Conditional Statements
Similar to Gleam, the if
statement is a reserved keyword in Rust. Unlike in Gleam, though, the if
statement in Rust does work instead of just stating that the if
statement is not available.
The syntax is similar to many other languages such as C, Java and Python. As an example, the following code snippet prints out the polarity of the integer x
.
The condition operand must evaluate to a boolean value, i.e. true
or false
. Numbers 1
and 0
do not equal true
and false
in Rust like in the C language.
Note also that the condition operand is not surrounded by parentheses, since they are not necessary in Rust unlike in many other languages. The compiler will even warn if we add unnecessary parentheses around the condition operand.
On the other hand, the curly braces for the if block are mandatory, and the if block must contain at least one statement.
Ifs are expressions
Similarly to code blocks, an if statement is an expression like any other expression in Rust. Leaving out semicolons from each branch causes the if expression to evaluate into the value.
When if is used as an expression, all cases must be covered by the branches and the branches must evaluate to the same type (unless returning early, e.g. with the return
keyword). Otherwise, the compiler will complain.
Comparison operators
Rust has the same comparison and logical operators available in most other contemporary programming languages. The following shows a program that calculates the price of a product per the amount of kilograms, given that the product is in stock and the price per kilogram is positive. Otherwise, the function returns -1
.
Floating point comparison with epsilon
Integer comparison works like we are used to and have already seen.
Floating point comparison on the other hand might not work as we expect due to the way floating point numbers are encoded in bytes — floating point encoding is not a topic for this course but more info can be found here.
The important thing to notice here is that floating point calculations are not always exact.
For more reliable floating point comparison, we can use an error margin suitable for our purposes and see if the values are within the margin’s radius of each other. Rust provides a handy constant EPSILON
that can be used as a baseline error margin.
Matching patterns
Rust has a match expression that is somewhat similar to what we’ve seen with Gleam and with Dart’s switch expressions. Matches are always exhaustive, meaning that all possible values of the type must be covered by the patterns.
The match syntax consists of the match
keyword, a scrutinee expression, and match arms. The value of the scrutinee expression is compared to patterns in the arms, which are enclosed in braces . Each arm has a pattern, followed by a fat arrow =>
and a target expression. Like with if
and else if
conditions, the matching is done from top to bottom, and the first matching arm of the expression is evaluated.
As stated earlier, all values of the type must be covered by the patterns. For booleans it is enough for the patterns to cover the two values true
and false
. On the other hand, for variables with near infinite possible values, like integers, we need to cover all possible values or use a wildcard pattern to match all remaining values. Similar to Gleam, the _
underscore is used as a wildcard pattern.
Rust also provides a range pattern that can be used to match a range of values. The range pattern is defined by two values separated by two dots ..
. The range pattern is inclusive of the start value and exclusive of the end value — pattern ..a
matches all numbers from minimum (e.g. i32::MIN
for i32
) to a
, a..
from a
to maximum, and a..=b
from a
to b
(inclusive range).
The following example shows how to use a range pattern to match a range of values.
The following shows an example of a function that converts integers to Roman numerals. The function uses a match expression to match the integer to a Roman numeral string. The function is recursive, calling itself with the integer incremented or decremented by a value depending on the match arm.
Note how the function call in the arm of the above example uses the &
operator in front of the function call to create a reference to the returned string. Try removing the &
operator and see what happens when you run the code — fortunately, the compiler provides quite informative error messages.
Match guards
Match guards are used to add conditions to match arms. A match guard is defined by adding an if
keyword and a condition after the pattern in a match arm. The condition is evaluated after the pattern is matched, and the arm is only evaluated if the condition is true.
With match guards, there is still a need to verify that all cases are covered by the patterns. The compiler isn’t however capable of determining whether a collection of guards cover all the possible cases of a pattern — thus, a wildcard is needed at the end.
Looping
Rust provides us with three types of loops: loop
, while
, and for
. The common keywords break
and continue
are also available for controlling the flow of the loops.
Infinite looping
The loop
loop is the simplest form, repeating code indefinitely.
loop {
println!("Maxing out the CPU...");
}
The following example shows using the loop
to calculate the powers of 2 until the power exceeds 100.
Loops as expressions
Like if statements, loops can be used as expressions. The value of the loop expression is the value specified with break <value>;
(using break
works just like using return
, only it returns from a surrounding loop expression and not the surrounding function).
We may also want to skip the rest of the loop body and jump to the beginning of the loop. This can be done with the continue
statement.
While
The while
works similarly in Rust as it does in other programming languages. The keyword while
is followed by an expression that evaluates to a boolean value. The body of the loop is executed as long as the condition evaluates to true
.
While and syntax abuse
The condition of a while-loop can be any expression (including one with side effects such as mutating variables) as long as the expression evaluates to a boolean value. Blocks are expressions in Rust, which means that a while
loop can be turned into a do-while
loop.
While this works, it is heavily frowned upon because it is much less readable than the Rust’s while
when used normally. And then there is this, in the Rust compiler’s test suite even.
For
The for
loop in Rust is used to iterate over a set of values. The for
loop is used with an iterator, which is a type that implements the Iterator
trait. Similarly to using the range pattern previously, we can create a range of values to iterate over with for
.
Leaving out the =
in the range pattern would create a range that does not include the last value, so 0..3
would iterate over the values 0
, 1
, and 2
.
Exclusive range in patterns?
The ..=
syntax is used for both ranges and patterns. In patterns, however the =
is required due to current compiler limitations (overlap with slice syntax parsing). Trying to use an exclusive range as a pattern causes an error.
There is no for (int i = 0; i < 10; i++)
syntax in Rust, as the for
loop uses iterators. To achieve the functionality, use while
.