Day 5 of Reading "The Rust Book" Cover to Cover: Control Flow

 



Rust Programming Language

Control flow lets you run code conditionally or repeatedly. Three main constructs: if, else, and loops.

if/else

Conditions don’t need parentheses, but they must evaluate to a boolean.

fn main() {
let x = 5;

if x > 3 {
println!("x is greater than 3"); // ✅ Prints this
}
}

Adding else:

fn main() {
let x = 5;

if x > 10 {
println!("x is greater than 10");
} else {
println!("x is 10 or less"); // ✅ Prints this
}
}

else if

Chain conditions:

fn main() {
let x = 6;

if x % 2 == 0 {
println!("x is even");
} else if x % 3 == 0 {
println!("x is divisible by 3");
} else {
println!("x is neither even nor divisible by 3");
}
}

if as an Expression

Here’s the weird one: if is an expression, not just a statement. You can assign its result to a variable.

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

println!("x: {}", x); // ✅ x: 5
}

The values being returned from both branches must be the same type.

let x = if condition { 5 } else { "six" };  // ❌ Error: type mismatch

Loops

Rust has three ways to loop: loop, while, and for.

loop

The simplest: run forever until you explicitly break.

fn main() {
let mut count = 0;

loop {
count += 1;
println!("Count: {}", count);

if count == 3 {
break; // ✅ Exit the loop
}
}
}

Output:

Count: 1
Count: 2
Count: 3

while

Keep looping while a condition is true.

fn main() {
let mut x = 5;

while x > 0 {
println!("x: {}", x);
x -= 1;
}

println!("Blastoff!");
}

Output:

x: 5
x: 4
x: 3
x: 2
x: 1
Blastoff!

for

Iterate over a collection or range. This is the idiomatic Rust way.

fn main() {
let arr = [10, 20, 30, 40, 50];

for element in arr {
println!("Element: {}", element);
}
}

Output:

Element: 10
Element: 20
Element: 30
Element: 40
Element: 50

Ranges work too:

fn main() {
for i in 1..4 {
println!("i: {}", i); // ✅ 1, 2, 3 (4 is excluded)
}
}

Use ..= to include the end value:

fn main() {
for i in 1..=4 {
println!("i: {}", i); // ✅ 1, 2, 3, 4 (4 is included)
}
}

Looping with break and continue

break exits the loop immediately. continue skips to the next iteration.

fn main() {
for i in 1..10 {
if i == 3 {
continue; // Skip 3
}
if i == 7 {
break; // Stop at 7
}
println!("i: {}", i);
}
}

Output:

i: 1
i: 2
i: 4
i: 5
i: 6

Key takeaway for Future Me:

Use for loops when iterating over collections or ranges. It's safer (no index out of bounds) and more idiomatic than while loops with manual indexing.

And don’t forget: in Rust, if is an expression, so you can assign its result.

Day 4 of Reading “The Rust Book” Cover to Cover: Functions & Expressions

 

Rust Logo

Functions are declared with the fn keyword. Rust doesn't care where you define them, before or aftermain(), as long as they're defined somewhere.

Function Basics

fn main() {
greet();
greet(); // Can call it multiple times
}
fn greet() {
println!("Hello!");
}

Output:

Hello!
Hello!

Parameters

Functions can take parameters. You must declare the type of each parameter.

fn add(a: i32, b: i32) {
println!("Sum: {}", a + b);
}
fn main() {
add(5, 3); // ✅ Sum: 8
}

Return Values

Functions can return values using the -> syntax. The last expression in a function is automatically returned (no semicolon).

fn add(a: i32, b: i32) -> i32 {
a + b // ✅ Returns the result (no semicolon)
}
fn main() {
let result = add(5, 3);
println!("Result: {}", result); // 8
}

Important: If you add a semicolon, it becomes a statement and returns nothing (unit type ()).

fn add(a: i32, b: i32) -> i32 {
a + b; // ❌ Error: expected i32, found ()
}

Statements vs Expressions

Statements are instructions that perform an action but don’t return a value. They end with a semicolon.

Expressions evaluate to a resulting value that can be returned or assigned. They do NOT end with a semicolon.

fn main() {
let x = 5 + 6; // ✅ Expression: 5 + 6 evaluates to 11

let y = {
let x = 3;
x + 1 // ✅ Expression: evaluates to 4 (no semicolon)
};
println!("y: {}", y); // 4
}

If you add a semicolon to that inner x + 1, it becomes a statement and returns () instead.

let y = {
let x = 3;
x + 1; // ❌ Semicolon makes it a statement, y becomes ()
};

That’s why return values in functions work without semicolons; the last line is an expression.

fn double(x: i32) -> i32 {
x * 2 // ✅ Expression, returns the value
}

Multiple Parameters

fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
fn main() {
print_labeled_measurement(5, 'm'); // ✅ The measurement is: 5m
}

Early Returns

You can return early from a function using the return keyword, but it's not common in idiomatic Rust. Usually, you just use an expression.

fn check(x: i32) -> i32 {
if x < 0 {
return 0; // Early return
}
x + 1 // Normal return
}

Key takeaway for Future Me:

No semicolon = expression = returns value

Semicolon = statement = returns nothing

If your function isn’t returning what you expect, check if you accidentally added a semicolon.

Day 3 of Reading “The Rust Book” Cover to Cover: Data Types

Rust is a statically typed language, which means the type of every variable is known at compile time. Most of the time, Rust can infer the type, so we don’t need an explicit declaration. But sometimes we do.

Scalar Types

Scalar types represent a single value. There are four primary scalar types: integers, floating-point numbers, booleans, and characters.

Integers

Integers come in different sizes. The main ones:

  • Signed: i8, i16, i32, i64, i128 (can be negative)
  • Unsigned: u8, u16, u32, u64, u128 (only positive)

By default, Rust uses i32.

fn main() {
let x = 5; // ✅ i32 inferred
let y: u8 = 255; // ✅ explicitly u8
let z: i64 = 10; // ✅ explicitly i64
}

An important note: if you try to store a value outside the range, you’ll get a compile error.

let num: u8 = 256;  // ❌ Compile error (u8 max is 255)

Floating-Point Numbers

Two floating-point types: f32 and f64 (default).

fn main() {
let x = 2.0; // ✅ f64 inferred
let y: f32 = 3.14; // ✅ explicitly f32
}

Booleans

Pretty straightforward.

fn main() {
let t = true;
let f: bool = false;
}

Characters

char represents a single Unicode character. Use single quotes, not double quotes.

fn main() {
let c = 'z';
let emoji = '😀'; // ✅ Works fine
}

Compound Types

Compound types group multiple values into one type. The two primary ones: tuples and arrays.

Tuples

A fixed-length collection of valuespotentially of different types.

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

// Destructuring
let (x, y, z) = tup;
println!("y: {}", y); // 6.4

// Or access by index
println!("First: {}", tup.0); // 500
}

Tuples don’t have a length that can change. Once created, they’re fixed.

Arrays

A fixed-length collection of values with the same type.

fn main() {
let arr = [1, 2, 3, 4, 5]; // ✅ Inferred as [i32; 5]
let arr2: [i32; 5] = [1, 2, 3, 4, 5]; // ✅ Explicit

// Access by index
println!("First: {}", arr[0]); // 1

// Initialize with same value
let arr3 = [3; 5]; // [3, 3, 3, 3, 3]
}

If you try to access an index that doesn’t exist, you’ll get a runtime panic. Rust catches this and prevents undefined behavior.

let arr = [1, 2, 3];
println!("{}", arr[10]); // ❌ Panic: index out of bounds

Tuples vs Arrays

Key difference:

  • Tuples: Fixed length, can have different types. Use when you want to group different data.
  • Arrays: Fixed length, same type. Use when all elements are the same type.

Think of tuples as a structured record, and arrays as a collection of the same thing.

That’s it for Day 3. Chapter 3 also covers functions, but that’s getting long, so it'll be in Day 4.