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.

Day 2 of Reading "The Rust Book" Cover to Cover: Variables, Shadowing & Constants

Quick summary of what I learnt in chapter 2:

Variables in Rust are immutable by default.

fn main() {
let x = 5;
// x = 6; // ❌ Won't compile
}

Trying to assign a new value to x will result in a compile-time error because Rust doesn't allow you to modify immutable variables. If you actually want to change a variable, you need to declare it as mutable.

let mut x = 5;x = 6;

Shadowing

Rust lets you declare a new variable with the same name as an existing one. This is called shadowing.

fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}
Output:
The value of x in the inner scope is: 12
The value of x is: 6

Each let x = ... creates an entirely new variable instead of modifying the previous one.

The inner x only exists inside that block. Once the block ends, Rust goes back to using the outer x.

Shadowing is not the same as mut

Shadowing looks similar to making a variable mutable, but they’re actually different.

Even after shadowing, the new variable is still immutable.

fn main() {
let x = 5;
let x = x + 1;
// x = 10; // ❌ Won't compile
}

The second let creates a brand-new variable. It doesn’t make x mutable.

Advantage of shadowing

You can change the type of a variable while keeping the same name.

let spaces = "   ";
let spaces = spaces.len();

In the first line, spaces is a string.

In the second line, spaces becomes a number representing the string's length.

This doesn't work with the mutable variables, because the type has to stay the same.

let mut spaces = "   ";
spaces = spaces.len(); // ❌ Type mismatch

Constants

Rust also has constants, which are declared using the const keyword instead of let.

A few things to remember:

  • Constants are always immutable.
  • Their type must be specified.
  • They can be declared in any scope, including the global scope.
  • They can only be assigned a constant expression — not something calculated at runtime.
  • By convention, constant names are written in UPPER_SNAKE_CASE.

Example:

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

Unlike regular variables, constants remain valid for the entire lifetime of the program within the scope where they’re defined.

Final thoughts

On to Day 3.