[The Rust Programming Language] 3. Common Programming Concepts

3. Common Programming Concepts

You’ll learn about variables, basic types, functions, comments, and control flow. These foundations will be in every Rust program.

3.1 Variables and Mutability

By default variables are immutable in Rust.

Example:

1
2
3
4
5
6
7
8
// src/main.rs

fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}

When we run the program now, we get this:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ cargo run 
Compiling playground v0.0.1 (/playground)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: make this binding mutable: `mut x`
3 | println!("The value of x is: {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable

However, you still have the option to make your variables mutable

You can make them mutable by adding mut in front of the variable name. In addition to allowing this value to change, mut conveys intent to future readers of the code by indicating that other parts of the code will be changing this variable’s value.

Example:

1
2
3
4
5
6
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}

When we run the program now, we get this:

1
2
3
4
5
6
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6

We’re allowed to change the value that x binds to from 5 to 6 when mut is used. In some cases, you’ll want to make a variable mutable because it makes the code more convenient to write than if it had only immutable variables.

Differences Between Variables and Constants

Constants like immutable variables, constants are values that are bound to a name and are not allowed to change, but there are a few differences between constants and variables.

  • First, you aren’t allowed to use mut with constants. Constants aren’t just immutable by default—they’re always immutable.

  • You declare constants using the const keyword instead of the let keyword, and the type of the value must be annotated.

  • Constants can be declared in any scope, including the global scope, which makes them useful for values that many parts of code need to know about.

  • The last difference is that constants may be set only to a constant expression, not the result of a function call or any other value that could only be computed at runtime.

1
const MAX_POINTS: u32 = 100_000;

Constants are valid for the entire time a program runs, within the scope they were declared in, making them a useful choice for values in your application domain that multiple parts of the program might need to know about, such as the maximum number of points any player of a game is allowed to earn or the speed of light.

Naming hardcoded values used throughout your program as constants is useful in conveying the meaning of that value to future maintainers of the code. It also helps to have only one place in your code you would need to change if the hardcoded value needed to be updated in the future.

Shadowing

We can shadow a variable by using the same variable’s name and repeating the use of the let keyword as follows:

1
2
3
4
5
6
7
8
9
fn main() {
let x = 5;

let x = x + 1;

let x = x * 2;

println!("The value of x is: {}", x); // The value of x is: 12
}

Shadowing is different from marking a variable as mut, because we’ll get a compile-time error if we accidentally try to reassign to this variable without using the let keyword. By using let, we can perform a few transformations on a value but have the variable be immutable after those transformations have been completed.

The other difference between mut and shadowing is that because we’re effectively creating a new variable when we use the let keyword again, we can change the type of the value but reuse the same name.

1
2
3
4
5
let spaces = "   ";
let spaces = spaces.len();

let mut spaces = " ";
spaces = spaces.len(); // error[E0308]: mismatched types

3.2 Data Types

Keep in mind that Rust is a statically typed language, which means that it must know the types of all variables at compile time. The compiler can usually infer what type we want to use based on the value and how we use it.

Scalar Types

A scalar type represents a single value. Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters. You may recognize these from other programming languages.

Integer Types

An integer is a number without a fractional component.

1
2
3
4
5
6
7
Length	Signed	Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

Each variant can be either signed or unsigned and has an explicit size. Signed and unsigned refer to whether it’s possible for the number to be negative.

Each signed variant can store numbers from -(2n - 1) to 2n - 1 - 1 inclusive, where n is the number of bits that variant uses. So an i8 can store numbers from -(27) to 27 - 1, which equals -128 to 127. Unsigned variants can store numbers from 0 to 2n - 1, so a u8 can store numbers from 0 to 28 - 1, which equals 0 to 255.

Additionally, the isize and usize types depend on the kind of computer your program is running on: 64 bits if you’re on a 64-bit architecture and 32 bits if you’re on a 32-bit architecture.

Note that all number literals except the byte literal allow a type suffix, such as 57u8, and _ as a visual separator, such as 1_000.

1
2
3
4
5
6
Number literals	Example
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte (u8 only) b'A'

Integer Overflow

Let’s say you have a variable of type u8 that can hold values between 0 and 255. If you try to change the variable to a value outside of that range, such as 256, integer overflow will occur. Rust has some interesting rules involving this behavior. When you’re compiling in debug mode, Rust includes checks for integer overflow that cause your program to panic at runtime if this behavior occurs. Rust uses the term panicking when a program exits with an error; we’ll discuss panics in more depth in the “Unrecoverable Errors with panic!” - https://doc.rust-lang.org/book/ch09-01-unrecoverable-errors-with-panic.html section in Chapter 9.

When you’re compiling in release mode with the --release flag, Rust does not include checks for integer overflow that cause panics. Instead, if overflow occurs, Rust performs two’s complement wrapping. In short, values greater than the maximum value the type can hold “wrap around” to the minimum of the values the type can hold. In the case of a u8, 256 becomes 0, 257 becomes 1, and so on. The program won’t panic, but the variable will have a value that probably isn’t what you were expecting it to have. Relying on integer overflow’s wrapping behavior is considered an error.

To explicitly handle the possibility of overflow, you can use these families of methods that the standard library provides on primitive numeric types:

  • Wrap in all modes with the wrapping_* methods, such as wrapping_add

  • Return the None value if there is overflow with the checked_* methods

  • Return the value and a boolean indicating whether there was overflow with the overflowing_* methods

  • Saturate at the value’s minimum or maximum values with saturating_* methods

Floating-Point Types

Rust also has two primitive types for floating-point numbers, which are numbers with decimal points. Rust’s floating-point types are f32 and f64, which are 32 bits and 64 bits in size, respectively. The default type is f64 because on modern CPUs it’s roughly the same speed as f32 but is capable of more precision.

1
2
3
4
5
fn main() {
let x = 2.0; // f64

let y: f32 = 3.0; // f32
}

Floating-point numbers are represented according to the IEEE-754 standard. The f32 type is a single-precision float, and f64 has double precision.

Numeric Operations

Rust supports the basic mathematical operations you’d expect for all of the number types: addition, subtraction, multiplication, division, and remainder. The following code shows how you’d use each one in a let statement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
// addition
let sum = 5 + 10;

// subtraction
let difference = 95.5 - 4.3;

// multiplication
let product = 4 * 30;

// division
let quotient = 56.7 / 32.2;

// remainder
let remainder = 43 % 5;
}

Each expression in these statements uses a mathematical operator and evaluates to a single value, which is then bound to a variable. Appendix B contains a list of all operators that Rust provides.

The Boolean Type

a Boolean type in Rust has two possible values: true and false. Booleans are one byte in size. The Boolean type in Rust is specified using bool. For example:

1
2
3
4
5
fn main() {
let t = true;

let f: bool = false; // with explicit type annotation
}

The main way to use Boolean values is through conditionals, such as an if expression. We’ll cover how if expressions work in Rust in the “Control Flow” - https://doc.rust-lang.org/book/ch03-05-control-flow.html#control-flow section.

The Character Type

Rust’s char type is the language’s most primitive alphabetic type, and the following code shows one way to use it. (Note that char literals are specified with single quotes, as opposed to string literals, which use double quotes.)

1
2
3
4
5
fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';
}

Rust’s char type is four bytes in size and represents a Unicode Scalar Value, which means it can represent a lot more than just ASCII. Accented letters; Chinese, Japanese, and Korean characters; emoji; and zero-width spaces are all valid char values in Rust. Unicode Scalar Values range from U+0000 to U+D7FF and U+E000 to U+10FFFF inclusive. However, a “character” isn’t really a concept in Unicode, so your human intuition for what a “character” is may not match up with what a char is in Rust. We’ll discuss this topic in detail in “Storing UTF-8 Encoded Text with Strings” - https://doc.rust-lang.org/book/ch08-02-strings.html#storing-utf-8-encoded-text-with-strings in Chapter 8.

Compound Types

Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.

The Tuple Type

A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size.

We create a tuple by writing a comma-separated list of values inside parentheses. Each position in the tuple has a type, and the types of the different values in the tuple don’t have to be the same. We’ve added optional type annotations in this example:

1
2
3
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}

The variable tup binds to the entire tuple, because a tuple is considered a single compound element. To get the individual values out of a tuple, we can use pattern matching to destructure a tuple value, like this:

1
2
3
4
5
6
7
fn main() {
let tup = (500, 6.4, 1);

let (x, y, z) = tup; // is called destructuring,

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

In addition to destructuring through pattern matching, we can access a tuple element directly by using a period (.) followed by the index of the value we want to access. For example:

1
2
3
4
5
6
7
8
9
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0;

let six_point_four = x.1;

let one = x.2;
}

The Array Type

Another way to have a collection of multiple values is with an array. Unlike a tuple, every element of an array must have the same type. Arrays in Rust are different from arrays in some other languages because arrays in Rust have a fixed length, like tuples.

In Rust, the values going into an array are written as a comma-separated list inside square brackets:

1
2
3
fn main() {
let a = [1, 2, 3, 4, 5];
}

Arrays are useful when you want your data allocated on the stack rather than the heap. An array isn’t as flexible as the vector type, though. A vector is a similar collection type provided by the standard library that is allowed to grow or shrink in size. If you’re unsure whether to use an array or a vector, you should probably use a vector. Chapter 8 - https://doc.rust-lang.org/book/ch08-00-common-collections.html discusses vectors in more detail.

You would write an array’s type by using square brackets, and within the brackets include the type of each element, a semicolon, and then the number of elements in the array, like so:

1
let a: [i32; 5] = [1, 2, 3, 4, 5];

Writing an array’s type this way looks similar to an alternative syntax for initializing an array: if you want to create an array that contains the same value for each element, you can specify the initial value, followed by a semicolon, and then the length of the array in square brackets, as shown here:

1
let a = [3; 5];

The array named a will contain 5 elements that will all be set to the value 3 initially. This is the same as writing let a = [3, 3, 3, 3, 3]; but in a more concise way.

Accessing Array Elements

An array is a single chunk of memory allocated on the stack. You can access elements of an array using indexing, like this:

1
2
3
4
5
6
fn main() {
let a = [1, 2, 3, 4, 5];

let first = a[0];
let second = a[1];
}

In this example, the variable named first will get the value 1, because that is the value at index [0] in the array. The variable named second will get the value 2 from index [1] in the array.

Invalid Array Element Access

What happens if you try to access an element of an array that is past the end of the array? Say you change the example to the following, which uses code similar to the guessing game in Chapter 2 to get an array index from the user:

This code panics!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use std::io;

fn main() {
let a = [1, 2, 3, 4, 5];

println!("Please enter an array index.");

let mut index = String::new();

io::stdin()
.read_line(&mut index)
.expect("Failed to read line");

let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");

let element = a[index];

println!(
"The value of the element at index {} is: {}",
index, element
);
}

When you attempt to access an element using indexing, Rust will check that the index you’ve specified is less than the array length. If the index is greater than or equal to the length, Rust will panic. This check has to happen at runtime, especially in this case, because the compiler can’t possibly know what value a user will enter when they run the code later.

3.3 Functions

Function definitions in Rust start with fn and have a set of parentheses after the function name. The curly brackets tell the compiler where the function body begins and ends.

Rust code uses snake case as the conventional style for function and variable names. In snake case, all letters are lowercase and underscores separate words. Here’s a program that contains an example function definition:

1
2
3
4
5
6
7
8
9
fn main() {
println!("Hello, world!");

another_function();
}

fn another_function() {
println!("Another function.");
}

Function Parameters

Functions can also be defined to have parameters, which are special variables that are part of a function’s signature. When a function has parameters, you can provide it with concrete values for those parameters. Technically, the concrete values are called arguments, but in casual conversation, people tend to use the words parameter and argument interchangeably for either the variables in a function’s definition or the concrete values passed in when you call a function.

1
2
3
4
5
6
7
fn main() {
another_function(5); // 5 is an argument.
}

fn another_function(x: i32) { // x is a parameter.
println!("The value of x is: {}", x);
}

Function Bodies Contain Statements and Expressions

Calling a function is an expression. Calling a macro is an expression. The block that we use to create new scopes, {}, is an expression

1
2
3
4
5
6
7
8
9
10
fn main() {
let x = 5;

let y = {
let x = 3;
x + 1 // This expression will be 4 to y.
};

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

Expressions do not include ending semicolons. If you add a semicolon to the end of an expression, you turn it into a statement, which will then not return a value. Keep this in mind as you explore function return values and expressions next.

Functions with Return Values

Functions can return values to the code that calls them. We don’t name return values, but we do declare their type after an arrow (->). In Rust, the return value of the function is synonymous with the value of the final expression in the block of the body of a function. You can return early from a function by using the return keyword and specifying a value, but most functions return the last expression implicitly.

1
2
3
4
5
6
7
8
9
fn five() -> i32 {
5
}

fn main() {
let x = five();

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

But if we place a semicolon at the end of the line containing x + 1, changing it from an expression to a statement, we’ll get an error.

1
2
3
4
5
6
7
8
9
10
fn main() {
let x = plus_one(5);

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

fn plus_one(x: i32) -> i32 {
x + 1; // implicitly returns `()` as its body has no tail or `return` expression
// - help: consider removing this semicolon
}

The main error message, “mismatched types,” reveals the core issue with this code. The definition of the function plus_one says that it will return an i32, but statements don’t evaluate to a value, which is expressed by (), an empty tuple. Therefore, nothing is returned, which contradicts the function definition and results in an error. In this output, Rust provides a message to possibly help rectify this issue: it suggests removing the semicolon, which would fix the error.

3.4 Comments

In Rust, the idiomatic comment style starts a comment with two slashes, and the comment continues until the end of the line. For comments that extend beyond a single line, you’ll need to include // on each line, like this:

1
2
3
// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.

Comments can also be placed at the end of lines containing code:

1
2
3
fn main() {
let lucky_number = 7; // I’m feeling lucky today
}

Rust also has another kind of comment, documentation comments, which we’ll discuss in the “Publishing a Crate to Crates.io” section of Chapter 14 - https://doc.rust-lang.org/book/ch14-00-more-about-cargo.html.

3.5 Control Flow

The most common constructs that let you control the flow of execution of Rust code are if expressions and loops.

if Expressions

An if expression allows you to branch your code depending on conditions. You provide a condition and then state, “If this condition is met, run this block of code. If the condition is not met, do not run this block of code.”

1
2
3
4
5
6
7
8
9
fn main() {
let number = 3;

if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}

All if expressions start with the keyword if, which is followed by a condition.

Optionally, we can also include an else expression, which we chose to do here, to give the program an alternative block of code to execute should the condition evaluate to false. If you don’t provide an else expression and the condition is false, the program will just skip the if block and move on to the next bit of code.

Unlike languages such as Ruby and JavaScript, Rust will not automatically try to convert non-Boolean types to a Boolean. You must be explicit and always provide if with a Boolean as its condition.

1
2
3
4
5
6
7
fn main() {
let number = 3;

if number { // error[E0308]: mismatched types. You must use number != 0
println!("number was three");
}
}

Handling Multiple Conditions with else if

You can have multiple conditions by combining if and else in an else if expression.

1
2
3
4
5
6
7
8
9
10
11
12
13
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");
}
}

Using if in a let Statement

Because if is an expression, we can use it on the right side of a let statement.

1
2
3
4
5
6
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 }; // will be bound to a value based on the outcome of the if expression

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

Repetition with Loops

Rust has three kinds of loops: loop, while, and for. Let’s try each one.

Repeating Code with loop

The loop keyword tells Rust to execute a block of code over and over again forever or until you explicitly tell it to stop.

1
2
3
4
5
fn main() {
loop {
println!("again!");
}
}

Returning Values from Loops

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let mut counter = 0;

let result = loop {
counter += 1;

if counter == 10 {
break counter * 2; // Returning Values from Loops
}
};

println!("The result is {}", result);
}

Conditional Loops with while

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let mut number = 3;

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

number -= 1;
}

println!("LIFTOFF!!!");
}

Looping Through a Collection with for

The safety and conciseness of for loops make them the most commonly used loop construct in Rust.

1
2
3
4
5
6
7
fn main() {
let a = [10, 20, 30, 40, 50];

for element in a.iter() {
println!("the value is: {}", element);
}
}

References

[1] The Rust Programming Language - The Rust Programming Language - https://doc.rust-lang.org/book/title-page.html

[2] Common Programming Concepts - The Rust Programming Language - https://doc.rust-lang.org/book/ch03-00-common-programming-concepts.html