🦀 The Land of Rust: Ferris the Crab’s Space Adventures
Chapter 1: Ferris, the Space Crab, and the Lost Toolbox (Installation & Hello World)
📋 Chapter Outline:
1.1. The Adventure Begins!
1.1.1. Meet Ferris
Hello, friend! 👋 My name is Ferris. I’m a cute space crab from the planet “Crab”! 🦀🚀
My spaceship crash-landed right in your backyard a few nights ago. (Yes, those weird noises you heard… that was me!)
Now my ship is wobbling. The engines are off, the navigation screen is black, and the only thing still working is an old Earth computer. 💻
I need to talk to this computer to fix my ship. But here’s the problem: Earth computers only understand certain languages. One of those languages is Rust.
💡 Fun Fact: The word “Rust” in English means corrosion. But don’t worry! This language is neither rusty nor old. Actually, it’s one of the newest, fastest, and safest languages in the world! ✨
![[Illustration: A cheerful red-shelled crab with large expressive eyes stands on two legs beside a retro computer monitor. The screen displays glowing green text reading “Earth, I am here!” The background shows a slightly messy spaceship interior with a round window revealing a starry sky. The art style is vibrant and warm, perfect for children’s books.]](assets/images/1.1.png)
1.1.2. Why Does Ferris Need a New Language?
You might ask: “Don’t computers understand my language?” 😄
No, my friend! Computers only understand zeros and ones. And we humans can’t speak in zeros and ones! That’s why engineers created programming languages—to be a bridge between what we say and what computers understand.
Rust is one of the best bridges:
- ⚡ It’s super fast
- 🛡️ It’s super safe (it won’t let your program suddenly freeze or crash!)
- 🎮 Big companies like Google, Microsoft, and even game developers use it
And most importantly: Rust teaches you to think like an engineer. I use Rust on all my space travels to keep my ship safe and avoid getting lost in space! 🌌
![[Illustration: A laptop screen shows a split view: on the left side are zeros and ones, on the right side are colorful code blocks. A small friendly crab mascot sits on the keyboard pointing at the code. The background shows a simple desk with a coffee mug, notebook, and a tiny toy rocket. Clean, bright, educational style.]](assets/images/1.2.png)
1.1.3. Ferris at the Terminal
![[Illustration: A wide scene showing Ferris the red crab sitting in front of a dark terminal screen, typing with his claws. Glowing code lines reflect on his shell. Floating around him are tiny cartoon spaceships, stars, and gears. The style is whimsical and colorful, with high-quality children’s book illustration and cinematic lighting.]](assets/images/1.3.png)
1.2. The Magic Toolbox: Installing rustup
To speak Rust to the computer, we first need to install a magic toolbox called rustup. This toolbox contains three important things:
🔹 Compiler (rustc): Like a translator robot, it converts our words into zeros and ones.
🔹 Package Manager (cargo): A smart assistant that handles repetitive tasks for us (we’ll meet it soon).
🔹 Local Documentation: A complete handbook that’s always available offline.
📌 Before We Start: In this chapter, we’ll use a special window called the Terminal. The terminal is a black screen where we can type text commands to tell the computer what to do.
To open it:
- Windows: Press Start, type
cmd, and click on Command Prompt.- Mac: Press
Command + Space, typeTerminal, and press Enter.- Linux: Press
Ctrl + Alt + Ttogether.
Now, let’s install!
1.2.1. Go to rustup.rs
Open your browser (Chrome, Firefox, whatever you have). In the address bar, type:
https://rustup.rs
and press Enter. A page like this will appear:
![[Illustration: A close-up of a laptop screen displaying the rustup.rs homepage. A large, inviting blue “Download” button is visible. A tiny cartoon crab logo peeks from the corner of the browser window. The background shows a clean, minimal desk setup with soft pastel lighting. Modern educational illustration style with clear UI focus.]](assets/images/1.4.png)
1.2.2. Click the Download Button
Click on the big blue button that says Download. The installer for your Windows, Mac, or Linux will start downloading.
📌 Note for Grown-ups: On Windows, a file named
rustup-init.exewill download. On Mac and Linux, you’ll receive a shell script.
1.2.3. Run the Downloaded File
🪟 Windows:
- Go to your Downloads folder and double-click the file. A black window (the terminal) will open.
🍎 Mac:
- Open Terminal. Then type these commands one by one and press Enter:
cd Downloads
sh rustup-init
(If you see a “permission denied” message, first run chmod +x rustup-init, then run sh rustup-init again.)
🐧 **Linux **(like Ubuntu)
- Open Terminal with
Ctrl+Alt+T. Then type:
cd Downloads
chmod +x rustup-init
./rustup-init
1.2.4. Choose Default Installation
After running, a text menu will appear. Just type 1 (for “Proceed with installation”) and press Enter.
Now wait a few seconds… Green lines will rain down the screen like magic. When you see this message:
Rust is installed now. Great!
You’re all done! 🎉
1.2.5. Verify the Installation
To make sure everything works, type this in the same terminal:
rustc --version
You should see something like:
rustc 1.85.0 (4d91de4e4 2025-02-17)
(The numbers might differ; what matters is that you don’t see a red error!)
Do the same for cargo:
cargo --version
If both commands show a version number, your magic toolbox is ready to go! 🛠️✨
![[Illustration: A terminal window with dark background and bright green success text “Rust is installed now. Great!” Next to the screen, Ferris the crab is doing a happy little dance, holding a tiny wrench. The desk has a small lamp and a notebook. Cheerful, cartoon, educational style with vibrant colors.]](assets/images/1.5.png)
1.3. Our First Words to the Computer: Hello, World
Now that we have the toolbox, let’s say our first sentence to the computer. We’re going to write: “Earth, I am here!”
1.3.1. Create the Adventure Folder
Create a new folder on your computer named majara. This will be the home for all our programs in this book.
You can create it with your mouse, or type these two commands in the terminal:
mkdir majara
cd majara
(mkdir means “make directory/folder”, and cd means “change directory/go into this folder”.)
1.3.2. Open a Text Editor
We need a magic notebook to write our code. You can use Notepad (Windows) or TextEdit (Mac), but it’s much better to install a dedicated editor. I recommend VS Code (free and excellent).
Download it from code.visualstudio.com. After installing, open it and open the majara folder (File → Open Folder).
![[Illustration: A child’s hand clicking “Open Folder” in VS Code interface. The folder name “majara” is highlighted in blue. Ferris the crab peeks curiously from behind the monitor. Semi-realistic cartoon style with warm lighting, educational tone.]](assets/images/1.6.png)
1.3.3. Write the Magic Code
In VS Code, create a new file (Ctrl+N) and type this code exactly:
fn main() {
println!("Earth, I am here!");
}
1.3.4. Line-by-Line Explanation
Let’s look at each line carefully:
Line 1: fn main() {
fnis short for “function”.mainis a special name that the compiler knows is where the program should start.- The parentheses
()are empty for now because we’re not giving it any information yet.
Line 2: println!("Earth, I am here!");
- Four spaces (or one
Tab) at the start mean: “This command is inside themainfunction.” - The
!afterprintlnmeans this is a magic spell that does something special (displays text on screen). lnmeans “after printing, go to the next line.”
Line 3: }
- The closing brace. It means the function is finished.
1.3.5. Save the File
Save the file as main.rs inside the majara folder. The .rs extension stands for Rust.
⚠️ Important: The filename must be
main.rs. When Rust seesmain, it knows where the program starts.
1.3.6. Compile and Run
Now we need to convert our code into zeros and ones. The compiler does this.
Open the terminal (if it’s closed) and go into the majara folder. To navigate, use cd with the folder path. For example, if majara is on your Desktop:
cd Desktop/majara
(If you’re unsure about paths, ask a parent for help.)
Then type:
rustc main.rs
Wait a moment. If there’s no error, a new file named main (or main.exe on Windows) will be created. Now run it:
- 🪟 Windows:
main.exe - 🍎🐧 Mac/Linux:
./main
1.3.7. See the Result
💥 Boom! The screen displays:
Earth, I am here!
Congratulations! 🎉 You just wrote and ran your first Rust program. Ferris is so excited to see this message!
![[Illustration: Ferris the crab jumping joyfully in front of a terminal screen that displays “Earth, I am here!” Colorful confetti and tiny stars float around. The background shows a cozy desk with a mug and notebook. Vibrant, celebratory children’s book illustration with high energy.]](assets/images/1.7.before.png)
1.4. My Smart Helper: Cargo
So far, we built our program manually with rustc. But for bigger projects, we need a smart assistant to handle repetitive tasks. Its name is Cargo. 📦🤖
1.4.1. What Is Cargo?
Cargo does three important things for us:
- Creates project structure (folders and initial files).
- Manages dependencies (libraries written by others).
- Builds and runs programs easily (with a single simple command!).
1.4.2. Create a New Project with Cargo
Let’s create a new project. Go to a clean folder (like your Desktop) and type in the terminal:
cargo new hello_ferris
cd hello_ferris
Cargo creates a folder named hello_ferris containing:
hello_ferris/
├── Cargo.toml
├── src/
│ └── main.rs
└── .gitignore
(Don’t worry about .gitignore for now; you’ll learn about it later.)
![[Illustration: A clean infographic-style illustration showing a folder tree: hello_ferris/ containing Cargo.toml and src/main.rs highlighted in bright colors. Ferris the crab stands beside it pointing like a tour guide. Modern, educational, vector-based style.]](assets/images/1.7.png)
1.4.3. Meet Cargo.toml
Open the Cargo.toml file. You’ll see something like this:
[package]
name = "hello_ferris"
version = "0.1.0"
edition = "2024"
[dependencies]
🔹 [package]: Your project’s ID card (name, version, and year standard).
🔹 [dependencies]: Here we’ll later write the names of helper libraries (it’s empty for now).
This file is also called the “project manifest.” Every Rust project must have a Cargo.toml.
1.4.4. Meet the src Folder
All our code must go inside the src folder. Cargo already placed a ready-made main.rs:
fn main() {
println!("Hello, world!");
}
(Yes! Exactly the same code we wrote, just in English.)
1.4.5. Run with cargo run
Now you don’t need to manually run rustc. Just type this in the terminal (inside the hello_ferris folder):
cargo run
Cargo does these things automatically:
- ✅ Checks if the code has changed.
- ✅ Compiles if needed.
- ✅ Runs the program.
You’ll see the output:
Hello, world!
Quick and easy, right? 😎
![[Illustration: Cartoon illustration of Ferris the crab pressing a big green button labeled “cargo run”. Next to him, a terminal screen pops up showing “Hello, world!” Speed lines and sparkles emphasize quick action. Fun, dynamic, children’s book style.]](assets/images/1.8.png)
1.4.6. Difference Between cargo build and cargo run
🔹 cargo run = Compile + Run (when you want to see the result immediately).
🔹 cargo build = Compile only (creates an executable in target/debug/, without running).
💡 A note for when you grow up: If you run
cargo build --release, you’ll get a faster, optimized output (but it takes longer to build). We don’t need that for now.
1.4.7. Exercise: Change the Message
Now let’s change the text. Open src/main.rs and instead of "Hello, world!", write:
#![allow(unused)]
fn main() {
println!("Hello Ferris! Welcome to Earth!");
}
Save it and run cargo run again. Now the output is:
Hello Ferris! Welcome to Earth!
Well done! You’re now friends with Cargo. 🤝
1.5. Summary & Challenge
1.5.1. What We Learned
In this chapter, you learned:
- ✅ Who Ferris is and why we need Rust.
- ✅ How to install
rustup. - ✅ How to write a
main.rsfile and compile it withrustc. - ✅ How to create a project with
cargo newand run it withcargo run. - ✅ The meaning of
fn main(),println!, braces, and semicolons.
1.5.2. New Terms Glossary
Let’s review some new words:
| Term | Simple Meaning | Emoji |
|---|---|---|
| Compiler | A robot that translates our code into zeros and ones | 🤖 |
| Source Code | The text we write (like main.rs) | 📝 |
| Terminal | The black screen where we type commands | ⬛ |
| Run | When we turn on the program to make it work | ▶️ |
| **Magic Spell **(Macro) | A command with ! that does special things | ✨ |
1.5.3. Small Challenge
Now it’s your turn, champion! 🏆 Complete these missions:
- 1️⃣ Use
cargo newto create a project namedmy_first_program. - 2️⃣ In
src/main.rs, write:"I'm learning to code!" - 3️⃣ Run it with
cargo runand see the result. - 4️⃣ (Optional) Write two
println!lines in a row:
fn main() {
println!("Hello!");
println!("I'm Ferris. 🦀");
}
See what happens.
If you can do these, you’re totally ready for the next chapter! In Chapter 2, we’ll build a “Guess the Number” game where the computer picks a number and you have to guess it. Exciting, right? 😉
💬 Remember: Every great programmer started with “Hello, World.” You’ve just taken your first step! 🚀
![[Illustration: A child sitting at a desk looking happily at a computer terminal with a big green checkmark on screen. Ferris the crab stands on the desk giving a thumbs up. Floating text “Good luck!” in playful font above. Encouraging, bright, cartoon children’s book illustration style.]](assets/images/1.9.png)
🔚 End of Chapter 1
🦀 The Land of Rust: Ferris the Crab’s Space Adventures
Chapter 2: The Lost Star Game (Variables, Input & Conditions)
📋 Chapter Outline:
2.1. The Mystery: A Hidden Star
2.1.1. The Story: Ferris and the Lost Star
During his space travels, Ferris found a glowing star that can grant one wish! 🌟 But this playful crab decided to hide it in a secret locker aboard his ship.
Ferris looks at you with big, curious eyes and says: 🦀 “I picked a secret number between 1 and 100. If you guess it in the fewest tries, the star is yours! Tell me a number, and I’ll tell you to go higher or lower.”
This is the classic “Guess the Number” game. Today, we’ll build it in Rust so you can play against the computer!
2.1.2. Random Numbers Are Like Rolling Dice
Computers are logical machines. They can’t truly “pick randomly” on their own. But we can use a clever math formula that acts just like rolling dice! We call these pseudo-random numbers.
In Rust, engineers built a ready-to-use toolbox called rand that does this for us. We just need to add it to our project.
![[Illustration: A cartoon scene of Ferris holding a glowing cosmic die. Floating around it are numbers like 42, 7, and 99. The background shows a cozy spaceship control panel with soft blinking lights. Style: playful, vibrant children’s book illustration, warm lighting.]](assets/images/2.1.png)
2.1.3. Adding the rand Crate
Open your terminal and create a new project:
cargo new guess_the_number
cd guess_the_number
Now we need to tell cargo that we want the rand toolbox. Open Cargo.toml and add this line under [dependencies]:
[dependencies]
rand = "0.8.5"
Your file should look like this:
[package]
name = "guess_the_number"
version = "0.1.0"
edition = "2021"
[dependencies]
rand = "0.8.5"
0.8.5 is the version we want. The next time you run cargo run, Cargo will automatically download it from the internet. (Make sure you’re connected!)
2.1.4. Bringing the Dice Box into Our Code
Now that the toolbox is added, we need to tell our program which tool to use. At the top of src/main.rs, add:
#![allow(unused)]
fn main() {
use rand::Rng;
}
Then, inside main(), we create our secret number:
#![allow(unused)]
fn main() {
let secret_number = rand::thread_rng().gen_range(1..=100);
}
Let’s break this magic spell down:
🔹 rand::thread_rng() → Builds a dice-rolling machine just for our program.
🔹 .gen_range(1..=100) → Tells it to pick a number between 1 and 100. (..= means both 1 and 100 are included.)
💡 Quick test: Let’s print it now just to see it work (we’ll delete this line later so the game stays fair):
#![allow(unused)]
fn main() {
println!("Secret number (for testing only): {}", secret_number);
}
2.2. Memory Boxes (Variables)
In programming, when we need to remember something, we use a variable. Think of a variable as a small box inside the computer’s memory that holds a number, a word, or a true/false value.
2.2.1. Making a Box with let
In Rust, we create a box using the word let:
#![allow(unused)]
fn main() {
let x = 5;
}
This means: “Make a box named x and put the number 5 inside it.”
In our game, secret_number was already created. But we also need a box for the player’s guess:
#![allow(unused)]
fn main() {
let mut guess = String::new();
}
String::new() creates an empty text box. (String means a piece of text that can grow as long as needed.)
2.2.2. Why Boxes Are Locked by Default (Immutability)
One of Rust’s superpowers is this: When you create a box, its contents are locked by default. This is called immutability.
Example:
#![allow(unused)]
fn main() {
let x = 5;
x = 6; // ❌ Error! You can't change `x`.
}
The compiler quickly says:
error[E0384]: cannot assign twice to immutable variable `x`
Think of it like a padlocked chest! Unless you hand over the mut key, nobody can change what’s inside. This prevents silly mistakes from breaking your program.
2.2.3. Opening the Box with mut
Sometimes we do need to change what’s inside. When creating the box, we add the word mut (short for mutable):
#![allow(unused)]
fn main() {
let mut y = 10;
y = 20; // ✅ Now it's totally fine!
}
In our game, guess needs to hold different answers from the player, so we must create it with let mut.
![[Illustration: An educational cartoon showing two labeled boxes on a wooden desk. Box 1: “let x = 5” (secured with a small padlock). Box 2: “let mut y = 10” (open, with a tiny wrench inside). Ferris stands beside them, pointing at the difference. Style: bright, clear, infographic-style children’s illustration.]](assets/images/2.2.png)
2.2.4. Our Friendly Guide: Compiler Errors
If you get an error in Rust, don’t worry! The Rust compiler acts like a kind teacher. It shows exactly where you slipped up and often suggests a fix. For example, if you forget mut, it says:
help: consider making this binding mutable: `mut guess`
Remember: Errors aren’t enemies. They’re your helpers! 🤝
2.3. Listening to You (Reading Input)
Now we need to ask the player to type their guess and read it.
2.3.1. The Input Magic Spell
First, at the top of the file, add the input/output library:
#![allow(unused)]
fn main() {
use std::io;
}
(If you already added use rand::Rng;, you’ll now have two use lines.)
Inside main(), write:
#![allow(unused)]
fn main() {
io::stdin().read_line(&mut guess).expect("Failed to read input");
}
2.3.2. Breaking Down the Spell
This line might look scary, but let’s open it piece by piece:
🔹 io::stdin() → “Pick up the phone and connect to the keyboard.”
🔹 .read_line(&mut guess) → “Wait until the player types something and presses Enter. Pour whatever they typed into the guess box.”
🔹 .expect("Failed to read input") → “If something breaks the connection, show this message and stop safely.”
2.3.3. Why &mut guess? (Handing Over the Key)
The &mut symbol means temporary key. The read_line function wants to write inside our guess box. We give it permission to do that, but we don’t give away ownership of the box. For now, just remember: &mut means “You’re allowed to change what’s inside this box.” (We’ll dive deeper into these permissions in Chapter 4!)
![[Illustration: Ferris handing a glowing key labeled “&mut” to a small, friendly robot labeled “read_line”. The robot stands next to an open box named “guess”. Background: a soft, tech-themed workspace. Style: metaphorical, warm children’s book illustration.]](assets/images/2.3.png)
2.4. Type Conversion (Text to Number)
2.4.1. The Problem: Text vs. Numbers
When the player types 42, the computer sees it as text "42". But secret_number is a real number. We can’t compare apples to oranges! We must convert the text into a number.
2.4.2. Cleaning Up with trim()
When you press Enter, the computer secretly adds a newline character \n at the end. The trim() function sweeps away these extra spaces:
#![allow(unused)]
fn main() {
let clean_guess = guess.trim();
}
2.4.3. Converting Text to Number with parse()
Now the text is clean. With parse(), we say “Please turn this into a number”:
#![allow(unused)]
fn main() {
let guess: u32 = guess.trim().parse().expect("Please type a valid number!");
}
- 🔹
parse()→ Tries to turn text into a number. - 🔹
: u32→ Tells the compiler: “I want a positive whole number.” - 🔹
expect()→ If it fails, stops the program with our message. (Later, we’ll learn how to handle this gracefully without stopping!)
2.4.4. Shadowing: Same Name, New Job
Notice we used let guess again? In Rust, this is called shadowing. The old String box steps aside, and a new u32 box takes its place with the exact same name. It’s super handy because we don’t need to invent long names like guess_as_number!
2.5. If Not, Then What? (if / else)
2.5.1. Comparing the Guess to the Secret
The game logic is simple:
- 🔸 If guess < secret → Say “Go higher!”
- 🔸 If guess > secret → Say “Go lower!”
- 🔸 If guess == secret → Say “You won!”
2.5.2. Writing if / else if / else
#![allow(unused)]
fn main() {
if guess < secret_number {
println!("❄️ Too low! Go higher!");
} else if guess > secret_number {
println!("🔥 Too high! Go lower!");
} else {
println!("🏆 You guessed it! The star is yours!");
}
}
2.5.3. Comparison Operators in Real Life
| Operator | Meaning | Real-Life Example |
|---|---|---|
< | Less than | My age < My dad’s age |
> | Greater than | Dad’s height > My height |
== | Equal to | 2 + 2 == 4 |
!= | Not equal to | 3 != 4 |
<= | Less than or equal | Fingers on one hand <= 5 |
>= | Greater than or equal | Passing score >= 50 |
2.5.4. Why = and == Are Different
🔹 = means Assignment: “Put the right side into the left box.” (let x = 5;)
🔹 == means Comparison: “Are these two things equal?” (if x == 5)
If you accidentally use = inside an if, the compiler will complain because it’s looking for a question, not an instruction!
![[Illustration: A split-screen educational graphic. Left: a balancing scale showing “x = 5” with a loading arrow. Right: a magnifying glass examining “x == 5” with a green checkmark. Ferris stands in the middle explaining. Style: clean, modern educational illustration, bright colors.]](assets/images/2.4.png)
2.6. The Repeat Loop (loop)
2.6.1. The Problem: The Game Ends Too Soon
Right now, the program asks once and quits. We want it to keep asking until the player guesses correctly!
2.6.2. Meet loop (The Never-Ending Circle)
loop creates a circle. Whatever you put inside { } repeats forever, unless you press the escape button:
#![allow(unused)]
fn main() {
loop {
println!("This prints forever!");
}
}
(To stop it, you’d press Ctrl+C. But we’ll escape properly using break.)
2.6.3. Putting Our Guess Code Inside loop
We wrap all the “ask & check” code inside loop. Each lap, it asks for a new guess.
2.6.4. Escaping with break
To jump out of the loop, we use break. When the guess matches:
#![allow(unused)]
fn main() {
if guess == secret_number {
println!("🏆 You guessed it! The star is yours!");
break; // 🚪 Exit the loop!
}
}
2.6.5. Adding a Guess Counter
Let’s count how many tries it takes:
#![allow(unused)]
fn main() {
let mut count = 0;
loop {
count += 1; // Short for: count = count + 1
// ... rest of the code
}
}
count += 1 is the fastest way to add one!
![[Illustration: Ferris running on a circular track labeled “loop”. He holds a counter flag showing “1, 2, 3…”. At one point, a red gate labeled “break” swings open. Style: dynamic, cartoon motion lines, encouraging mood.]](assets/images/2.5.png)
2.7. Gentle Error Handling (match)
2.7.1. What If the Player Types Words?
If someone types "hello", our expect will crash the game. That’s not fun! Instead, let’s say “Please type a number!” and ask again.
2.7.2. Using match to Save the Game
Instead of expect, we’ll use match. Think of match as a sorting machine: if the conversion works, it hands back the number. If it fails, it shows a friendly message and tries again:
#![allow(unused)]
fn main() {
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("❌ Please enter only numbers! ❌");
continue;
}
};
}
- 🔹
Ok(num)→ Success! Grab the number and give it toguess. - 🔹
Err(_)→ Any error? Run the code inside this block. - 🔹
continue→ Means “Skip the rest of this lap and jump back to the top of the loop.”
2.7.3. In Simple Words
“Try to turn the text into a number. If it works, great! Keep going. If it fails, show a polite message and ask again without crashing.” Now the game never freezes! 🛡️
![[Illustration: A cartoon sorting machine labeled “match”. Left chute: text “abc” goes in, comes out as “❌ Try again!”. Right chute: text “42” goes in, comes out as a golden “42 ✅”. Ferris operates the machine with a friendly smile. Style: playful, technical metaphor, children’s book.]](assets/images/2.6.png)
2.8. Summary & Challenge
2.8.1. What You Learned
In this chapter, you discovered:
- ✅ How to generate random numbers with
rand - ✅ How to create memory boxes with
letandlet mut - ✅ How to read player input with
stdin().read_line() - ✅ How to clean and convert text using
trim()andparse() - ✅ How to make decisions with
if / else - ✅ How to repeat actions with
loopand escape withbreak - ✅ How to handle errors gracefully with
matchandcontinue
2.8.2. Big Challenge: The “Close Call” Hint
Now add a pro feature: when the player’s guess is within 5 numbers of the secret, print: "🔥 You're getting close! 🔥"
💡 Hint: Calculate the distance. You can use as i32 to convert to a signed number and .abs() for absolute value:
#![allow(unused)]
fn main() {
let diff = (guess as i32 - secret_number as i32).abs();
if diff < 5 && guess != secret_number {
println!("🔥 You're getting close! 🔥");
}
}
🎮 Final Game Code (Ready to Run)
use std::io;
use rand::Rng;
fn main() {
println!("🎲 Welcome to the Guess the Number Game! 🎲");
let secret_number = rand::thread_rng().gen_range(1..=100);
let mut count = 0;
loop {
count += 1;
println!("Please guess a number between 1 and 100: ");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read input");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("❌ Please enter only numbers! ❌");
continue;
}
};
if guess < secret_number {
println!("⬆️ Too low! Go higher!");
} else if guess > secret_number {
println!("⬇️ Too high! Go lower!");
} else {
println!("🏆 You guessed it! The star is yours! 🏆");
println!("✨ You won in {} tries! ✨", count);
break;
}
}
}
Run it with cargo run and challenge your friends!
In the next chapter, we’ll learn how to wrap repetitive code inside functions so our programs stay tidy, short, and professional. 🚀
🔚 End of Chapter 2
![[Illustration: A child sitting at a desk, smiling at a terminal screen showing a green checkmark. Ferris stands on the desk giving a thumbs up. Floating above is playful text saying “Great job!”. Style: encouraging, bright, cartoon children’s book illustration.]](assets/images/2.7.png)
🦀 The Land of Rust: Ferris the Crab’s Space Adventures
Chapter 3: The Space Chocolate Cake Recipe (Functions, Parameters & Data Types)
📋 Chapter Outline:
3.1. The Big Kitchen Problem
3.1.1. The Story: Ferris and the Space Chocolate Cake
Ferris loves space chocolate cake! 🍰 He even has his grandmother’s secret recipe: “200g flour, 150g sugar, 3 eggs, a splash of vanilla, mix well, and bake for 30 minutes.”
The problem is: every time Ferris craves cake, he has to write down and repeat all these steps from scratch. If he wants 10 cakes, he’d write the same instructions 10 times! Exhausting, right? 😮💨
In programming, the exact same thing happens. When we want to do a repetitive task, we shouldn’t rewrite the code every time. What’s the solution? Using a Function!
3.1.2. Introducing Functions as Recipes
A function is exactly like a magical recipe with a name. Whenever you call that name, it performs all the steps written inside. You can even give it ingredients (parameters) and get a finished result back (return value). 🧁✨
In Rust, we build functions with the keyword fn (short for function). Even main is a special function that the compiler knows is where the program must start.
![[Illustration: A cartoon scene inside a spaceship kitchen. Ferris looks exhausted, surrounded by floating recipe cards that repeat “Mix, Bake, Wait.” A glowing magical cookbook labeled “fn” appears, promising to save the day. Style: vibrant children’s book illustration, warm lighting, playful mood.]](assets/images/3.1.png)
3.1.3. Our First Simple Function
Let’s build a simple function that just prints a greeting. First, create a new project:
cargo new cake_functions
cd cake_functions
Now, in src/main.rs, write this code:
fn main() {
say_hello(); // Calling the function
}
// Defining our function
fn say_hello() {
println!("Hello from inside the function!");
}
🔹 fn say_hello() { ... } means: “Create a function named say_hello that takes no input and returns nothing.”
🔹 Inside main, writing say_hello(); tells the computer: “Go run the instructions in this function, then come back.”
When you run cargo run, you’ll see:
Hello from inside the function!
3.1.4. Calling Functions
The real power of a function shines when we want to repeat a task multiple times:
fn main() {
say_hello();
say_hello();
say_hello();
}
See how easy that was? Instead of writing println! three times, we just called the function’s name. This keeps our code tidy and much easier to read! 🧹
![[Illustration: An educational illustration showing a large button labeled “say_hello()” being pressed three times. Each press triggers a speech bubble saying “Hello from inside the function!”. Ferris stands beside it giving a thumbs up. Style: clean, cartoon, educational infographic, bright colors.]](assets/images/3.2.png)
3.2. Building a Magic Machine (Parameters & Return Values)
The say_hello function always does the exact same thing. But more powerful functions can take inputs and produce outputs. Think of them like a magic machine that takes raw materials and delivers a finished product! 🏭
3.2.1. Functions with Input Parameters
A parameter is like the ingredient we give to a recipe. For example, let’s make a function that takes anyone’s name and greets them:
#![allow(unused)]
fn main() {
fn greet(name: String) {
println!("Hello {}! Welcome!", name);
}
}
🔹 name: String means: “This function expects one parameter called name of type String (text).”
🔹 Inside the function body, name behaves just like a regular variable.
Now let’s call it in main:
fn main() {
greet(String::from("Ferris"));
greet(String::from("Sara"));
}
Output:
Hello Ferris! Welcome!
Hello Sara! Welcome!
3.2.2. Multiple Parameters
We can add multiple parameters by separating them with commas:
fn bake_cake(flour_grams: i32, sugar_grams: i32, eggs: i32) {
println!("Baking a cake with {}g flour, {}g sugar, and {} eggs!",
flour_grams, sugar_grams, eggs);
}
fn main() {
bake_cake(200, 150, 3);
bake_cake(300, 200, 4); // A bigger cake!
}
3.2.3. Return Values with ->
Some functions produce a result that we want to use later. For example, a function that adds two numbers and gives back the sum. We use -> followed by the return type for this:
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let sum = add(5, 3);
println!("5 + 3 = {}", sum);
}
⚠️ Golden Rust Rule: In Rust, the last expression in a function without a semicolon (;) is automatically returned. In the example above, a + b has no semicolon, so its value is sent back.
If you add a semicolon (a + b;), the compiler thinks the function returns nothing, but since you promised an i32, it will complain!
3.2.4. Early Exit with return
Sometimes we want to exit a function early, before reaching the end, and send back a specific value. We use the return keyword for this. Example: A function that returns 0 if a number is negative (since length can’t be negative):
fn safe_length(n: i32) -> i32 {
if n < 0 {
return 0; // Exit immediately, don't continue
}
n // If not negative, return the number itself
}
fn main() {
println!("Safe length: {}", safe_length(-5)); // 0
println!("Safe length: {}", safe_length(10)); // 10
}
return is like a “quick escape” button from the function! 🏃♂️💨
3.2.5. Exercise: Multiply and Combine
- Write a function called
multiplythat takes twoi32numbers and returns their product. - Call it in
mainand print the result. - Write another function called
squarethat takes one number and calculates its square usingmultiply.
💡 Sample Answer:
fn multiply(x: i32, y: i32) -> i32 {
x * y
}
fn square(x: i32) -> i32 {
multiply(x, x) // We reuse our multiply function!
}
fn main() {
let num = 7;
let sq = square(num);
println!("The square of {} is {}", num, sq);
}
![[Illustration: A friendly robot machine labeled “fn” with input hoppers for “flour”, “sugar”, “eggs” and an output conveyor belt delivering a glowing “Cake Result”. Rust syntax arrows connect inputs to outputs. Ferris watches proudly holding a slice. Style: educational cartoon, bright, technical metaphor for children.]](assets/images/3.3.png)
3.3. Flour vs. Sugar (Data Types)
In cooking, you can’t swap sugar for salt (unless you want a salty cake! 🧂). In programming, every piece of data has a specific Type. Rust is very strict: if you mix up types, the compiler will warn you right away. This strictness prevents many crashes before your program even runs! 🛡️
3.3.1. Type Categories
Data types in Rust fall into two main groups:
- 🔹 Scalar: A single value. Like one number, one letter, or a true/false value.
- 🔹 Compound: A collection of multiple values. Like tuples and arrays.
3.3.2. Integers (i32, u32)
Integers are whole numbers (like 5, -42, 0). Rust has several, but the most important are:
| Type | Sign | Approx. Range | Common Use |
|---|---|---|---|
i32 | Positive & Negative | -2B to +2B | Default for whole numbers |
u32 | Positive only | 0 to ~4B | Counting, indexes |
u8 | Positive only | 0 to 255 | Single bytes, small counters |
Example:
#![allow(unused)]
fn main() {
let temperature = -5; // Rust guesses i32 automatically
let age: u32 = 12; // We explicitly said u32
let byte: u8 = 255; // Fits exactly in one byte
}
3.3.3. Floating-Point Numbers (f64)
When we need decimal precision (like 3.14 or 2.718), we use floats:
- 🔹
f32: Lower precision, 32 bits. - 🔹
f64: Higher precision, 64 bits. Default for decimals.
#![allow(unused)]
fn main() {
let pi = 3.1415926535; // f64
let gravity: f32 = 9.81; // f32
}
3.3.4. Booleans (bool)
Can only hold two values: true or false. Very useful in conditions:
#![allow(unused)]
fn main() {
let is_raining = true;
let has_umbrella = false;
if is_raining && !has_umbrella {
println!("Oh no, we'll get wet!");
}
}
3.3.5. Characters (char)
A single letter, number, or even an emoji. In Rust, every char takes 4 bytes and can hold any Unicode character. Written with single quotes:
#![allow(unused)]
fn main() {
let first_letter = 'A';
let digit = '7';
let smiley = '😊';
let crab = '🦀'; // That's Ferris!
}
3.3.6. Tuples – Boxes Side by Side
A tuple lets us group multiple values of different types together. Its length is fixed (you can’t add or remove items later).
#![allow(unused)]
fn main() {
let ferris_info = ("Ferris", 42, true, '🦀');
}
We access members using a dot and an index (starting from 0):
#![allow(unused)]
fn main() {
println!("Name: {}", ferris_info.0); // Ferris
println!("Age: {}", ferris_info.1); // 42
println!("Happy? {}", ferris_info.2); // true
}
You can also “destructure” a tuple to put its values into separate variables:
#![allow(unused)]
fn main() {
let (name, age, is_happy, emoji) = ferris_info;
println!("{} is {} years old and loves {}", name, age, emoji);
}
3.3.7. Arrays – Organized Shelves
An array is a collection of multiple values that are all the same type and have a fixed length. Like a shelf with a set number of slots where you can only put one type of item in each.
#![allow(unused)]
fn main() {
let numbers = [10, 20, 30, 40, 50];
let first = numbers[0]; // 10
let third = numbers[2]; // 30
}
To fill an array with a repeating value:
#![allow(unused)]
fn main() {
let all_fives = [5; 10]; // Means [5, 5, 5, 5, 5, 5, 5, 5, 5, 5]
}
📌 A note for later: Arrays have a fixed size and are very fast. Later, you’ll learn about Vec (vectors), which can grow and shrink as needed.
3.3.8. Exercise: Personal Info Tuple
Create a tuple with your info: Name (String), Height in cm (f64), and whether you have a pet (bool). Then destructure it into separate variables and print a sentence.
💡 Sample Answer:
fn main() {
let my_info = (String::from("Aria"), 145.5, true);
let (name, height, has_pet) = my_info;
println!("My name is {}. I am {} cm tall.", name, height);
if has_pet {
println!("I have a pet! 🐾");
} else {
println!("I don't have a pet. 😢");
}
}
![[Illustration: A cartoon sorting robot with labeled bins: “i32”, “String”, “bool”, “char”. Different items (numbers, letters, emojis) are flying into the correct bins. Ferris stands beside holding a checklist. Style: playful, educational, bright vector illustration.]](assets/images/3.4.png)
3.4. Secret Notes (Comments)
Sometimes we want to write explanations inside our code that the computer will completely ignore, but we (or our friends) can read later to understand why we wrote it that way. These notes are called Comments. 📝
3.4.1. Single-Line Comments //
Anything after two slashes // on the same line is treated as a comment. The compiler completely ignores it:
#![allow(unused)]
fn main() {
// This is a comment
let x = 5; // This is also a comment at the end of a line
}
3.4.2. Multi-Line Comments /* */
If you need to write several lines of explanation, use /* to start and */ to end:
#![allow(unused)]
fn main() {
/*
This is a long comment.
You can write any explanation you want here.
The compiler will completely skip this section.
*/
fn do_something() { }
}
3.4.3. When Should We Comment?
-
✅ Good Comment:
-
Explains why the code is written this way (e.g., “Because library X has a bug, we must use method Y here”).
-
Documents complex parts for future reference.
-
Marks unfinished work:
// TODO: Complete this part later. -
❌ Bad Comment:
-
Explains something already obvious from the code itself.
-
Example:
x = x + 1; // Add one to x(The code already says exactly that!)
3.4.4. Exercise: Comment the Guessing Game
Open your Chapter 2 guessing game code. Add short explanatory comments for each major section (random number generation, reading input, type conversion, comparison). See how much easier the code becomes to read! 🧐
![[Illustration: Ferris wearing a detective hat, writing a secret note inside a glowing code file. A small compiler robot next to him wears sunglasses and ignores the note. Background: cozy desk with coffee and books. Style: whimsical children’s book illustration, soft lighting.]](assets/images/3.5.png)
3.5. Summary & Project
3.5.1. What You Learned
In this chapter, you discovered:
- ✅ What a function is and how to define it with
fn. - ✅ How to give parameters and get return values (
->). - ✅ The difference between
returnand the last expression without a semicolon. - ✅ Main data types: integers, floats,
bool,char. - ✅ Tuples for grouping different types.
- ✅ Arrays for storing fixed-length lists of the same type.
- ✅ Comments for documenting and making code readable.
3.5.2. Project: Simple Calculator
Write a program that takes two decimal numbers (f64) and an operator (+, -, *, /) from the user, then prints the result. Write a separate function for each operation.
💡 Structure Hint:
use std::io;
fn add(a: f64, b: f64) -> f64 { a + b }
fn subtract(a: f64, b: f64) -> f64 { a - b }
fn multiply(a: f64, b: f64) -> f64 { a * b }
fn divide(a: f64, b: f64) -> f64 { a / b }
fn main() {
// Get input from user (like in Chapter 2)
// Use if or match to detect the operator and call the correct function
// If operator is '/' and the second number is 0, print an error (division by zero is impossible!)
}
🎁 Bonus Challenge: If the user enters an invalid operator, print an error and ask again (you can use loop).
3.5.3. Challenge: Maximum Number in an Array
Write a function called max_in_array that takes an array of i32 numbers (or a slice of it) and returns the largest value inside.
💡 Hint: Create a max variable initialized with the first element. Use a loop (while) to compare the rest. (We haven’t learned for yet, so we’ll use while.)
💡 Sample Answer with while:
fn max_in_array(arr: &[i32]) -> i32 {
let mut max = arr[0];
let mut i = 1;
while i < arr.len() {
if arr[i] > max {
max = arr[i];
}
i = i + 1;
}
max
}
fn main() {
let numbers = [15, 42, 7, 99, 23];
let result = max_in_array(&numbers);
println!("Largest number: {}", result);
}
📌 Note: &[i32] means “a reference to a slice of i32 numbers”. This allows the function to look at the array’s contents without taking ownership of it. We’ll talk extensively about these “permissions” in the next chapter!
![[Illustration: Ferris standing proudly next to a computer screen showing completed code. A golden trophy labeled “Chapter 3 Master” sits on the desk. Floating code symbols (fn, i32, {}, //) surround him. Style: celebratory, vibrant children’s book illustration, encouraging mood.]](assets/images/3.6.png)
🦀 The Land of Rust: Ferris the Crab’s Space Adventures
Chapter 4: Ferris’s Borrowing Club (Learning Ownership with Toys)
📋 Chapter Outline:
4.1. The Fight Over the Red Tractor (The Move Concept)
4.1.1. The Story: The Tractor with Only One Owner
Ferris has a beautiful red toy tractor. 🚜💨 One day, his friend Bill visits and says: “What a cool tractor! Can I play with it?” Ferris, being a kind crab, says: “Sure, take it! It’s yours now!” and hands it over.
Later, when Ferris misses his tractor and wants to play with it again, he suddenly remembers he doesn’t have it anymore! Because he gave it away.
In Rust, this exact thing happens, and we call it Ownership Transfer (Move).
4.1.2. Rule #1: Every Value Has Exactly One Owner
In Rust, everything you create (a text, a list, a number, or a toy) has only one owner. We call that owner the Owner. As long as you own it, you can use it. But when the owner leaves the room (for example, when a function finishes or reaches a closing brace {}), the toy automatically gets cleaned up from the computer’s memory. This keeps the memory tidy and prevents it from filling up with digital junk! 🧹✨
4.1.3. Moving Ownership in Code
Let’s see this happen in Rust code:
fn main() {
let s1 = String::from("tractor"); // s1 is the owner
let s2 = s1; // Ownership moved from s1 to s2
// println!("{}", s1); // ❌ If you uncomment this, you'll get an error!
println!("{}", s2); // ✅ This works perfectly
}
The compiler quickly says: value moved. It means: “Hey friend, this doesn’t belong to you anymore! Ownership went to s2.”
4.1.4. Why Do Some Things Just Copy? (The Copy Trait)
You might ask: “But I used to swap numbers like that before, and I didn’t get any errors!”
You guessed exactly right. Some things, like simple numbers (i32, f64) or single characters (char), are so tiny and lightweight that Rust just makes a copy instead of moving ownership:
#![allow(unused)]
fn main() {
let x = 5;
let y = x; // x is copied here, not moved
println!("x = {}, y = {}", x, y); // Both work fine!
}
Why? Because copying a number is like remembering a fact in your head: instant and free! But a String can be huge (like a thousand-page book). Copying it would waste time and memory. So Rust prefers to just move the ownership. We call types that copy automatically Copy types.
![[Illustration: Two labeled boxes side-by-side. Box “s1” is empty with a faded cross mark. Box “s2” holds a shiny red toy tractor. A curved arrow shows ownership moving from s1 to s2. Ferris stands beside them, pointing at the boxes with a surprised but happy expression. Style: bright children’s book illustration, clean lines, educational metaphor, 16:9 aspect ratio.]](assets/images/4.1.png)
4.2. Borrowing Cards (Borrowing & References)
4.2.1. Solving the Fight: A Borrowing Card Instead of Giving Away the Toy
Ferris shouldn’t have given the tractor away permanently. He could have just handed Bill a borrowing card. With that card, Bill can look at the tractor, or even ride it, but the tractor stays in Ferris’s storage.
In Rust, we call this borrowing card a Reference. The act of lending is called Borrowing.
4.2.2. Creating a Reference with &
You create a borrowing card by putting an & symbol before a variable’s name:
#![allow(unused)]
fn main() {
let s1 = String::from("tractor");
let s2 = &s1; // s2 holds a reference to s1
println!("Owner: {}", s1); // Ferris still has his tractor
println!("Borrower: {}", s2); // Bill can also look at it
}
No error occurs here! Both can use the value because they’re just “looking at it,” not “taking ownership.”
4.2.3. Rule: Unlimited Normal Borrowing Cards Allowed
You can issue as many normal borrowing cards (&) as you want:
#![allow(unused)]
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{} {} {}", r1, r2, r3); // Everyone can look!
}
This is just like several kids gathering around a fish tank to watch the fish. As long as nobody puts their hand in the water to change things, everyone is happy! 🐠
4.2.4. Special Borrowing Card for Changes (&mut)
But what if Bill wants to paint the tractor red (modify it)? A normal card isn’t enough anymore. He needs a special golden borrowing card labeled &mut. But it comes with a strict rule:
- ⚠️ At any given moment, only one person can hold this special card.
#![allow(unused)]
fn main() {
let mut s = String::from("tractor"); // The original must be mutable too
let r1 = &mut s; // Bill gets the golden card
r1.push_str(" red"); // Paints the tractor (adds text to the string)
println!("{}", r1);
}
If two people try to grab the golden card at the same time, the guard shouts:
#![allow(unused)]
fn main() {
let mut s = String::from("tractor");
let r1 = &mut s;
let r2 = &mut s; // ❌ Error! Two mutable references at once are forbidden
}
4.2.5. Ferris’s Golden Rule (Borrowing Rules Summary)
This is the most important rule of the Borrowing Club. Write it down somewhere visible:
- 🔹 You can have many normal cards (
&) for looking only. - 🔹 You can have exactly one special card (
&mut) for looking + changing. - ❌ Mixing them is absolutely forbidden!
Example of breaking the rule:
#![allow(unused)]
fn main() {
let mut s = String::from("tractor");
let r1 = &s; // Normal card (look only)
let r2 = &mut s; // ❌ Error! You can't look and change at the same time
}
![[Illustration: A cartoon scene at a “Borrowing Club” desk. On the left, several kids hold blue cards labeled “&” while looking at a toy. On the right, one kid holds a golden card labeled “&mut” and is carefully painting the toy. Ferris stands behind the desk holding a large rulebook. Style: playful, educational, vibrant colors, clear visual metaphor, 16:9 aspect ratio.]](assets/images/4.2.png)
4.3. The Club Guard: Borrow Checker
4.3.1. Meet the Guard
Living inside the Rust compiler is a cute but serious creature called the Borrow Checker. He’s exactly like a strict but kind club guard. His job is to check all the rules we just learned. If he sees someone breaking a rule, he stops the program and tells you exactly what went wrong with friendly, colorful messages.
He’s our friend because he prevents our program from crashing, losing data, or having two people change the same thing at once! 🛡️
4.3.2. Expiration Date of Borrowing Cards (A Peek into the Future)
Every borrowing card has an expiration date. This means the card is only valid as long as the original toy exists. If the toy gets thrown away (removed from memory), the borrowing card becomes useless and dangerous. The compiler checks these expiration dates automatically.
This example causes an error because the borrowing card (r) tries to live longer than the toy (s):
#![allow(unused)]
fn main() {
let r;
{
let s = String::from("hello");
r = &s; // Borrowing card created
} // s is destroyed here (closing brace)
println!("{}", r); // ❌ Error! r points to a destroyed variable
}
(Don’t worry about the details yet. You’ll learn more about this in later chapters. Just know that the guard carefully watches the expiration dates of all cards.)
4.3.3. Exercise: Intentionally Break the Rules (and See Friendly Errors)
Now it’s your turn! Write some code that intentionally breaks the golden rule. For example:
fn main() {
let mut text = String::from("game");
let a = &text;
let b = &text;
let c = &mut text; // The guard shouts here!
println!("{} {} {}", a, b, c);
}
Run it and read the compiler’s error message carefully. See how precisely it tells you: “cannot borrow text as mutable because it is also borrowed as immutable.” That means the guard is protecting you! 🤝
![[Illustration: Ferris wearing a friendly security guard uniform, holding a flashlight and checking a rule clipboard. In the background, a cartoon compiler robot shows a red warning light next to broken code and a green checkmark next to safe code. Style: gentle technical metaphor, children’s book illustration, soft lighting, 16:9 aspect ratio.]](assets/images/4.3.png)
4.4. Puzzle Pieces (Slices)
4.4.1. Sometimes We Only Need Part of a Text
Imagine Ferris has a sign that says: Welcome to Crab Planet. He only wants to highlight the word Crab. Should he copy the whole sign? No! He can just grab a magnifying glass and point to that exact section. In Rust, we call this magnifying glass a Slice.
4.4.2. Creating a Slice with Ranges
To make a slice of text, we use the [start..end] syntax:
#![allow(unused)]
fn main() {
let s = String::from("Ferris the crab");
let ferris = &s[0..6]; // "Ferris" (indices 0 to 5)
let the_crab = &s[7..]; // "the crab" (index 7 to the end)
let all = &s[..]; // "Ferris the crab" (the whole text)
}
📌 Important Note for Non-English Text: In Rust, [..] indices work on bytes, not characters. Since characters like Persian letters or emojis take multiple bytes, slicing them might cause errors. For now, practice with English text to keep things simple!
4.4.3. Important: Slices Are Also Borrowing Cards
A slice is actually a type of reference (&str). So the guard’s rules apply to it too! As long as you hold a slice of a text, you cannot modify the original text:
#![allow(unused)]
fn main() {
let mut s = String::from("hello world");
let word = &s[0..5]; // Borrowing card pointing to "hello"
s.clear(); // ❌ Error! Can't clear the text while word is still looking at it
}
4.4.4. Exercise: Find the First Word
Write a function called first_word that takes a &str (a text reference) and returns the first word (everything before the first space). If there’s no space, return the whole text.
💡 Hint: You can use the .find(' ') method, which returns the position of the space. This method returns an Option: if it finds a space, it gives Some(position); if not, it gives None. (This is exactly like the Result from Chapter 2, but uses Some/None instead of Ok/Err.)
fn first_word(s: &str) -> &str {
match s.find(' ') {
Some(pos) => &s[..pos], // Space found: slice up to that position
None => s, // No space: return the whole text
}
}
fn main() {
let sentence = String::from("Ferris the crab");
let word = first_word(&sentence);
println!("First word: {}", word); // Output: Ferris
}
This code is safe, fast, and exactly what a professional Rustacean would write! 🛠️
![[Illustration: A close-up of a cartoon magnifying glass hovering over a long paper strip labeled “Ferris the crab”. The glass brightly highlights only the word “Ferris”. A pair of scissors (representing slicing) rests nearby. Ferris proudly holds the magnifying glass. Style: clean educational vector, bright colors, clear metaphor, 16:9 aspect ratio.]](assets/images/4.4.png)
4.5. Summary & Challenge
4.5.1. The Three Main Rules of Ownership
To keep things clear, write these three rules on a piece of paper and stick it where you can see it:
- Every value in Rust has exactly one owner.
- You can have many immutable references (
&) OR exactly one mutable reference (&mut). Mixing them is forbidden. - When the owner leaves the scope, the value is automatically cleaned up.
4.5.2. Challenge: A Function That Doesn’t Take Ownership
Write a function called calculate_length that takes a &String reference and returns its length (without taking ownership). In main, create a String, calculate its length with your function, and then print the original String again afterward.
💡 Sample Answer:
fn calculate_length(s: &String) -> usize {
s.len() // .len() returns the string's length
}
fn main() {
let my_string = String::from("Ferris the crab");
let len = calculate_length(&my_string);
println!("The length of '{}' is {} characters.", my_string, len);
// my_string is still alive because we never gave away its ownership!
}
4.5.3. Final Words: Don’t Worry, Just Practice!
This chapter might have felt a bit hard or strange at first. That’s completely normal! Ownership is Rust’s most famous and challenging concept. Even experienced programmers sometimes wrestle with the Borrow Checker.
But the good news is: the more you code, the more naturally these rules will click into place. It’s like riding a bicycle; it feels tricky at first, but soon you won’t even have to think about balancing. 🚴♂️💨
In the next chapter, we’ll explore a super fun tool: Structs, which are like “ID cards for space monsters”! 🦑✨
![[Illustration: Ferris sitting at a cozy wooden desk, carefully writing the “3 Ownership Rules” on a glowing parchment. Floating around him are small icons: a locked box (ownership), a blue card (&), a golden card (&mut), and a magnifying glass (slice). Warm, encouraging lighting. Style: children’s book illustration, whimsical, high detail, 16:9 aspect ratio.]](assets/images/4.5.png)
🦀 The Land of Rust: Ferris the Crab’s Space Adventures
Chapter 5: Monster ID Cards (Structs)
📋 Chapter Outline:
5.1. What Does My Monster Look Like?
5.1.1. The Story: Ferris’s Monster Notebook
During his space travels, Ferris has met dozens of strange and wonderful creatures. Some are giant, some are tiny and cute. Ferris decided to create a notebook to keep track of each monster: What’s its name? What color is it? How many legs does it have? How strong is it?
This information works exactly like an ID card for each creature. In programming, when we want to keep related information together and organized, we use a fantastic tool called a struct (short for structure).
5.1.2. The Problem: Too Many Separate Variables
If we tried to store a monster’s information without a struct, we’d need a bunch of separate variables:
#![allow(unused)]
fn main() {
let name = String::from("Dodo");
let color = String::from("green");
let legs = 4;
let power = 100;
}
For one monster, it’s fine. But what if we have ten monsters? We’d end up with messy names like name2, color3, and it would be impossible to pass all that information to a function as one neat package.
5.1.3. Introducing struct as an ID Card Template
A struct lets us group several pieces of data together under one name. Think of it like a blank ID card template. First, we design the template (deciding what fields it will have), and then we fill out real cards for different monsters.
![[Illustration: A magical notebook open on a wooden desk. The left page shows messy scattered variables tied together with tangled string. The right page displays a clean, glowing ID card template with labeled slots: “Name”, “Color”, “Legs”, “Power”. Ferris stands proudly pointing at the organized page. Style: vibrant children’s book illustration, educational metaphor, soft lighting, 16:9.]](assets/images/5.1.png)
5.2. The Monster Application Form (Defining a Struct)
5.2.1. Writing a Simple Struct
We start with the keyword struct, give it a name, and list its fields inside {}:
#![allow(unused)]
fn main() {
struct Monster {
name: String,
color: String,
legs: u8,
power: u32,
}
}
This code says: “I’m creating a new blueprint called Monster. Every monster made from this blueprint will have four parts: a name (String), a color (String), leg count (u8, a small positive number), and power (u32).”
5.2.2. Naming Rules: PascalCase vs snake_case
In Rust, we follow a neat naming tradition:
🔹 Struct names use PascalCase: Each word starts with a capital letter, no spaces. Examples: Monster, SpaceShip, StudentGrade.
🔹 Field names use snake_case: All lowercase, words separated by underscores. Examples: name, legs_count, fuel_amount.
5.2.3. Fields and Their Types
Each field has a name and a type. You can use any type you’ve learned so far: i32, f64, bool, char, String, or even other structs! For example, you could make a Coordinates struct and put it inside Monster:
#![allow(unused)]
fn main() {
struct Coordinates {
x: f64,
y: f64,
}
struct Monster {
name: String,
position: Coordinates,
// other fields...
}
}
5.2.4. Exercise: Define a Spaceship Struct
Create a struct named SpaceShip with three fields:
fuelof typeu32(remaining fuel)passenger_countof typeu8(number of passengers)modelof typeString(spaceship model)
💡 Sample Answer:
#![allow(unused)]
fn main() {
struct SpaceShip {
fuel: u32,
passenger_count: u8,
model: String,
}
}
![[Illustration: An educational infographic showing a Rust struct definition as a blueprint sheet. Fields are highlighted with colorful tags matching their data types (String=blue, u8=green, u32=orange). Ferris holds a ruler and pencil, drawing the blueprint. Style: clean, modern educational cartoon, bright colors, 16:9.]](assets/images/5.2.png)
5.3. Introducing a New Monster (Creating Instances)
5.3.1. Making a Real Monster (Instance)
Now that we have the Monster blueprint, let’s create an actual monster. We write the struct name, open {}, and give each field a value:
#![allow(unused)]
fn main() {
let monster1 = Monster {
name: String::from("Dodo"),
color: String::from("green"),
legs: 4,
power: 100,
};
}
Now monster1 is a real monster holding Dodo’s information!
5.3.2. Accessing Fields with a Dot (.)
To read a field, we use the dot operator, just like visiting a specific room in a house:
#![allow(unused)]
fn main() {
println!("Monster name: {}", monster1.name);
println!("Power level: {}", monster1.power);
}
5.3.3. Changing Fields (With mut)
If we want to change a field later (like training the monster to get stronger), we must create the instance with mut:
#![allow(unused)]
fn main() {
let mut monster2 = Monster {
name: String::from("Bombi"),
color: String::from("red"),
legs: 2,
power: 50,
};
monster2.power = 75; // Now Bombi's power is 75!
}
5.3.4. Shortcut: Field Init Shorthand
If you already have variables with the exact same names as the struct fields, Rust lets you skip writing name: name:
#![allow(unused)]
fn main() {
let name = String::from("Dodo");
let color = String::from("green");
let legs = 4;
let power = 100;
let monster = Monster {
name, // means name: name
color, // means color: color
legs, // means legs: legs
power, // means power: power
};
}
This keeps your code short and tidy! 🧹✨
5.3.5. Building a New Monster from an Old One (.. Syntax)
Sometimes you want to create a monster that’s almost identical to another, but with one difference. Instead of rewriting all fields, use ..:
#![allow(unused)]
fn main() {
let monster2 = Monster {
name: String::from("Bombi"),
..monster1 // copy the rest from monster1
};
}
⚠️ Important Note: This moves ownership for String fields! Because String doesn’t implement Copy, monster1.name now belongs to monster2. You can’t use monster1.name after this line. But numbers like legs and power are Copy, so they get copied safely.
Remember: .. is convenient, but always keep Chapter 4’s ownership rules in mind!
![[Illustration: Split scene: Left shows a crab holding a “monster blueprint” copying data to a new card using a “..” stamp. Right shows a friendly warning sign: “String fields move ownership!”. Ferris explains with a helpful gesture. Style: playful technical metaphor, children’s book illustration, clear visual cues, 16:9.]](assets/images/5.3.png)
5.4. Things Monsters Can Do (Methods)
5.4.1. Function vs Method
A function is a standalone block of code (like add from Chapter 3).
A method is a function that belongs to a specific struct. Methods are called with a dot (.) on an instance, and their first parameter always points to the instance itself (usually &self). Think of it as saying “Monster, roar!” instead of “Roar at the monster!”
5.4.2. Writing Your First Method with impl
To attach methods to a struct, we use an impl (implementation) block:
#![allow(unused)]
fn main() {
impl Monster {
fn roar(&self) {
println!("{} roooooaaar!", self.name);
}
}
}
Now we can make any monster roar:
#![allow(unused)]
fn main() {
let dodo = Monster { /* ... */ };
dodo.roar(); // Prints: "Dodo roooooaaar!"
}
5.4.3. The &self Parameter
&self is an immutable reference to the current instance. It means the method can read fields but not change them. We have three main options:
🔹 &self : Read-only (most common)
🔹 &mut self : Read + modify (requires mut instance)
🔹 self : Take ownership (destroys the instance after use – rare)
5.4.4. Methods with Extra Parameters
Methods can take extra parameters besides &self:
#![allow(unused)]
fn main() {
impl Monster {
fn attack(&self, target: &str) {
println!("{} attacked {} with {} power!", self.name, target, self.power);
}
}
}
Usage: dodo.attack("Bill");
5.4.5. Methods with Return Values
Methods can return values just like regular functions:
#![allow(unused)]
fn main() {
impl Monster {
fn power_level(&self) -> u32 {
self.power
}
}
}
5.4.6. Modifying Methods (&mut self)
If a method needs to change the instance’s data, it must use &mut self, and the instance must be mut:
#![allow(unused)]
fn main() {
impl Monster {
fn heal(&mut self, amount: u32) {
self.power += amount;
println!("{} powered up to {}!", self.name, self.power);
}
}
}
Usage: bombi.heal(20);
5.4.7. Associated Functions (Like new)
Sometimes we want a function related to the struct that doesn’t need an instance. The most famous example is new, which builds a fresh monster. We call these with :: (double colon):
#![allow(unused)]
fn main() {
impl Monster {
fn new(name: String, color: String, legs: u8, power: u32) -> Monster {
Monster { name, color, legs, power }
}
}
}
Usage:
#![allow(unused)]
fn main() {
let dodo = Monster::new(
String::from("Dodo"),
String::from("green"),
4,
100,
);
}
This feels like a “monster factory”! 🏭
![[Illustration: Cartoon scene showing a “Method Factory” conveyor belt. Crabs input raw monster parts, a machine labeled “impl Monster” adds behaviors (roar, attack, heal) via &self/&mut self tags, and finished monsters roll out with speech bubbles. Ferris operates a control panel. Style: dynamic, educational, bright colors, 16:9.]](assets/images/5.4.png)
5.5. Project: The Monster Roster
Now let’s build a small program that stores a list of monsters and performs actions on them.
5.5.1. Creating a Vec of Monsters
First, we’ll use a Vec (vector) to store monsters. A Vec is like a magical backpack that automatically grows when you add items. (We’ll explore Vec deeply in Chapter 8, but for now, just know we add items with .push().)
#![allow(unused)]
fn main() {
let mut monster_list = Vec::new();
monster_list.push(Monster::new(
String::from("Dodo"), String::from("green"), 4, 100,
));
monster_list.push(Monster::new(
String::from("Bombi"), String::from("red"), 2, 75,
));
monster_list.push(Monster::new(
String::from("Zarzar"), String::from("yellow"), 6, 120,
));
}
5.5.2. Looping to Make Everyone Roar
We want every monster in the list to roar. Rust has a simple loop called for that automatically visits each item one by one:
#![allow(unused)]
fn main() {
for monster in &monster_list {
monster.roar();
}
}
💡 Why &monster_list? The & means we’re borrowing the list, not taking ownership. Just like the borrowing cards from Chapter 4!
5.5.3. Finding the Strongest Monster
Let’s write a function that returns the monster with the highest power:
#![allow(unused)]
fn main() {
fn strongest(monsters: &Vec<Monster>) -> &Monster {
let mut strongest = &monsters[0];
for monster in monsters {
if monster.power > strongest.power {
strongest = monster;
}
}
strongest
}
}
Usage in main:
#![allow(unused)]
fn main() {
let champ = strongest(&monster_list);
println!("Strongest monster: {} with power {}", champ.name, champ.power);
}
5.5.4. Exercise: is_stronger_than Method
Add a method to Monster called is_stronger_than that takes another &Monster and returns true if the current monster is stronger.
💡 Sample Answer:
#![allow(unused)]
fn main() {
impl Monster {
fn is_stronger_than(&self, other: &Monster) -> bool {
self.power > other.power
}
}
}
Usage:
#![allow(unused)]
fn main() {
if dodo.is_stronger_than(&bombi) {
println!("Dodo is stronger than Bombi!");
}
}
![[Illustration: A cozy notebook open to a “Monster Roster” page. Each entry shows a mini ID card with a cartoon monster sketch and stats. A golden trophy icon highlights the strongest monster. Ferris sits on the desk stamping “Approved” with a smile. Style: warm, inviting children’s book illustration, detailed UI metaphor, 16:9.]](assets/images/5.5.png)
5.6. Summary & Challenge
5.6.1. What You Learned
In this chapter, you discovered:
✅ struct is a template for grouping related data.
✅ Instances are created with let x = StructName { fields };
✅ Access fields with the dot operator (.).
✅ Methods are defined in impl blocks and use self, &self, or &mut self.
✅ Associated functions (like new) are called with ::.
✅ The .. syntax copies remaining fields (watch out for ownership moves!).
✅ The for loop is a simple way to visit each item in a list.
5.6.2. Challenge: Student Struct & Grades
Create a struct named Student with name: String and grade: f64. Write a method passed(&self) -> bool that returns true if grade >= 10.0. Then, create a Vec of students and print only those who passed.
💡 Sample Answer:
struct Student {
name: String,
grade: f64,
}
impl Student {
fn new(name: String, grade: f64) -> Student {
Student { name, grade }
}
fn passed(&self) -> bool {
self.grade >= 10.0
}
}
fn main() {
let students = vec![
Student::new(String::from("Sara"), 18.5),
Student::new(String::from("Reza"), 9.0),
Student::new(String::from("Maryam"), 14.0),
];
for student in &students {
if student.passed() {
println!("{} passed with grade {}", student.name, student.grade);
} else {
println!("{} needs more practice.", student.name);
}
}
}
Now you know how to organize related data into structs and give them behaviors with methods! In the next chapter, we’ll explore enum and learn how to handle different states (like weather, game modes, or traffic lights) in a clean, safe way. 🌈✨
![[Illustration: Ferris wearing a graduation cap, holding a glowing “Chapter 5 Complete” badge. Floating around him are mini structs, impl blocks, and &self tags turning into a neat organized library. Encouraging, bright lighting, children’s book style.]](assets/images/5.6.png)
🦀 The Land of Rust: Ferris the Crab’s Space Adventures
Chapter 6: The Smart Wardrobe Machine (Enums, Option & Match)
📋 Chapter Outline:
6.1. How’s the Weather Today? (enum)
6.1.1. The Story: Ferris’s Smart Wardrobe
Ferris has a magical wardrobe that tells him what to wear based on the weather. But weather isn’t just one thing: it can be sunny, rainy, snowy, or cloudy. Ferris wants to store these four states in his program. Instead of using plain numbers or text (which are easy to mistype or mess up), Rust has a fantastic tool called an enum (short for enumeration).
6.1.2. The Problem: Numbers or Text?
Imagine we used numbers:
#![allow(unused)]
fn main() {
let weather = 1; // 1 = sunny, 2 = rainy, ...
}
But what if we accidentally type 5? The program won’t know what to do! What about text?
#![allow(unused)]
fn main() {
let weather = "sunny";
}
If we mistype "suny" (missing an n), the program gets confused! An enum solves this perfectly. It only lets you pick from a fixed list. Nothing else is allowed. 🛡️
6.1.3. Defining an enum with Different States
We use the enum keyword to create a new type that can only be one of several specific values:
#![allow(unused)]
fn main() {
enum Weather {
Sunny,
Rainy,
Snowy,
Cloudy,
}
}
Now Weather is a brand new data type, just like i32 or String. But it only has four valid values.
6.1.4. Creating Values from an enum
To create a value from this enum, we write the enum name, then two colons (::), and finally the variant we want:
#![allow(unused)]
fn main() {
let today = Weather::Sunny;
let tomorrow = Weather::Rainy;
}
The :: means: “From inside this enum, pick this specific option.”
![[Illustration: Ferris the crab stands in front of a magical weather map. Four glowing icons float above: a sun, a rain cloud, a snowflake, and a gray cloud. A friendly selector tool connects them to a dropdown menu. Style: vibrant children’s book illustration, clear educational metaphor, soft lighting, 16:9.]](assets/images/6.1.png)
6.2. A Wardrobe Full of Clothes (Enum with Data)
6.2.1. Not All Sunny Days Are the Same
Some sunny days are 20°C, others are 35°C. On rainy days, we might want to know the umbrella color. In Rust, each enum variant can hold its own extra data!
6.2.2. Defining an enum with Data
#![allow(unused)]
fn main() {
enum WeatherInfo {
Sunny { temperature: u8 },
Rainy { umbrella_color: String },
Snowy { scarf_material: String },
Cloudy,
}
}
🔹 Sunny holds a temperature field (u8).
🔹 Rainy holds an umbrella_color field (String).
🔹 Snowy holds a scarf_material field (String).
🔹 Cloudy needs no extra data.
6.2.3. Creating Instances of an enum with Data
#![allow(unused)]
fn main() {
let sunny_day = WeatherInfo::Sunny { temperature: 32 };
let rainy_day = WeatherInfo::Rainy { umbrella_color: String::from("red") };
let snowy_day = WeatherInfo::Snowy { scarf_material: String::from("wool") };
let cloudy_day = WeatherInfo::Cloudy;
}
6.2.4. enum vs struct
🔹 struct: All fields are always present. A Monster always has a name, color, legs, and power.
🔹 enum: Only one variant is active at a time. A WeatherInfo is either Sunny, Rainy, Snowy, or Cloudy. It can’t have a temperature AND an umbrella color simultaneously!
![[Illustration: Split educational graphic. Left: a “struct” box showing all four slots filled at once. Right: an “enum” wardrobe with four drawers, but only ONE drawer is open at a time. Ferris points to the open drawer explaining the difference. Style: clean, cartoon, bright colors, clear visual metaphor, 16:9.]](assets/images/6.2.png)
6.3. The Lost Bag (Option)
6.3.1. The Story: Searching for the Spaceship Key
Ferris always keeps his spaceship key in his backpack. But some days, he forgets, and the bag is empty! In programming, we call this “something might exist, or it might not.” Many languages use null for this, but null is dangerous and causes programs to crash. Rust has no null. Instead, it uses Option.
6.3.2. Introducing Option<T>
Option is a super useful enum built right into Rust:
#![allow(unused)]
fn main() {
enum Option<T> {
Some(T),
None,
}
}
<T> means “this can hold any type you want.” Some(T) means “we have a value of type T,” and None means “there’s nothing here.”
6.3.3. Using Some and None
#![allow(unused)]
fn main() {
let key = Some(String::from("golden key")); // The key exists
let no_key: Option<String> = None; // No key
}
If you just write None, you usually need to tell the compiler what type of Option it is (like Option<String>).
6.3.4. Why Option is Safer Than null
In languages with null, if you forget to check whether a value is null before using it, your program might suddenly crash. In Rust, the compiler forces you to handle both Some and None. This makes your programs incredibly safe! 🛡️
6.3.5. Useful Methods on Option
🔹 .unwrap() → Gives you the value if it’s Some. Crashes if it’s None. (Only use when you’re 100% sure it’s there!)
🔹 .unwrap_or(default) → Returns a fallback value if it’s None.
🔹 .is_some() / .is_none() → Checks which state it’s in.
#![allow(unused)]
fn main() {
let key = Some(String::from("abc"));
let value = key.unwrap_or(String::from("Key not found"));
println!("{}", value); // Prints: abc
}
6.3.6. Exercise: safe_divide Function
Write a function called safe_divide that takes two f64 numbers. If the second number isn’t zero, return the result inside Some. Otherwise, return None.
💡 Sample Answer:
fn safe_divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 {
None
} else {
Some(a / b)
}
}
fn main() {
let result = safe_divide(10.0, 2.0);
match result {
Some(r) => println!("Result: {}", r),
None => println!("Error: Cannot divide by zero!"),
}
}
![[Illustration: Ferris opening two floating treasure chests. One chest contains a glowing key labeled “Some(key)”. The other chest is empty with a gentle “None” tag. A friendly compiler robot holds a checklist saying “Always check both!”. Style: playful, educational, warm lighting, children’s book, 16:9.]](assets/images/6.3.png)
6.4. The Smart Remote Control (match)
6.4.1. The Problem: How to Decide Based on an enum?
Now that we know the weather is Sunny or Rainy, how do we tell the user what to wear? We could use many if statements, but Rust has a much cleaner tool: match.
6.4.2. Introducing match
match is like a smart sorting machine: it takes a value, compares it against several patterns, and runs the code for the first pattern that fits.
#![allow(unused)]
fn main() {
let weather = Weather::Sunny;
match weather {
Weather::Sunny => println!("Wear a sun hat!"),
Weather::Rainy => println!("Take an umbrella!"),
Weather::Snowy => println!("Wrap up in a scarf!"),
Weather::Cloudy => println!("Perfect weather, wear what you like!"),
}
}
6.4.3. The Exhaustive Rule
With match, you must cover every possible variant. If you forget even one, the compiler will complain: “You didn’t handle the Snowy case!” This is a great feature because forgotten cases never turn into hidden bugs! ✅
6.4.4. Extracting Data from an enum with match
If our enum holds extra data, we can pull it out right inside match:
#![allow(unused)]
fn main() {
let info = WeatherInfo::Sunny { temperature: 35 };
match info {
WeatherInfo::Sunny { temperature } => {
println!("Sunny at {} degrees. Don't forget sunscreen!", temperature);
}
WeatherInfo::Rainy { umbrella_color } => {
println!("It's raining. Grab the {} umbrella!", umbrella_color);
}
// ... other variants
}
}
6.4.5. The Catch-All Pattern with _
Sometimes we only care about one or two variants and want to group everything else. We use _ (underscore) to mean “anything else”.
#![allow(unused)]
fn main() {
match weather {
Weather::Sunny => println!("Let's go to the park!"),
_ => println!("Better stay indoors today."),
}
}
6.4.6. match with Option
match works beautifully with Option:
#![allow(unused)]
fn main() {
let key = Some(String::from("golden key"));
match key {
Some(k) => println!("Found the key: {}", k),
None => println!("Key is missing. Let's search!"),
}
}
6.4.7. if let for Simpler Cases
If we only care about one specific variant and want to ignore the rest, if let is a shorter alternative:
#![allow(unused)]
fn main() {
let weather = Weather::Sunny;
if let Weather::Sunny = weather {
println!("What a beautiful sunny day!");
} else {
println!("Not sunny today.");
}
}
6.4.8. Exercise: Coin Values
Create a Coin enum with Penny, Nickel, Dime, and Quarter. Write a function that takes a coin and returns its value in cents (1, 5, 10, 25).
💡 Sample Answer:
#![allow(unused)]
fn main() {
enum Coin { Penny, Nickel, Dime, Quarter }
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
}
![[Illustration: A cartoon remote control with four glowing buttons labeled with enum variants (Sunny, Rainy, Snowy, Cloudy). A hand presses “Rainy” and a speech bubble pops up saying “Take an umbrella!”. Ferris watches from the side holding a checklist. Style: dynamic, educational vector illustration, bright colors, 16:9.]](assets/images/6.4.png)
6.5. Project: The Smart Shirt Recommender
Let’s build a small program that asks the user for the weather and suggests a shirt color!
6.5.1. Define the Weather enum
#![allow(unused)]
fn main() {
enum Weather {
Sunny,
Rainy,
Snowy,
Cloudy,
}
}
6.5.2. The Shirt Recommendation Function
#![allow(unused)]
fn main() {
fn recommend_shirt(weather: Weather) -> &'static str {
match weather {
Weather::Sunny => "white (cool and bright)",
Weather::Rainy => "blue (matches the rainy sky)",
Weather::Snowy => "red (warm and cheerful)",
Weather::Cloudy => "gray (perfect for cloudy days)",
}
}
}
💡 Note on &'static str: This is a special type of text that lives for the entire lifetime of the program. It’s perfect for fixed, unchanging messages like these!
6.5.3. Getting User Input
use std::io;
fn main() {
println!("Choose the weather:");
println!("1: Sunny | 2: Rainy | 3: Snowy | 4: Cloudy");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Failed to read input");
let choice: u32 = input.trim().parse().expect("Please enter a valid number!");
let weather = match choice {
1 => Weather::Sunny,
2 => Weather::Rainy,
3 => Weather::Snowy,
4 => Weather::Cloudy,
_ => {
println!("Invalid number. Defaulting to Sunny.");
Weather::Sunny
}
};
let shirt = recommend_shirt(weather);
println!("My recommendation: wear a {} shirt!", shirt);
}
6.5.4. Adding a Preferred Color with Option
What if the user has a favorite color? If they provide one, use it. Otherwise, stick with our recommendation:
#![allow(unused)]
fn main() {
let preferred_color: Option<String> = None; // We could ask the user later
let final_color = match preferred_color {
Some(color) => color,
None => recommend_shirt(weather).to_string(),
};
println!("Final color choice: {}", final_color);
}
![[Illustration: A friendly cartoon robot display panel showing a weather input screen. A dropdown enum selector points to a suggested shirt popping out of a slot. Ferris stands beside holding a rainbow-colored shirt. Style: cozy tech, children’s book illustration, bright and inviting, 16:9.]](assets/images/6.5.png)
6.6. Summary & Challenge
6.6.1. What You Learned
In this chapter, you discovered:
✅ enum is a type that can only be one of several predefined variants.
✅ Each variant can hold its own unique data.
✅ Option<T> is Rust’s safe way to say “we have something (Some) or nothing (None).”
✅ match is a powerful tool to make decisions based on enum values and extract inner data.
✅ if let is a handy shortcut when you only care about one case.
6.6.2. Challenge: Calculator with Result
Instead of Option, let’s peek at another famous enum: Result. It’s perfect for error handling:
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
Write a divide function that takes two f64 numbers. If the second isn’t zero, return Ok(result). Otherwise, return Err("Cannot divide by zero"). Handle it in main using match.
💡 Sample Answer:
fn divide(a: f64, b: f64) -> Result<f64, &'static str> {
if b == 0.0 {
Err("Division by zero is impossible!")
} else {
Ok(a / b)
}
}
fn main() {
let result = divide(10.0, 2.0);
match result {
Ok(r) => println!("Result: {}", r),
Err(e) => println!("Error: {}", e),
}
}
Now you know how to manage different states beautifully with enum and match. In the next chapter, we’ll learn how to organize our code into files and modules, turning our program into a tidy, professional library! 📚✨
![[Illustration: Ferris wearing a graduation cap, holding a glowing “Chapter 6 Complete” badge. Floating around him are colorful enum tags, match arms, Option/Result boxes, and a small weather remote. Encouraging, bright lighting, children’s book style, 16:9.]](assets/images/6.6.png)
🦀 The Land of Rust: Ferris the Crab’s Space Adventures
Chapter 7: The Big City Library (Modules & Files)
📋 Chapter Outline:
7.1. The Library Got Too Messy!
7.1.1. The Story: Ferris’s Disorganized Library
Ferris loves reading. He has a huge library aboard his spaceship. At first, he just piled all his books onto one big table. 📚📖
But soon, the number of books grew, and the library started looking like a junkyard! Every time he wanted to find “The Galaxy Encyclopedia,” he had to dig through piles of papers for hours.
Finally, Ferris decided to organize it. He built separate shelves: one for science books, one for stories, one for star maps, and one for space cookbooks! Now everything has its place, and finding a book is as easy as pie! 💧
7.1.2. The Problem: Everything in One File
In programming, the exact same thing happens. Until now, we’ve been writing all our code in a single file called main.rs. For our Guess-the-Number game (which was about 50 lines), that was perfectly fine.
But imagine you’re writing a big game like Minecraft! Thousands of lines of code, hundreds of functions, and countless variables! If you put them all in one file, finding a single line becomes like finding a needle in a haystack! 😵💫
On top of that, if multiple people work together, they’d all be editing the same file, which quickly causes conflicts and confusion.
7.1.3. Introducing Modules as Shelves
In Rust, the solution is to use Modules. A module is exactly like a separate bookshelf. Each shelf can hold its own special code.
Modules do three amazing things for us: 🔹 Organization: Groups code into neat, logical categories. 🔹 Privacy: Lets us hide certain code so no one from the outside can accidentally change it. 🔹 No Conflicts: Two different modules can have functions with the exact same name without mixing them up!
![[Illustration: Educational illustration of Ferris the crab standing in a messy spaceship room full of scattered books. On the right side, a clean, organized bookshelf with labeled shelves (Science, Stories, Maps) is shown. Ferris is happily placing a book on the correct shelf. Style: vibrant children’s book, clean lines, soft lighting, 16:9.]](assets/images/7.1.png)
7.2. Building Shelves (mod)
7.2.1. Defining a Module in a File
The easiest way to create a shelf (module) is using the mod keyword. We can even define it directly inside main.rs:
mod animals {
pub fn bark() {
println!("Woof!");
}
pub struct Dog {
pub name: String,
}
}
fn main() {
// Using a function inside the module
animals::bark();
// Creating a dog
let my_dog = animals::Dog {
name: String::from("Rexy"),
};
println!("My dog's name: {}", my_dog.name);
}
Notice the word pub (short for public). Anything we want to use from outside the module must be marked pub. Otherwise, it stays private and can only be used inside that module.
7.2.2. Calling Functions from a Module
To access something inside a module, we use the :: symbol (double colon).
For example, animals::bark() means: “Go to the animals shelf and find the bark function, then run it!” 🐕
7.2.3. Nested Modules
We can put modules inside other modules, just like stacking boxes inside bigger boxes:
mod house {
pub mod kitchen {
pub fn cook() {
println!("Cooking in the kitchen!");
}
}
pub mod living_room {
pub fn watch_tv() {
println!("Watching TV in the living room!");
}
}
}
fn main() {
house::kitchen::cook();
house::living_room::watch_tv();
}
7.2.4. Moving Modules to Separate Files
When modules grow, they no longer fit nicely inside main.rs. It’s much better to give each module its own file!
Here’s how:
- Create a new file next to
main.rs(inside thesrcfolder) with the exact same name as the module and a.rsextension. - Write the module’s content inside it (without the
modkeyword). - In
main.rs, simply write:mod animals;
📂 File src/animals.rs:
#![allow(unused)]
fn main() {
pub fn bark() {
println!("Woof!");
}
pub struct Dog {
pub name: String,
}
}
📂 File src/main.rs:
mod animals; // This tells Rust to find and read animals.rs!
fn main() {
animals::bark();
}
Rust is smart enough to automatically find animals.rs when you write mod animals;. So clean and tidy! 🧹✨
7.2.5. Exercise: Math Modules
Create a new project (cargo new math_modules). Create two modules in separate files: math and strings.
mathmodule:addandsubtractfunctions.stringsmodule:to_uppercaseandto_lowercasefunctions. Then use all of them inmain.rs.
![[Illustration: A cartoon carpenter Ferris building wooden shelves labeled “mod” and “pub”. Each shelf holds different code blocks (functions, structs). Tools like a hammer and ruler are scattered around. Style: educational, playful, bright colors, children’s book, 16:9.]](assets/images/7.2.png)
7.3. The Library’s Lock (Privacy)
7.3.1. Private by Default
In Rust, we have a golden rule: Everything is private by default! 🔒 Think of it like a diary. Would you let just anyone open it and read your secrets? No! You decide exactly which pages to share.
7.3.2. Making Things Public with pub
To let other parts of your program see or use something, you must attach the pub label to it:
#![allow(unused)]
fn main() {
mod my_module {
pub fn public_function() {
println!("Everyone can see me!");
}
fn private_function() {
println!("Only this module can see me.");
}
}
}
7.3.3. Private Fields in Structs
Even if a struct itself is public, its inner fields (like name or power) are private by default!
This is actually a wonderful thing because it lets us control how information changes. For example, in a game, players shouldn’t be able to directly change their hero’s health to 999. Instead, they must use a heal() function that checks if they actually drank a potion first.
7.3.4. Why Privacy Matters
🔹 Safety: Prevents accidental or unauthorized changes. 🔹 Simplicity: Users only interact with what they need, avoiding confusion. 🔹 Flexibility: You can completely change how the private code works inside, without breaking anyone else’s program (as long as the public names stay the same).
7.3.5. Exercise: The Bank Module
Create a bank module that manages a bank account. It should have an Account struct where the balance field is private.
Write public methods: new (starting balance), deposit, and withdraw (checks if there’s enough money before subtracting).
💡 Sample Answer:
#![allow(unused)]
fn main() {
pub struct Account {
balance: u32,
}
impl Account {
pub fn new(initial_balance: u32) -> Account {
Account { balance: initial_balance }
}
pub fn deposit(&mut self, amount: u32) {
self.balance += amount;
}
pub fn withdraw(&mut self, amount: u32) -> bool {
if amount <= self.balance {
self.balance -= amount;
true
} else {
false
}
}
pub fn get_balance(&self) -> u32 {
self.balance
}
}
}
![[Illustration: Illustration of Ferris the crab standing in front of a bank vault door. One door is open with a bright “pub” sign, revealing shiny gold coins. The other door is closed with a heavy lock and a “private” sign. Style: educational cartoon, bright colors, clear metaphor, 16:9.]](assets/images/7.3.png)
7.4. Library Shortcuts (use & as)
Sometimes writing the full address (house::kitchen::cook()) gets very long!
With the use keyword, we can bring a function right to our desk, exactly like taking a book off the shelf and placing it in front of us.
7.4.1. Bringing Functions with use
mod animals {
pub mod dog {
pub fn bark() {
println!("Woof!");
}
}
}
use crate::animals::dog::bark;
fn main() {
bark(); // No need to write the long address anymore!
}
(💡 Quick note: crate simply means “from the root of our current project.”)
7.4.2. Renaming with as
What if we have two functions with the exact same name from different modules? They would clash!
To solve this, we use as to give them nicknames:
#![allow(unused)]
fn main() {
use animals::dog::bark as dog_bark;
use animals::cat::speak as cat_speak;
}
💡 Tip: as is super handy when you want shorter or clearer names for your imports.
For now, these two uses of use are all you need. As you grow as a programmer, you’ll learn more advanced ways, but for now, just remember: use keeps your code clean and easy to read!
![[Illustration: A cartoon map showing a path from “crate” root to a function. A magnifying glass focuses on a shortcut sign labeled “use”. Ferris is walking a shorter path thanks to the shortcut. Style: playful vector illustration, clear educational graphic, 16:9.]](assets/images/7.4.png)
7.5. Project: Rebuilding Guess-the-Number with Modules
Now let’s rebuild our Guess-the-Number game (from Chapter 2) using modules, and see how much cleaner and more professional it becomes!
7.5.1. Project Structure
First, let’s organize our files inside the src folder:
src/
├── main.rs
├── input.rs // Handles getting input from the user
├── random.rs // Handles generating the random number
└── game_logic.rs // Handles comparison and game rules
(Don’t forget to add rand = "0.8.5" to your Cargo.toml!)
7.5.2. Module input (src/input.rs)
This file’s only job is to get a clean number from the user. If they type something wrong, it politely asks again.
#![allow(unused)]
fn main() {
use std::io;
pub fn read_number() -> u32 {
loop {
println!("Please guess a number:");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Failed to read");
match input.trim().parse() {
Ok(num) => return num,
Err(_) => println!("Only numbers, please!"),
}
}
}
}
7.5.3. Module random (src/random.rs)
We generate the secret number here.
#![allow(unused)]
fn main() {
use rand::Rng;
pub fn generate_secret() -> u32 {
rand::thread_rng().gen_range(1..=100)
}
}
7.5.4. Module game_logic (src/game_logic.rs)
Here we handle comparisons. We’ll also create an enum to hold the game’s state.
#![allow(unused)]
fn main() {
pub enum GuessResult {
TooLow,
TooHigh,
Correct,
}
pub fn check_guess(guess: u32, secret: u32) -> GuessResult {
if guess < secret { GuessResult::TooLow }
else if guess > secret { GuessResult::TooHigh }
else { GuessResult::Correct }
}
pub fn get_message(result: &GuessResult) -> &'static str {
match result {
GuessResult::TooLow => "⬆️ Too low! Go higher!",
GuessResult::TooHigh => "⬇️ Too high! Go lower!",
GuessResult::Correct => "🏆 You got it!",
}
}
}
(💡 Note: &'static str is Rust’s way of saying “a fixed text that lives for the entire program”. Perfect for short, unchanging messages like these!)
7.5.5. File main.rs
Look how clean and readable main.rs is now!
mod input;
mod random;
mod game_logic;
use game_logic::GuessResult;
fn main() {
println!("🎲 Welcome to Guess the Number! 🎲");
let secret = random::generate_secret();
let mut count = 0;
loop {
let guess = input::read_number();
count += 1;
let result = game_logic::check_guess(guess, secret);
println!("{}", game_logic::get_message(&result));
if let GuessResult::Correct = result {
println!("✨ You won in {} tries! ✨", count);
break;
}
}
}
No more scrolling through 100 lines of mixed code! Each file does exactly one job. 😎
![[Illustration: A cartoon folder tree structure showing src/ folder with four glowing files: main.rs, input.rs, random.rs, game_logic.rs. Arrows connect them showing how modules interact. Ferris stands beside giving a thumbs up. Style: clean infographic, educational, bright colors, 16:9.]](assets/images/7.5.png)
7.6. Summary & Challenge
7.6.1. What You Learned
In this chapter, you discovered:
✅ Modules (mod): Divide code into small, organized sections.
✅ pub: Makes functions or variables visible outside their module (otherwise, they stay private).
✅ Files: Modules can live in separate .rs files.
✅ use: Shortens long paths to items.
✅ as: Renames imports to avoid name clashes.
7.6.2. Challenge: shapes Crate
Create a new project named shapes.
Make two modules in separate files: circle and rectangle.
Each should have two functions: area and perimeter.
- Circle: radius
r. (Area: π × r², Perimeter: 2 × π × r) - Rectangle: length
aand widthb. (Area: a × b, Perimeter: 2 × (a + b))
In main.rs, call these functions and print the results for a circle with radius 5 and a rectangle of 4×7.
💡 Sample Answer (circle.rs):
#![allow(unused)]
fn main() {
pub fn area(radius: f64) -> f64 {
std::f64::consts::PI * radius * radius
}
pub fn perimeter(radius: f64) -> f64 {
2.0 * std::f64::consts::PI * radius
}
}
Now you’re a real programmer who knows how to manage large projects! 🏗️ In the next chapter, we’ll dive into Collections (Vectors and Hash Maps): magical boxes that can grow, shrink, and hold tons of data! 📦✨
![[Illustration: Ferris the crab wearing a graduation cap, holding a glowing badge “Chapter 7 Master”. Background shows a well-organized file cabinet with labels “mod”, “pub”, “use”. Floating icons of Rust files and folders surround him. Style: encouraging, vibrant children’s book, 16:9.]](assets/images/7.6.png)
🦀 The Land of Rust: Ferris the Crab’s Space Adventures
Chapter 8: Magic Boxes That Grow and Shrink (Collections)
📋 Chapter Outline:
8.1. Mom’s Shopping List (Vector)
8.1.1. The Story: Ferris’s Shopping Trip
Before going to the space market, Ferris’s mom always grabs a piece of paper and writes down what they need: "Milk, Bread, Eggs, Apple". 🛒 As they walk through the aisles, she might suddenly remember: “Oh! We need cheese too!” and quickly adds it to the bottom of the list. When they pick something up, she crosses it off.
In programming, we often need exactly this kind of smart list: one that can grow and shrink as we go. Remember arrays ([]) from Chapter 3? Their size is fixed once created. But a Vector is exactly our magical shopping list! 🦀✨
![[Illustration: A cute cartoon crab (Ferris) holding a magical floating shopping list that keeps growing and shrinking. Items like milk, bread, eggs, and cheese pop in and out with gentle sparkles. Background: a cozy space-market aisle with glowing shelves. Style: vibrant children’s book illustration, playful, high quality.]](assets/images/8.1.png)
8.1.2. Introducing Vec<T>
Vec<T> is a smart box that can hold many items of the same type (T) in a row. That T can be anything: numbers, text, or even structs we built ourselves! The best thing about vectors is that their size isn’t fixed. You can add or remove items anytime.
8.1.3. Creating a Vector
There are two main ways to create a vector:
#![allow(unused)]
fn main() {
// Method 1: Create an empty vector (we must specify the type)
let mut shopping_list: Vec<String> = Vec::new();
// Method 2: Create with initial values (quick and easy with the vec! macro)
let mut shopping_list = vec![
"Milk".to_string(),
"Bread".to_string(),
];
}
The vec! macro is super handy. Just put your values separated by commas inside [].
8.1.4. Adding Items with push
To add a new item to the end of the vector, we use the push method:
#![allow(unused)]
fn main() {
shopping_list.push("Eggs".to_string());
shopping_list.push("Apple".to_string());
}
Now our vector has four items: Milk, Bread, Eggs, Apple. 🥚🍎
8.1.5. Removing the Last Item with pop
If we change our mind and don’t want the apple anymore, we can use pop to take the last item out. Important note: pop returns an Option<T>! If the vector isn’t empty, it gives Some(last_item). If it is empty, it gives None. This way, the program never crashes!
#![allow(unused)]
fn main() {
let last_item = shopping_list.pop();
match last_item {
Some(item) => println!("Removed: {}", item),
None => println!("The list was already empty!"),
}
}
8.1.6. Accessing Elements (Index vs get)
To read a specific item by its number (index starts at 0), we have two ways:
🔹 Fast but risky []:
#![allow(unused)]
fn main() {
let second = &shopping_list[1]; // "Bread"
// let bad = &shopping_list[10]; // ❌ If index doesn't exist, the program panics!
}
🔹 Safe and recommended get: This method returns an Option<&T>. If the index exists, it gives Some, otherwise None.
#![allow(unused)]
fn main() {
match shopping_list.get(1) {
Some(item) => println!("Second item: {}", item),
None => println!("This index doesn't exist!"),
}
}
💡 Safety Tip: Always try to use get when you’re not 100% sure the index is valid.
8.1.7. Looping Over a Vector
To visit every item in the vector one by one, we use a for loop:
#![allow(unused)]
fn main() {
// Read-only (immutable reference)
for item in &shopping_list {
println!("- {}", item);
}
// If you want to change the items inside, use &mut
for item in &mut shopping_list {
item.push_str("!"); // Adds an exclamation mark to each item
}
}
8.1.8. Exercise: Sum & Average
Create a vector of integers (i32) and calculate their sum and average.
💡 Sample Answer:
fn main() {
let numbers = vec![10, 20, 30, 40, 50];
// Professional & fast way
let sum: i32 = numbers.iter().sum();
let count = numbers.len();
let average = sum as f64 / count as f64;
println!("Sum: {}", sum);
println!("Average: {:.2}", average); // Two decimal places
}
![[Illustration: A cartoon vector visualized as a train of glowing train cars. Each car holds a different item (Milk, Bread, Eggs, Apple). A crab is pushing a new car “Cheese” onto the end, and another crab is popping off the last car. Style: playful, educational, bright colors, 16:9.]](assets/images/8.2.png)
8.2. The Secret Address Book (HashMap)
8.2.1. The Story: Ferris’s Contact Book
Ferris has many friends across the galaxy: Bill, Luna, Stella… 🌌 He keeps a magical notebook. Whenever he opens it to a friend’s name, their space phone number magically appears. In this book, every name is connected to a number. In programming, we call this a Map. In Rust, it’s called HashMap.
![[Illustration: A cartoon crab holding a glowing magical address book. When a page labeled “Luna” opens, a holographic phone number floats out. The background shows a starry galaxy with tiny planets. Style: whimsical, educational children’s book illustration, bright colors.]](assets/images/8.3.png)
8.2.2. Introducing HashMap<K, V>
HashMap<K, V> is a smart box that connects a Key (K) to a Value (V). Its superpower is speed: finding a value by its key is incredibly fast, even if we have millions of entries!
Before using it, we must bring it into our code from the standard library:
#![allow(unused)]
fn main() {
use std::collections::HashMap;
}
8.2.3. Creating a HashMap
#![allow(unused)]
fn main() {
let mut phone_book = HashMap::new();
}
We can also create one with initial data (using vec and collect):
#![allow(unused)]
fn main() {
let data = vec![("Ferris", "12345"), ("Bill", "67890")];
let phone_book: HashMap<_, _> = data.into_iter().collect();
}
8.2.4. Insert & Update (insert)
With insert, we add a key-value pair. If the key already exists, it updates the value:
#![allow(unused)]
fn main() {
phone_book.insert(String::from("Ferris"), String::from("123456"));
phone_book.insert(String::from("Bill"), String::from("789101"));
}
8.2.5. Getting a Value (get)
To find a phone number, we use get, which returns Option<&V> (because the person might not be in the book):
#![allow(unused)]
fn main() {
let name = "Ferris";
match phone_book.get(name) {
Some(number) => println!("Number for {}: {}", name, number),
None => println!("{} is not in the book!", name),
}
}
8.2.6. Checking for a Key (contains_key)
If you just want to know if a key exists without reading its value:
#![allow(unused)]
fn main() {
if phone_book.contains_key("Ferris") {
println!("Ferris is in the book. ✅");
}
}
8.2.7. Removing with remove
remove deletes a key and its value. It returns Option<V> (the removed value, if it existed):
#![allow(unused)]
fn main() {
let removed = phone_book.remove("Bill");
if removed.is_some() {
println!("Bill was removed from the book. 🗑️");
}
}
8.2.8. Conditional Update with entry & or_insert
Sometimes we want to say: “If the key doesn’t exist, put a default value, then give me a way to change it.” We use entry and or_insert for this. It’s perfect for counting:
#![allow(unused)]
fn main() {
// If "Stella" doesn't exist, insert "0000" and give us a mutable handle to it
let count = phone_book.entry("Stella").or_insert(String::from("0000"));
// Now `count` is a `&mut String` we can modify
}
8.2.9. Looping Over a HashMap
#![allow(unused)]
fn main() {
for (key, value) in &phone_book {
println!("{} -> {}", key, value);
}
}
📌 Note: HashMap does not guarantee order! It’s like a magic bag where items might come out in a different sequence each time. But don’t worry, finding them is always fast.
8.2.10. Exercise: Word Counter
Write a program that takes a sentence from the user and counts how many times each word appears using a HashMap.
💡 Sample Answer:
use std::collections::HashMap;
use std::io;
fn main() {
println!("Write a sentence:");
let mut text = String::new();
io::stdin().read_line(&mut text).expect("Failed to read");
let mut counts = HashMap::new();
// Split the sentence by whitespace
for word in text.split_whitespace() {
let count = counts.entry(word).or_insert(0);
*count += 1; // The `*` means: change the actual number inside the reference (since `count` is a `&mut i32`)
}
println!("\nWord frequencies:");
for (word, count) in &counts {
println!("{}: {}", word, count);
}
}
![[Illustration: A cartoon address book with pages that have key-value pairs (Ferris→12345, Bill→67890, Luna→99999). A magical magnifying glass highlights the “Luna” page, and a holographic number pops out. Style: educational, whimsical, bright colors, 16:9.]](assets/images/8.4.png)
8.3. Story: Collecting RPG Items
Let’s build a mini adventure together! Ferris explores an unknown planet and collects items. 🗺️💎
8.3.1. Defining struct Item
#![allow(unused)]
fn main() {
#[derive(Debug)] // This line lets us easily print the items
struct Item {
name: String,
value: u32, // Value in coins
}
}
8.3.2. Item Bag (Vec<Item>)
#![allow(unused)]
fn main() {
let mut inventory: Vec<Item> = Vec::new();
inventory.push(Item { name: String::from("Wooden Sword"), value: 10 });
inventory.push(Item { name: String::from("Health Potion"), value: 25 });
}
8.3.3. Treasure Map (HashMap<String, u32>)
A map of different locations and how much treasure is hidden there:
#![allow(unused)]
fn main() {
let mut treasure_map = HashMap::new();
treasure_map.insert(String::from("Dark Cave"), 500);
treasure_map.insert(String::from("Misty Forest"), 200);
treasure_map.insert(String::from("Snowy Peak"), 1000);
}
8.3.4. Helper Functions
#![allow(unused)]
fn main() {
fn add_item(inventory: &mut Vec<Item>, item: Item) {
println!("✅ Added '{}' to the bag.", item.name);
inventory.push(item);
}
fn show_inventory(inventory: &Vec<Item>) {
if inventory.is_empty() {
println!("The bag is empty! 😢");
} else {
println!("🎒 Your inventory:");
for item in inventory {
println!(" - {} (Value: {} coins)", item.name, item.value);
}
}
}
fn search_treasure(map: &HashMap<String, u32>, place: &str) -> Option<u32> {
map.get(place).copied() // `.copied()` turns `&u32` into `u32` (copies the number)
}
}
8.3.5. Interactive Story
fn main() {
let mut inventory = Vec::new();
let mut treasure_map = HashMap::new();
treasure_map.insert(String::from("Cave"), 500);
treasure_map.insert(String::from("Forest"), 200);
add_item(&mut inventory, Item { name: String::from("Rusty Key"), value: 5 });
add_item(&mut inventory, Item { name: String::from("Old Map"), value: 50 });
show_inventory(&inventory);
let place = "Cave";
match search_treasure(&treasure_map, place) {
Some(gold) => println!("🎉 Hooray! You found {} coins in {}!", gold, place),
None => println!("❌ No treasure found in {}.", place),
}
}
![[Illustration: A cartoon RPG game scene. Ferris stands in front of a treasure chest with a map in hand. A vector backpack is on his back with item slots (Wooden Sword, Health Potion). A glowing map table shows locations: Dark Cave, Misty Forest, Snowy Peak. Style: fantasy children’s book illustration, adventurous, bright, 16:9.]](assets/images/8.5.png)
8.4. Performance & Choosing the Right Tool
8.4.1. When to Use a Vector?
✅ When order matters (e.g., a queue or to-do list). ✅ When you need to access items by index (number). ✅ When you mostly add to or remove from the end.
8.4.2. When to Use a HashMap?
✅ When you want fast lookup using a key (like a name or ID). ✅ When order doesn’t matter. ✅ When each key has exactly one value (e.g., a phone number for a person).
8.4.3. Introducing HashSet (No Values)
Sometimes we just want a collection of unique items (like a guest list for a party). HashSet<T> is exactly like a HashMap, but without values! It automatically prevents duplicates.
#![allow(unused)]
fn main() {
use std::collections::HashSet;
let mut names = HashSet::new();
names.insert("Ferris");
names.insert("Bill");
names.insert("Ferris"); // Duplicate, won't be added!
println!("Number of guests: {}", names.len()); // 2
}
8.4.4. Exercise: Intersection of Two Lists
You have two vectors: [1, 2, 3, 4, 5] and [4, 5, 6, 7, 8]. Find the common numbers using HashSet.
💡 Sample Answer:
use std::collections::HashSet;
fn main() {
let a = vec![1, 2, 3, 4, 5];
let b = vec![4, 5, 6, 7, 8];
let set_a: HashSet<_> = a.into_iter().collect();
let set_b: HashSet<_> = b.into_iter().collect();
let common: Vec<_> = set_a.intersection(&set_b).collect();
println!("Common numbers: {:?}", common); // [4, 5]
}
![[Illustration: Two overlapping magical circles labeled “List A” and “List B”. In the intersection, glowing numbers “4” and “5” float with sparkles. Ferris the crab stands nearby holding a magnifying glass. Style: educational vector illustration, clean, bright.]](assets/images/8.6.png)
8.5. Project: Interactive Phonebook
Let’s combine everything into a complete terminal-based phonebook project! 📞
8.5.1. Main Menu
use std::collections::HashMap;
use std::io;
fn main() {
let mut phone_book: HashMap<String, String> = HashMap::new();
loop {
println!("\n📞 Ferris's Phonebook 📞");
println!("1. Add new contact");
println!("2. Search number");
println!("3. Delete contact");
println!("4. Show all contacts");
println!("5. Exit");
let mut choice = String::new();
io::stdin().read_line(&mut choice).unwrap();
match choice.trim() {
"1" => add_contact(&mut phone_book),
"2" => search_contact(&phone_book),
"3" => delete_contact(&mut phone_book),
"4" => show_all(&phone_book),
"5" => {
println!("Goodbye! 👋");
break;
}
_ => println!("Invalid number! Try again."),
}
}
}
8.5.2. Add Contact
#![allow(unused)]
fn main() {
fn add_contact(book: &mut HashMap<String, String>) {
println!("Enter contact name:");
let mut name = String::new();
io::stdin().read_line(&mut name).unwrap();
let name = name.trim().to_string();
println!("Enter phone number:");
let mut number = String::new();
io::stdin().read_line(&mut number).unwrap();
let number = number.trim().to_string();
book.insert(name, number);
println!("✅ Contact added.");
}
}
8.5.3. Search Contact
#![allow(unused)]
fn main() {
fn search_contact(book: &HashMap<String, String>) {
println!("Enter name to search:");
let mut name = String::new();
io::stdin().read_line(&mut name).unwrap();
let name = name.trim();
match book.get(name) {
Some(number) => println!("📞 {}: {}", name, number),
None => println!("❌ {} is not in the book.", name),
}
}
}
8.5.4. Delete Contact
#![allow(unused)]
fn main() {
fn delete_contact(book: &mut HashMap<String, String>) {
println!("Enter name to delete:");
let mut name = String::new();
io::stdin().read_line(&mut name).unwrap();
let name = name.trim();
if book.remove(name).is_some() {
println!("✅ {} was deleted.", name);
} else {
println!("❌ {} does not exist.", name);
}
}
}
8.5.5. Show All
#![allow(unused)]
fn main() {
fn show_all(book: &HashMap<String, String>) {
if book.is_empty() {
println!("📭 The phonebook is empty.");
} else {
println!("📋 Contacts:");
for (name, number) in book {
println!(" {} : {}", name, number);
}
}
}
}
![[Illustration: A cartoon smartphone screen showing Ferris’s Phonebook app interface. The screen displays menu options (Add, Search, Delete, Show All) and a sample contact list. Ferris sits next to the phone holding a pen and notebook. Style: clean, educational, bright UI metaphor, 16:9.]](assets/images/8.7.png)
8.6. Summary & Challenge
8.6.1. What You Learned
In this chapter, you discovered:
✅ Vec<T>: A resizable list. (push, pop, get, [], for loops)
✅ HashMap<K, V>: Key-to-value mapping. (insert, get, entry().or_insert(), remove)
✅ HashSet<T>: A collection with no duplicates.
✅ The difference between safe access (get) and fast access ([]).
8.6.2. Challenge: Student Grades System
Write a program that takes a student’s name and their grade. You can enter multiple grades for the same student. At the end, print the average grade for each student.
💡 Hint: Use HashMap<String, Vec<f64>>. When a new name appears, use entry and or_insert_with(Vec::new) to create an empty vector, then push the grade.
💡 Sample Answer:
use std::collections::HashMap;
use std::io;
fn main() {
let mut grades: HashMap<String, Vec<f64>> = HashMap::new();
loop {
println!("Enter student name (or 'exit' to finish):");
let mut name = String::new();
io::stdin().read_line(&mut name).unwrap();
let name = name.trim().to_string();
if name == "exit" { break; }
println!("Enter grade:");
let mut grade_str = String::new();
io::stdin().read_line(&mut grade_str).unwrap();
let grade: f64 = grade_str.trim().parse().expect("Please enter a valid number");
grades.entry(name).or_insert_with(Vec::new).push(grade);
}
println!("\n📊 --- Average Grades ---");
for (name, scores) in &grades {
let sum: f64 = scores.iter().sum();
let avg = sum / scores.len() as f64;
println!("{}: {:.2}", name, avg);
}
}
Now you know how to use vectors and hash maps to store dynamic, real-world data! 🎒📦 In the next chapter, we’ll dive into Error Handling: learning how to prevent crashes and manage mistakes like a true programming hero! 🛡️🦀
![[Illustration: Ferris wearing a graduation cap, holding a glowing “Chapter 8 Master” badge. Floating around him are colorful Vectors, HashMaps, and Set symbols transforming into a neat digital backpack. Style: encouraging, vibrant children’s book illustration.]](assets/images/8.8.png)
🦀 The Land of Rust: Ferris the Crab’s Space Adventures
Chapter 9: When the Spaceship Breaks! (Error Handling)
📋 Chapter Outline:
9.1. Don’t Press the Red Button! (panic!)
9.1.1. The Story: The Self-Destruct Button
Inside Ferris’s spaceship, there’s a big, shiny red button with a warning label: ⛔ DO NOT PRESS! IMMEDIATE SELF-DESTRUCT. Ferris knows that if anyone presses it, the ship explodes in an instant. No warnings, no second chances. 💥
In Rust, we have the exact same button: panic!. When panic! runs, the program immediately stops, prints an error message, and crashes. Just like the spaceship exploding!
9.1.2. panic! in Practice
Let’s intentionally press the red button:
fn main() {
panic!("The spaceship broke down! Fire everywhere! 🔥");
}
If you run this, you’ll see:
thread 'main' panicked at 'The spaceship broke down! Fire everywhere! 🔥', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The program stops completely. Any code after panic! will never run.
9.1.3. When Does panic Happen?
Besides calling panic! ourselves, some dangerous actions automatically trigger it:
🔹 Accessing a missing index in a vector: vec![1, 2, 3][99]
🔹 Using unwrap() on a None or Err value
🔹 Dividing by zero in debug mode
Example:
#![allow(unused)]
fn main() {
let v = vec![10, 20, 30];
println!("{}", v[10]); // panic: index out of bounds
}
9.1.4. Tracing the Crash with RUST_BACKTRACE
When a panic happens, Rust can show you the exact path that led to the crash (like a detective’s trail!). To see it, run your program with this environment variable:
RUST_BACKTRACE=1 cargo run
You’ll get a list of functions called one after another, helping you pinpoint exactly where the problem started. 🔍
9.1.5. Exercise: Intentional Panic
Write a program with a 5-element vector. Ask the user for an index and print that element. If the index is out of bounds, let it panic. (Try running it with RUST_BACKTRACE=1 and watch the trail!)
![[Illustration: A close-up of a shiny red emergency button labeled “panic!” on a spaceship control panel, surrounded by yellow warning tape. Ferris the crab stands nearby with a shocked expression, holding his claws up to stop someone from pressing it. Style: vibrant, dramatic but child-friendly, high contrast, soft lighting.]](assets/images/9.1.png)
9.2. The Warning Lights (Result)
9.2.1. The Story: The Ship’s Warning Lights
Ferris’s ship also has yellow and amber warning lights. For example, if the engine gets too hot, a yellow light turns on with a message: ⚠️ Engine overheating. Wait 30 seconds. This is a predictable error that can be managed. Ferris can just wait for it to cool down and continue flying.
In Rust, we use Result for these manageable situations. It means: “Either everything went fine, or a predictable problem occurred that we can handle.” 🟡
9.2.2. Introducing Result<T, E>
Result is a super common enum in Rust:
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T), // Success! We got a value of type T
Err(E), // Oops! An error of type E happened
}
}
🔹 T: The type of the successful result.
🔹 E: The type of the error.
9.2.3. Example: Opening a File
The File::open function tries to open a file. The file might not exist, or we might not have permission. So it returns a Result<File, std::io::Error>:
use std::fs::File;
fn main() {
let file_result = File::open("hello.txt");
// file_result could be Ok(File) or Err(Error)
}
9.2.4. How to Handle a Result
🔸 Fast but risky: unwrap() and expect()
If you call .unwrap(), you’re saying: “If it’s an error, crash the program!” .expect(msg) does the same but with your custom message.
#![allow(unused)]
fn main() {
let file = File::open("hello.txt").expect("Couldn't open the file! 📁");
}
⚠️ Warning: Only use these in quick tests or when you’re 100% sure it won’t fail!
🔸 The safe way: match
You can check both outcomes:
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::ErrorKind;
let file = match File::open("hello.txt") {
Ok(f) => f,
Err(error) => match error.kind() {
ErrorKind::NotFound => {
println!("File not found. Creating a new one. 🛠️");
File::create("hello.txt").expect("Failed to create")
}
_ => panic!("An unexpected error occurred! 😱"),
},
};
}
🔸 The cleaner way: unwrap_or_else
It takes a “mini-helper function” (closure) and only runs it if there’s an error:
#![allow(unused)]
fn main() {
let file = File::open("hello.txt").unwrap_or_else(|error| {
panic!("Oops! Error occurred: {:?}", error);
});
}
9.2.5. Exercise: Convert String to Number with Result
Write a function parse_number that takes a &str, tries to convert it to i32, and returns Ok(num) or Err(String) with a friendly message.
💡 Sample Answer:
fn parse_number(s: &str) -> Result<i32, String> {
s.trim()
.parse()
.map_err(|_| format!("'{}' is not a valid number 🔢", s))
}
fn main() {
let inputs = ["42 ", "hello", "-5 ", "3.14"];
for inp in inputs {
match parse_number(inp) {
Ok(n) => println!("{} -> Number: {}", inp, n),
Err(e) => println!("{} -> Error: {}", inp, e),
}
}
}
![[Illustration: A cartoon dashboard with two glowing indicators. Left: a green “OK” light shining on a wrapped gift labeled “T”. Right: a yellow “Warning” light flashing over a toolbox labeled “E”. Ferris stands between them holding a checklist, looking thoughtful. Style: educational metaphor, clean vector, bright colors.]](assets/images/9.2.png)
9.3. Ferris’s Rescue Tool (?)
9.3.1. The Story: The Magic Rescue Question Mark
Ferris carries a magical tool shaped like a question mark (?). Whenever a warning light turns on (a Result returns Err), he can slap the ? on it and say: “If there’s an error, immediately exit this function and pass the error up to whoever called me!” This saves us from writing long match blocks and keeps our code clean! ✨
9.3.2. Using ? in Functions that Return Result
Imagine we want a function that reads a username from a file. With ?, it looks like this:
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username() -> Result<String, io::Error> {
let mut file = File::open("username.txt")?;
let mut username = String::new();
file.read_to_string(&mut username)?;
Ok(username)
}
}
If File::open fails, ? immediately returns that error. If read_to_string fails, it does the same. If everything succeeds, it returns Ok(username).
💡 Crucial Rule: The ? operator only works inside functions that return Result (or Option). If the function’s return type isn’t Result, the compiler will complain.
9.3.3. Chaining ?
You can chain multiple ? operators to make the code even shorter:
#![allow(unused)]
fn main() {
fn read_username_short() -> Result<String, io::Error> {
let mut s = String::new();
File::open("username.txt")?.read_to_string(&mut s)?;
Ok(s)
}
}
9.3.4. ? with Option
The ? operator works on Option too! If the Option is None, the function immediately returns None:
#![allow(unused)]
fn main() {
fn first_char(s: &str) -> Option<char> {
s.chars().next()? // If s is empty, returns None immediately
}
}
9.3.5. Translating Errors with map_err
Sometimes a function’s error type doesn’t match what we want to return. We can use map_err to translate it into a friendlier message:
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::Read;
fn read_number_from_file(filename: &str) -> Result<i32, String> {
let mut s = String::new();
File::open(filename)
.map_err(|e| format!("Failed to open {}: {}", filename, e))?
.read_to_string(&mut s)
.map_err(|e| format!("Failed to read {}: {}", filename, e))?;
s.trim().parse()
.map_err(|_| format!("No valid number found in {}", filename))
}
}
map_err means: “If there’s an error, grab it, run this tiny helper function (|e| ...), and return the new error instead.”
9.3.6. Exercise: Read Two Files and Divide
Assume a.txt and b.txt each contain a number. Write a function that reads both, divides a / b, and returns f64. Handle any errors (missing file, invalid number, division by zero) with clear String messages.
💡 Sample Answer:
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::Read;
fn read_number(filename: &str) -> Result<i32, String> {
let mut s = String::new();
File::open(filename)
.map_err(|e| format!("{}: {}", filename, e))?
.read_to_string(&mut s)
.map_err(|e| format!("{}: {}", filename, e))?;
s.trim().parse().map_err(|_| format!("{}: Not a valid number", filename))
}
fn divide_files() -> Result<f64, String> {
let a = read_number("a.txt")?;
let b = read_number("b.txt")?;
if b == 0 {
return Err(String::from("Division by zero is forbidden! ⛔"));
}
Ok(a as f64 / b as f64)
}
}
![[Illustration: A magical floating question mark tool (?) glowing with soft blue light, acting as a shortcut tunnel. On one side, a long winding path labeled “match match match”. On the other side, a straight fast road through the “?” tunnel. Ferris happily zooms through the shortcut. Style: dynamic, educational cartoon, bright colors.]](assets/images/9.3.png)
9.4. Project: The Resilient Calculator
Now let’s build a real program that never crashes! A simple calculator that handles the four basic operations and manages errors like a professional. 🧮
9.4.1. Getting Expressions from the User
The user types something like 10 + 2 or 8 / 0. The program runs until they type quit.
9.4.2. The parse_expression Function
This function splits the input into: first number, operator, second number. If the format is wrong, it returns an error.
#![allow(unused)]
fn main() {
fn parse_expression(expr: &str) -> Result<(f64, char, f64), String> {
let parts: Vec<&str> = expr.split_whitespace().collect();
if parts.len() != 3 {
return Err("Format must be 'number operator number' (e.g., 5 + 3) 📝".to_string());
}
let a = parts[0].parse::<f64>()
.map_err(|_| format!("'{}' is not a valid first number 🔢", parts[0]))?;
let op = parts[1].chars().next()
.ok_or("Operator must be a single character (e.g., +) 🔣".to_string())?;
let b = parts[2].parse::<f64>()
.map_err(|_| format!("'{}' is not a valid second number 🔢", parts[2]))?;
Ok((a, op, b))
}
}
9.4.3. The calculate Function
#![allow(unused)]
fn main() {
fn calculate(a: f64, op: char, b: f64) -> Result<f64, String> {
match op {
'+' => Ok(a + b),
'-' => Ok(a - b),
'*' => Ok(a * b),
'/' => {
if b == 0.0 {
Err("Division by zero is impossible! ⛔".to_string())
} else {
Ok(a / b)
}
}
_ => Err(format!("Operator '{}' is not supported. Use + - * / ⚠️", op)),
}
}
}
9.4.4. The Main Loop with Error Handling
use std::io::{self, Write};
fn main() {
println!("🧮 Ferris's Resilient Calculator 🧮");
println!("Example: 10 + 5");
println!("Type 'quit' to exit.\n");
loop {
print!("> ");
io::stdout().flush().unwrap();
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let input = input.trim();
if input == "quit" { break; }
match parse_expression(input) {
Ok((a, op, b)) => match calculate(a, op, b) {
Ok(result) => println!("= {} ✅", result),
Err(e) => println!("❌ Error: {}", e),
},
Err(e) => println!("❌ Input error: {}", e),
}
}
println!("Goodbye! 🦀✨");
}
![[Illustration: A cozy cartoon desk with a retro-style calculator that has a glowing screen. Around it float colorful math symbols (+, -, *, /) and a small shield icon labeled “Error Safe”. Ferris types on a keyboard with a confident smile. Style: warm, educational, children’s book illustration.]](assets/images/9.4.png)
9.5. Summary & Challenge
9.5.1. What You Learned
In this chapter, you discovered:
✅ panic!: The red self-destruct button. For unrecoverable errors.
✅ Result<T, E>: For predictable, manageable errors.
✅ unwrap() / expect(): Fast but dangerous (crashes on error).
✅ match: The safe, thorough way to handle all cases.
✅ The ? operator: A magic shortcut for early exit and error propagation (only in Result/Option functions!).
✅ map_err: Translating technical errors into friendly, readable messages.
9.5.2. Challenge: Calculator Without Spaces
Improve the project so it can handle inputs without spaces, like 10+5 or 8/2.
💡 Hint: You can iterate through the string character by character, find the first operator (+, -, *, /), split the string at that position, and parse the two numbers.
Now you know how to handle errors like a champion and write programs that guide users instead of crashing! 🛡️🦀 In the next chapter, we’ll explore Generics and Traits: tools that let us write “one-size-fits-all” code that works with any type, just like a universal space wrench! 🔧🌌
![[Illustration: Ferris wearing a superhero cape, holding a glowing “Chapter 9 Master” badge. Floating around him are safe shields, Result enums, crossed-out panic buttons, and a question mark tool. Encouraging, bright lighting, children’s book style.]](assets/images/9.5.png)
🦀 The Land of Rust: Ferris the Crab’s Space Adventures
Chapter 10: The Toy Factory (Generics & Traits)
📋 Chapter Outline:
10.1. Play-Doh Molds (Generics)
10.1.1. The Story: Star & Heart Molds
In Ferris’s toy factory, he has a special plastic mold shaped like a star. 🌟 This mold only makes one shape, but Ferris can pour red clay, blue clay, green clay, or even glittery clay into it. The result is always a star, but the material and color change.
In programming, we do the exact same thing! We call it Generics. It means writing one piece of code that works with many different types of data, so we don’t have to copy-paste the same code for numbers, text, or other things.
10.1.2. The Problem: Duplicate Code for Different Types
Imagine we want to write a function that finds the largest number in a list. If we write it only for i32:
#![allow(unused)]
fn main() {
fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest { largest = item; }
}
largest
}
}
But what if we want the same thing for f64 (decimals) or char (letters)? We’d have to copy the whole function and just change the type! That’s slow, messy, and full of bugs. 🥲
10.1.3. Introducing Generics with <T>
Instead of copying code, we use a placeholder letter (usually T for Type). T acts like a blank space on a form that the compiler fills in later with the exact type we use:
#![allow(unused)]
fn main() {
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest { largest = item; } // ❌ This gives an error! Why?
}
largest
}
}
⚠️ Important Note: The compiler complains here because it doesn’t know if T even supports comparison (>)! To fix this, we need Traits (which we’ll learn next). For now, let’s look at simpler generic examples that don’t need comparisons.
10.1.4. Generics in Functions
A simple function that takes anything and gives it right back:
fn identity<T>(value: T) -> T {
value
}
fn main() {
let x = identity(42); // T becomes i32 here
let y = identity("Hello!"); // T becomes &str here
println!("{} and {}", x, y);
}
Rust automatically guesses what T should be. We call this Type Inference. 🧠
10.1.5. Generics in Structs
We can build structures where the fields aren’t fixed to one type:
struct Point<T> {
x: T,
y: T,
}
fn main() {
let int_point = Point { x: 5, y: 10 }; // Point<i32>
let float_point = Point { x: 1.5, y: 3.2 }; // Point<f64>
}
Notice that x and y must be the same type. If we want mixed types, we use multiple generics.
10.1.6. Multiple Generic Types
struct Pair<T, U> {
first: T,
second: U,
}
fn main() {
let pair = Pair { first: 42, second: "hello" }; // Pair<i32, &str>
}
10.1.7. Generics in Methods
When writing methods for a generic struct, we must declare <T> before impl:
#![allow(unused)]
fn main() {
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
}
We can even write a method that only works for a specific type of T:
#![allow(unused)]
fn main() {
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
}
Now distance_from_origin only works on decimal Points, not integer ones! How smart is that? 📐
10.1.8. Exercise: Container<T>
Create a generic box that holds anything, and give it a get() method to show what’s inside.
💡 Sample Answer:
struct Container<T> {
item: T,
}
impl<T> Container<T> {
fn new(item: T) -> Self {
Container { item }
}
fn get(&self) -> &T {
&self.item
}
}
fn main() {
let c1 = Container::new(42);
let c2 = Container::new(String::from("Ferris"));
println!("{} and {}", c1.get(), c2.get());
}
![[Illustration: A cartoon workbench with a flexible clay mold labeled <T>. Around it are colorful play-doh shapes (numbers, letters, emojis) being pressed into the mold, each emerging as a perfectly formed generic toy. Ferris the crab stands beside it holding a blueprint. Style: playful, educational children’s book illustration, bright colors, clear metaphor.]](assets/images/10.1.png)
10.2. Toy Certificates (Traits)
10.2.1. The Story: “Makes Sound” & “Flies” Certificates
In Ferris’s factory, every toy comes with a set of certificates. Some have a “Makes Sound” (MakeSound) certificate. Others have a “Flies” (Fly) certificate. These certificates don’t tell us what the toy looks like; they tell us what it can do. In Rust, we call these certificates Traits. 🏅
10.2.2. Defining a Trait
A trait is just a list of behaviors a type must support:
#![allow(unused)]
fn main() {
trait MakeSound {
fn make_sound(&self);
}
}
We don’t write the function body here. We just say: “Whoever claims this certificate must have this method.”
10.2.3. Implementing a Trait for a Type
With impl Trait for Type, we give a struct or enum its certificate:
#![allow(unused)]
fn main() {
struct Dog { name: String }
impl MakeSound for Dog {
fn make_sound(&self) {
println!("{} says: Woof! Woof!", self.name);
}
}
struct Car;
impl MakeSound for Car {
fn make_sound(&self) {
println!("Honk! Honk!");
}
}
}
Now Dog and Car both have the MakeSound trait, but they each make their own unique sound!
10.2.4. Using Trait as a Parameter (Trait Bound)
We can write a function that says: “I don’t care what exact type it is, as long as it has this trait!”
#![allow(unused)]
fn main() {
fn notify(item: &impl MakeSound) {
item.make_sound();
}
}
Or the classic syntax:
#![allow(unused)]
fn main() {
fn notify<T: MakeSound>(item: &T) {
item.make_sound();
}
}
Both do the same thing. The second is better when you have multiple generic types and want to list their rules separately.
10.2.5. Multiple Traits with +
If a type must have two certificates at once, we use +:
#![allow(unused)]
fn main() {
fn fly_and_sound(item: &(impl MakeSound + Fly)) {
item.make_sound();
item.fly();
}
}
10.2.6. Returning with impl Trait
We can write a function that returns an unknown type, but promises it has a specific trait:
#![allow(unused)]
fn main() {
fn get_sound_maker() -> impl MakeSound {
Dog { name: String::from("Bella") }
}
}
This is super useful when the real return type is complicated, but the user only needs to know it “makes a sound”. 🔊
10.2.7. Built-in Traits (Debug, Clone, PartialEq)
Rust comes with ready-made traits you can add with a single #[derive(...)] line:
| Trait | Purpose | Example |
|---|---|---|
Debug | Pretty printing with {:?} | println!("{:?}", obj); |
Clone | Explicit copying with .clone() | let copy = original.clone(); |
Copy | Automatic copying (for simple types) | let x = 5; let y = x; |
PartialEq | Comparison with == and != | if a == b { ... } |
Example:
#[derive(Debug, Clone, PartialEq)]
struct Monster { name: String, power: u32 }
fn main() {
let m1 = Monster { name: String::from("Dodo"), power: 100 };
let m2 = m1.clone();
println!("{:?}", m1); // Pretty print!
println!("Are they equal? {}", m1 == m2); // true
}
10.2.8. Exercise: Area Trait
Create an Area trait that calculates surface area. Implement it for two different shapes and print their areas.
💡 Sample Answer:
trait Area {
fn area(&self) -> f64;
}
struct Circle { radius: f64 }
impl Area for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
struct Rectangle { width: f64, height: f64 }
impl Area for Rectangle {
fn area(&self) -> f64 { self.width * self.height }
}
fn main() {
let c = Circle { radius: 5.0 };
let r = Rectangle { width: 4.0, height: 6.0 };
println!("Circle area: {:.2}", c.area());
println!("Rectangle area: {:.2}", r.area());
}
![[Illustration: A cartoon quality-control desk. A friendly robot inspector stamps “CERTIFIED” badges labeled “MakeSound”, “Fly”, and “Debug” onto different toys (a dog, a car, a robot). Ferris watches proudly holding a checklist. Style: clean vector illustration, educational metaphor, bright colors.]](assets/images/10.2.png)
10.3. Expiration Date Stickers (Lifetimes – Brief Intro)
10.3.1. The Story: Yogurt Expiration Dates
When you buy yogurt, it has an expiration date. It’s safe to eat until that date, but afterward, it goes bad. In Rust, when we create a reference, it also has an “expiration date” called a Lifetime ('a). It tells the compiler how long that reference stays valid. ⏳
10.3.2. The Problem: Reference to Dead Data
#![allow(unused)]
fn main() {
let r;
{
let x = 5;
r = &x;
} // x dies here and is cleaned up!
println!("{}", r); // ❌ Error! r points to empty space.
}
Rust sees this and refuses to compile. This is exactly why Rust is one of the safest languages in the world! 🛡️
10.3.3. Writing Lifetime with 'a
Sometimes the compiler gets confused about which input a function’s output belongs to. We help it with 'a:
#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
}
This means: “The returned reference will live exactly as long as the shorter-lived input.” Now the compiler knows it won’t point to dead data.
10.3.4. Lifetime Elision (Rust’s Smart Guessing)
🎉 Great news: In 95% of cases, you don’t need to write 'a! Rust has simple rules to guess it automatically:
- Every input reference gets its own hidden lifetime.
- If there’s only one input reference, the output gets that same lifetime.
- If there are multiple inputs and one is
&selfor&mut self, the output getsself’s lifetime.
So functions like fn first_word(s: &str) -> &str work perfectly without explicit 'a! ✨
10.3.5. Lifetimes in Structs
If a struct wants to hold a reference, it must declare a lifetime:
struct Excerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Once upon a time...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = Excerpt { part: first_sentence };
println!("{}", excerpt.part);
}
This tells the compiler: “As long as excerpt exists, novel must also exist so part doesn’t break.”
10.3.6. Don’t Worry, Rust Has Your Back!
For now, just remember: Lifetimes are safety stickers that prevent dangling references. In most of this book, Rust figures them out for you. If you’re curious, check Appendix A for a deeper dive! 📖
![[Illustration: A cartoon yogurt cup labeled “&’a str” with a glowing expiration date sticker. A friendly compiler robot checks the sticker with a magnifying glass, giving a green checkmark. Ferris stands nearby pointing at the safety seal. Style: educational, playful, soft lighting.]](assets/images/10.3.png)
10.4. Project: Shape Library with Generics & Traits
Let’s build a small but professional shape library that calculates area and perimeter for any shape! 📐🔺🟦
10.4.1. Define the Shape Trait
#![allow(unused)]
fn main() {
trait Shape {
fn area(&self) -> f64;
fn perimeter(&self) -> f64;
}
}
10.4.2. Define struct Circle<T>
#![allow(unused)]
fn main() {
struct Circle<T> {
radius: T,
}
}
10.4.3. Implement Shape for Circle<T>
Since formulas need f64, we must ensure T can convert to it. We use where for clean conditions:
#![allow(unused)]
fn main() {
use std::f64::consts::PI;
impl<T> Shape for Circle<T>
where
T: Into<f64> + Copy,
{
fn area(&self) -> f64 {
let r: f64 = self.radius.into();
PI * r * r
}
fn perimeter(&self) -> f64 {
let r: f64 = self.radius.into();
2.0 * PI * r
}
}
}
where means: “T must be able to convert to f64 AND be copyable.”
10.4.4. Define struct Rectangle<T, U>
#![allow(unused)]
fn main() {
struct Rectangle<T, U> { width: T, height: U }
impl<T, U> Shape for Rectangle<T, U>
where
T: Into<f64> + Copy,
U: Into<f64> + Copy,
{
fn area(&self) -> f64 {
let w: f64 = self.width.into();
let h: f64 = self.height.into();
w * h
}
fn perimeter(&self) -> f64 {
2.0 * (self.width.into() + self.height.into())
}
}
}
10.4.5. Using Trait Objects (Box<dyn Shape>)
Now we want a list containing both circles and rectangles. Since they’re different sizes, we can’t put them directly in an array. The solution? Trait Objects!
fn main() {
let circle = Circle { radius: 3.0 };
let rect = Rectangle { width: 4.0, height: 5.0 };
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(circle),
Box::new(rect),
];
for shape in shapes {
println!("Area: {:.2}, Perimeter: {:.2}", shape.area(), shape.perimeter());
}
}
dyn Shape means: “This box can hold anything that has the Shape trait.” Box puts it in flexible memory so size doesn’t matter. Now we can loop through completely different shapes in one list! 🎉
![[Illustration: A cartoon universal shipping box labeled “Box<dyn Shape>”. Inside, different 3D shapes (circle, rectangle, triangle) with glowing trait badges are neatly stacked. Ferris operates a conveyor belt placing them into the box. Style: dynamic, educational vector, bright colors.]](assets/images/10.4.png)
10.5. Summary & Challenge
10.5.1. What You Learned
In this chapter, you discovered:
✅ Generics (<T>): Writing reusable code for any type without duplication.
✅ Traits: Defining shared behaviors (like MakeSound or Shape).
✅ Trait Bounds (T: Trait): Restricting generics to types that implement a trait.
✅ impl Trait: Simplifying parameters and return types.
✅ Lifetimes ('a): Ensuring reference safety by defining valid time ranges (brief intro).
✅ Trait Objects (Box<dyn Trait>): Storing different types with shared behavior in one collection.
10.5.2. Challenge: Fix the largest Function
Remember the largest function that errored at the start? Now, using the PartialOrd trait (which adds > comparison) and Copy, complete it so it works for any slice of comparable types.
💡 Sample Answer:
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest { largest = item; }
}
largest
}
fn main() {
let numbers = vec![34, 50, 25, 100, 65];
println!("Largest number: {}", largest(&numbers));
let chars = vec!['y', 'm', 'a', 'q'];
println!("Largest character: {}", largest(&chars));
}
Now you know how to write all-purpose code with Generics and define shared behaviors with Traits. You also took your first look at Lifetimes and saw how Rust guarantees memory safety! 🛡️ In the next chapter, we’ll learn how to use Tests to make sure our programs always work correctly, just like testing a spaceship before launch! 🚀🧪
![[Illustration: Ferris wearing a graduation cap and safety goggles, holding a glowing “Chapter 10 Master” badge. Floating around him are generic molds <T>, trait certificates, lifetime stickers 'a, and a universal box. Encouraging, bright lighting, children’s book style.]](assets/images/10.5.png)
🦀 The Land of Rust: Ferris the Crab’s Space Adventures
Chapter 11: Test the Self-Destruct Button! (Writing Tests)
📋 Chapter Outline:
11.1. Before Launch: The Simulator
11.1.1. The Story: Ferris’s Flight Simulator
Before Ferris presses the real launch button on his spaceship, he tests every system in a special simulator room. 🚀🕹️ He pushes buttons, starts engines, turns the steering wheel, and checks if everything works correctly. If a red light turns on in the simulator, Ferris is actually happy! Why? Because he found a problem before the real danger and can now fix it.
In programming, we do exactly the same thing, and we call it Testing – one of the most important skills for a computer wizard to ensure the program works correctly before sharing it with others. 🧙♂️
![[Illustration: A cartoon cockpit labeled “SIMULATION MODE”. Ferris the crab sits in the pilot seat, pressing buttons on a dashboard. Green checkmarks float above working systems, while a red warning light blinks on a “test failed” panel. Style: vibrant children’s book illustration, playful tech metaphor, soft lighting, 16:9.]](assets/images/11.1.png)
11.1.2. What Is a Test?
A test is a small piece of code that calls a part of the main program and compares its result with what we expected.
✅ If the result matches what we wanted → the test turns green (passes) and the trust light turns on. ❌ If the result is different → the test turns red (fails) and the compiler tells us exactly where we made a mistake.
11.1.3. Your First Test with #[test]
In Rust, to turn a regular function into a test, we just add a magic label above it: #[test].
Inside the function, we use checking tools (like assert_eq!) to verify correctness:
#![allow(unused)]
fn main() {
#[test]
fn check_addition() {
assert_eq!(2 + 2, 4);
}
}
This code tells the compiler: “This is a test. Please check that 2+2 really equals 4!”
11.1.4. Running Tests with cargo test
To run all tests, open your terminal inside the project folder and type:
cargo test
Cargo searches through all files, finds functions marked with #[test], and runs them one by one.
11.1.5. Reading Test Output
If everything is correct, you’ll see a nice green output:
running 1 test
test check_addition ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
If a test fails, it shows up in red with detailed information:
failures:
---- check_addition stdout ----
thread 'check_addition' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `4`', src/lib.rs:3:5
The compiler tells you exactly: “I saw 5 on the left side, but I expected 4 on the right side!”
![[Illustration: A friendly robot quality inspector holding a rubber stamp. One stamp says “assert_eq! ✅”, the other shows a red warning symbol. Ferris stands beside a conveyor belt of code blocks waiting for inspection. Style: clean educational cartoon, bright colors, clear visual metaphor.]](assets/images/11.2.png)
👨👩👧 Note for parents and teachers
Test‑writing is one of the most valuable professional habits in programming. This chapter shows how writing tests helps us ensure code works correctly. If a child feels tired of writing tests at first, remind them that tests are like seatbelts – they might take a moment to buckle, but they save lives. The official Rust book has a complete chapter about testing:
doc.rust-lang.org/book/ch11-00-testing.html
11.2. Essential Testing Macros
11.2.1. assert!
This macro takes a condition and checks that it is definitely true. If it’s false, the test fails.
#![allow(unused)]
fn main() {
#[test]
fn test_is_positive() {
let num = 5;
assert!(num > 0); // This is true, so the test passes
}
}
11.2.2. assert_eq! and assert_ne!
🔹 assert_eq!(left, right): Checks that two values are exactly equal.
🔹 assert_ne!(left, right): Checks that two values are not equal.
#![allow(unused)]
fn main() {
fn add(a: i32, b: i32) -> i32 { a + b }
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5); // Should be 5
assert_ne!(add(2, 2), 10); // Should NOT be 10
}
}
11.2.3. Adding Custom Messages
You can add a custom message so that if a test fails, you understand exactly what happened:
#![allow(unused)]
fn main() {
#[test]
fn test_add_with_message() {
let result = add(2, 2);
assert_eq!(result, 5, "We expected 5, but got {}.", result);
}
}
11.2.4. #[should_panic]
Some functions are designed to panic in certain situations. For example, a division function should panic if the divisor is zero. To test this behavior, we use #[should_panic]:
#![allow(unused)]
fn main() {
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Division by zero is forbidden!");
}
a / b
}
#[test]
#[should_panic(expected = "Division by zero")]
fn test_divide_by_zero() {
divide(10, 0);
}
}
The expected attribute helps us make sure the panic happened for exactly the reason we anticipated.
11.2.5. Exercise: Test the add Function
Write an add function that adds two numbers. Then write three tests for it: adding two positive numbers, adding a positive and a negative number, and adding two negative numbers.
💡 Sample Answer:
#![allow(unused)]
fn main() {
fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_positive() { assert_eq!(add(2, 3), 5); }
#[test]
fn test_add_mixed() { assert_eq!(add(5, -3), 2); }
#[test]
fn test_add_negative() { assert_eq!(add(-2, -3), -5); }
}
}
![[Illustration: A friendly robot quality inspector holding a rubber stamp. One stamp says “assert_eq! ✅”, the other “assert_ne! ❌”. Ferris stands beside a conveyor belt of code blocks waiting for inspection. Style: clean educational cartoon, bright colors, clear visual metaphor.]](assets/images/11.3.png)
11.3. Organizing Your Tests
11.3.1. Unit Tests
Unit tests examine the smallest parts of a program (like a single function or method) in isolation. These tests are usually written in the same file as the main code.
11.3.2. The tests Module and #[cfg(test)]
To separate test code from main code (and prevent it from being compiled in the final version), we put tests inside a module called tests and write #[cfg(test)] above it:
#![allow(unused)]
fn main() {
pub fn add(a: i32, b: i32) -> i32 { a + b }
#[cfg(test)]
mod tests {
use super::*; // Bring everything from outside into this module
#[test]
fn test_add() { assert_eq!(add(2, 2), 4); }
}
}
#[cfg(test)] means: “Only compile this module when I’m running tests.” It’s like a secret room that only opens during inspection! 🕵️♂️
🧠 Sometimes things are hard, and that’s okay!
Writing tests might feel a bit tedious at first, but the more you practice, the faster and more enjoyable it becomes. Even professional programmers, when they find a bug, first write a test to make sure that bug never comes back.
11.3.3. Integration Tests
These tests examine the program from an external user’s perspective. Integration tests live in a separate folder called tests/ (next to the src/ folder). Each .rs file in this folder behaves like an independent project and must use our library.
📂 Folder Structure:
my_project/
├── Cargo.toml
├── src/
│ └── lib.rs // Main code
└── tests/
└── integration_test.rs // External tests
Example tests/integration_test.rs:
#![allow(unused)]
fn main() {
use my_project::add; // Write your project name here
#[test]
fn test_add_integration() {
assert_eq!(add(2, 2), 4);
}
}
11.3.4. Running Just One Test
If your project is large, you can run just one specific test:
cargo test test_add_positive
You can even type part of the name to run all similar tests: cargo test add
11.3.5. Ignoring Tests with #[ignore]
If a test takes too long or isn’t ready yet, you can temporarily disable it:
#![allow(unused)]
fn main() {
#[test]
#[ignore]
fn long_running_test() { /* Code that takes 10 minutes */ }
}
To run ignored tests: cargo test -- --ignored
![[Illustration: Architectural blueprint of a codebase. Left side: main factory labeled “src”. Right side: a hidden laboratory labeled “#[cfg(test)] mod tests” connected by a secret tunnel. Top side: an external inspection booth labeled “tests/ integration”. Ferris points to the different zones. Style: playful technical diagram, children’s book style, bright, 16:9.]](assets/images/11.4.png)
11.4. Project: Testing the Guess-the-Number Game (from Chapter 2)
Now it’s time to rewrite the guess-the-number game so it can be tested. (Remember? It generated a random number, took user input, and gave hints.)
11.4.1. Converting the Game to a Library
First, we create a library project so we can test its functions:
cargo new guess_game_lib --lib
cd guess_game_lib
In Cargo.toml, add the rand dependency (new version):
[dependencies]
rand = "0.9.0"
11.4.2. The generate_secret Function with Fixed Seed
In tests, we don’t want the number to be truly random (because it changes every time and we can’t predict the result). So we make a helper function just for tests that always returns a fixed number:
#![allow(unused)]
fn main() {
// src/lib.rs
use rand::Rng;
pub fn generate_secret() -> u32 {
rand::thread_rng().gen_range(1..=100)
}
#[cfg(test)]
pub fn generate_secret_fixed() -> u32 { 42 } // Always returns 42
}
11.4.3. The check_guess Function
This function contains the main game logic:
#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq)]
pub enum GuessResult { TooLow, TooHigh, Correct }
pub fn check_guess(guess: u32, secret: u32) -> GuessResult {
if guess < secret { GuessResult::TooLow }
else if guess > secret { GuessResult::TooHigh }
else { GuessResult::Correct }
}
}
11.4.4. Writing the Tests
Now in lib.rs, under the tests module, we write our tests:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_check_guess_too_low() {
assert_eq!(check_guess(10, 42), GuessResult::TooLow);
}
#[test]
fn test_check_guess_correct() {
assert_eq!(check_guess(42, 42), GuessResult::Correct);
}
#[test]
fn test_generate_secret_fixed() {
assert_eq!(generate_secret_fixed(), 42);
}
}
}
11.4.5. Testing read_input with Mocking
How do we test a function that reads from the keyboard? Instead of using real stdin, we write the function to accept anything that can be read from (the BufRead trait). In tests, we use Cursor, which acts like a virtual tape recorder that reads text character by character.
#![allow(unused)]
fn main() {
use std::io::{BufRead, Cursor};
pub fn read_number<R: BufRead>(reader: &mut R) -> Result<u32, String> {
let mut input = String::new();
reader.read_line(&mut input)
.map_err(|e| format!("Read error: {}", e))?;
input.trim().parse()
.map_err(|_| "Please enter a valid number".to_string())
}
}
And its test:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_read_number_ok() {
let input = b"42\n"; // Virtual tape
let mut cursor = Cursor::new(input);
assert_eq!(read_number(&mut cursor), Ok(42));
}
#[test]
fn test_read_number_invalid() {
let input = b"hello\n";
let mut cursor = Cursor::new(input);
assert!(read_number(&mut cursor).is_err());
}
}
}
![[Illustration: Cartoon scene showing a “Mock Input” machine. A tape labeled “Cursor: b’42\n’” feeds into a reader slot. The machine outputs a glowing green “Ok(42)” ticket. Ferris operates the controls with a satisfied smile. Style: dynamic, educational, bright colors, technical metaphor for children, 16:9.]](assets/images/11.5.png)
11.5. Summary & Challenge
11.5.1. What You Learned
In this chapter, you discovered:
✅ Testing is like a flight simulator: We test everything before real use.
✅ #[test] turns a function into a test, and cargo test runs them.
✅ assert!, assert_eq!, and assert_ne! are used to verify correctness.
✅ #[should_panic] tests functions that are supposed to panic intentionally.
✅ Unit tests live in #[cfg(test)] modules; integration tests live in the tests/ folder.
✅ For testing input, we use BufRead and Cursor to avoid needing real typing.
✅ Test‑writing turns you into a real software engineer – someone who finds problems before users experience them. 🧙
11.5.2. Challenge: Tests for the Monster Struct
Return to the Monster struct from Chapter 5. Add an attack method that attacks another monster and reduces its power. Then write three tests:
- An attack that reduces the victim’s power.
- An attack with zero power (should not reduce anything).
- Checking that the returned damage value is correct.
💡 Sample Answer:
#![allow(unused)]
fn main() {
struct Monster { name: String, power: u32 }
impl Monster {
fn attack(&self, other: &mut Monster) -> u32 {
let damage = self.power;
other.power = other.power.saturating_sub(damage);
damage
}
}
#[cfg(test)]
mod monster_tests {
use super::*;
#[test]
fn test_attack_reduces_power() {
let mut victim = Monster { name: String::from("Weak"), power: 100 };
let attacker = Monster { name: String::from("Strong"), power: 30 };
attacker.attack(&mut victim);
assert_eq!(victim.power, 70);
}
#[test]
fn test_attack_with_zero_power() {
let mut victim = Monster { name: String::from("Strong"), power: 100 };
let attacker = Monster { name: String::from("Harmless"), power: 0 };
attacker.attack(&mut victim);
assert_eq!(victim.power, 100); // Should stay 100
}
#[test]
fn test_attack_returns_damage() {
let mut victim = Monster { name: String::from("Victim"), power: 50 };
let attacker = Monster { name: String::from("Attacker"), power: 20 };
let damage = attacker.attack(&mut victim);
assert_eq!(damage, 20);
}
}
}
Now you know how to write tests to ensure your program works correctly and add new changes with confidence. 🛡️✨
In the next chapter, we’ll build a complete, professional command-line project (similar to the grep command) and put together everything we’ve learned so far! 🔍📜
![[Illustration: Ferris wearing a graduation cap and safety goggles, holding a glowing “Chapter 11 Master” badge. Floating around him are testing tools: a green checkmark stamp, a red panic button, a mock tape reader, and a hidden lab door. Style: encouraging, vibrant children’s book illustration, celebratory mood, 16:9.]](assets/images/11.6.png)
Chapter 12: Mini Search Robot
Here’s the rewritten file with image paths added to each illustration prompt:
# 🦀 The Land of Rust: Ferris the Crab's Space Adventures
## Chapter 12: The Mini-Robot Searcher (A Command-Line Project)
> 📋 *Chapter Outline:*
> - [12.1. Building a Tiny Robot (Simple grep)](#121-building-a-tiny-robot-simple-grep)
> - [12.2. Getting Command-Line Arguments](#122-getting-command-line-arguments)
> - [12.3. Reading a File](#123-reading-a-file)
> - [12.4. The Search Logic](#124-the-search-logic)
> - [12.5. Testing Our Mini-Robot](#125-testing-our-mini-robot)
> - [12.6. Making It Smarter](#126-making-it-smarter)
> - [12.7. Summary & Challenge](#127-summary--challenge)
---
## 12.1. Building a Tiny Robot (Simple grep)
### 12.1.1. The Story: Searching Ferris's Space Diary
Ferris has a very thick space diary where he writes down all his cosmic adventures. One day, he wonders: *"How many times did I write the word 'dinosaur' in my diary?"* 🦕
He could flip through page by page, but that would take hours! 😴
Instead, Ferris decides to build a tiny search robot that does the work for him. The robot asks: *"What word should I search for? In which file?"* Then it quickly shows Ferris every line that contains that word. 🤖✨
**This means you're building your very first command-line tool – a big step toward becoming a computer wizard!** 🧙♂️
> 👨👩👧 **Note for Parents and Teachers**
> This project combines concepts from earlier chapters (input, structs, error handling, testing). If your child feels confused about certain parts (like lifetimes in the `search` function), don't worry – this is just a brief mention, and understanding it deeply isn't required to run the program. The official Rust book has a complete implementation of this same tool:
> [doc.rust-lang.org/book/ch12-00-an-io-project.html](https://doc.rust-lang.org/book/ch12-00-an-io-project.html)
### 12.1.2. Project Goal
Our program will do exactly this. We'll build a command-line tool that:
1. Takes two inputs from the user: the search word + the file path.
2. Opens the file and reads its contents.
3. Finds and prints every line containing the word.
This is exactly what the famous `grep` command does in Linux and macOS. We'll name our program `minigrep` (meaning "tiny grep").
### 12.1.3. Creating the Project with Cargo
First, let's create a new project:
```bash
cargo new minigrep
cd minigrep
Open src/main.rs. For simplicity, we’ll write all the code right here.
(💡 Tip: If you want to use edition = "2024" in Cargo.toml, that’s perfectly fine – our code works with both editions.)
![[Illustration: A friendly cartoon crab (Ferris) standing next to a small, boxy robot with a magnifying glass lens. The robot is scanning an open giant notebook with glowing search lines. Background: cozy spaceship desk with a starry window. Style: vibrant children’s book illustration, soft lighting, playful tech metaphor.]](assets/images/12.1.png)
12.2. Getting Command-Line Arguments
12.2.1. Introducing std::env::args
When you run a program from the terminal, you can add extra words after the program name. For example:
cargo run -- dinosaur poem.txt
Those extra words (dinosaur poem.txt) are called command-line arguments. In Rust, the std::env module has a function called args that gives us these words. It’s like handing the robot a note before we turn it on! 📝
12.2.2. Collecting Arguments into a Vec
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
println!("Arguments: {:?}", args);
}
If you run the program with the command above, the output looks something like this:
Arguments: ["target/debug/minigrep", "dinosaur", "poem.txt"]
🔹 Index 0: The program name itself (we don’t need this).
🔹 Index 1: The search word.
🔹 Index 2: The file path.
12.2.3. Building a Config Struct
Instead of constantly using args[1] and args[2] in our code (which gets confusing), let’s build a neat struct to hold our settings in one place:
#![allow(unused)]
fn main() {
struct Config {
query: String,
file_path: String,
}
}
query: The word we’re searching for. file_path: The file’s address.
12.2.4. A build Function for Config
Let’s write an associated function that takes the arguments, checks if there are enough, and builds a Config. If not, it returns an error:
#![allow(unused)]
fn main() {
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("Not enough arguments! You must provide a word and a file.");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
}
💡 Why clone()? Because args owns the strings. We don’t want to take ownership and break things. clone() makes a clean, independent copy. (In bigger projects there are more efficient ways, but here simplicity matters most!)
12.2.5. Error Handling in main
Now in main, we use build. If it returns an error, we print a friendly message and exit with code 1 (which signals an error):
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("❌ Error in arguments: {}", err);
eprintln!("✅ Usage: cargo run -- <word> <file>");
process::exit(1);
});
println!("🔍 Searching for '{}' in file '{}'", config.query, config.file_path);
}
📌 eprintln! is like println!, but it writes to the error output (stderr). This way, if you save the program’s output to a file, error messages won’t get mixed up with the data!
![[Illustration: A cartoon command-line terminal with floating speech bubbles. One bubble says “cargo run – word file.txt”. A small config card labeled “Config { query, path }” is being stamped “APPROVED”. Ferris watches with a checklist. Style: clean educational vector, bright colors, clear UI metaphor.]](assets/images/12.2.png)
12.3. Reading a File
12.3.1. Using std::fs
To work with files, we use the std::fs module:
#![allow(unused)]
fn main() {
use std::fs;
}
12.3.2. Reading File Contents
The fs::read_to_string function is very convenient: it opens the file, reads all the text, and converts it to a String:
#![allow(unused)]
fn main() {
let contents = fs::read_to_string(&config.file_path)
.unwrap_or_else(|err| {
eprintln!("❌ Cannot read file '{}': {}", config.file_path, err);
process::exit(1);
});
}
12.3.3. Handling File-Opening Errors
If the file doesn’t exist or we don’t have permission to read it, unwrap_or_else catches the error, prints a friendly message, and exits the program. This way, the user isn’t left confused! 📂🔍
![[Illustration: A cartoon file cabinet with one drawer open. A glowing document floats out labeled “contents: String”. A small robot arm holds a stamp reading “READ SUCCESS”. Ferris stands nearby giving a thumbs up. Style: clean educational vector, bright colors, 16:9.]](assets/images/12.3.png)
12.4. The Search Logic
12.4.1. The search Function
Now let’s write a function that takes the file text and the search word, goes line by line, and returns the lines that contain the word:
#![allow(unused)]
fn main() {
fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
}
🔹 .lines() splits the text based on line breaks.
🔹 .contains(query) checks if the word is in the line.
🔹 If yes, it adds that line to results.
12.4.2. A Simple Explanation of Lifetimes in search
You see 'a here. Don’t panic! This is just a safety label. It means:
“The lines I return are pieces of the original contents. So as long as contents is alive, this list is valid too. Don’t delete the original text while I’m still using it!”
The compiler uses this label to make sure memory stays safe. For now, just know it’s there for safety! 🛡️
12.4.3. Printing Results
Now in main, after reading the file:
#![allow(unused)]
fn main() {
let results = search(&config.query, &contents);
if results.is_empty() {
println!("❌ No lines containing '{}' were found.", config.query);
} else {
println!("📋 Lines found:");
for line in results {
println!(" {}", line);
}
}
}
If nothing is found, it says “Nothing found.” If something is found, it shows each line one by one! ✅
![[Illustration: A magnifying glass hovering over a long scroll of text. Highlighted lines glow with a soft yellow light while others remain dim. A tiny conveyor belt carries matching lines into a box labeled “Vec<&str>”. Ferris operates the controls. Style: playful technical metaphor, warm lighting, children’s book illustration.]](assets/images/12.4.png)
12.5. Testing Our Mini-Robot
12.5.1. Writing a Test for search
Before we send our robot out into the world, let’s test it in the lab. We’ll write a unit test:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_search_one_result() {
let query = "dinosaur";
let contents = "\
My name is Ferris.
Dinosaurs were very big.
I am afraid of dinosaurs.";
assert_eq!(
vec!["Dinosaurs were very big."],
search(query, contents)
);
}
}
}
We have a sample text and check that only the line containing “dinosaur” is returned.
12.5.2. Running the Test
cargo test
You should see a green output: test tests::test_search_one_result ... ok. This means our robot is working correctly! 🟢
![[Illustration: A cartoon laboratory setting with a test tube rack. One tube glows green labeled “search test ✅”. A checklist shows “assert_eq! passed”. Ferris wears safety goggles and smiles. Style: playful, educational, bright colors, 16:9.]](assets/images/12.5.png)
12.6. Making It Smarter
12.6.1. Case Sensitivity
So far, our search looks for the exact word. It finds "dinosaur" but not "Dinosaurs" or "DINOSAUR". Sometimes users want a case‑insensitive search.
12.6.2. Reading the CASE_INSENSITIVE Environment Variable
Operating systems have hidden environment variables that act like advanced computer settings. If the user runs this before executing the program:
# Linux/macOS
export CASE_INSENSITIVE=1
# Windows (CMD)
set CASE_INSENSITIVE=1
The program understands it should do a case‑insensitive search. In code, we check:
#![allow(unused)]
fn main() {
use std::env;
let ignore_case = env::var("CASE_INSENSITIVE").is_ok();
}
If the variable exists, is_ok() returns true.
12.6.3. The search_case_insensitive Function
We build a similar function, but before comparing, we convert both the word and the line to lowercase:
#![allow(unused)]
fn main() {
fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
}
12.6.4. Using the Environment Variable in Config
We expand Config to hold this setting too:
#![allow(unused)]
fn main() {
struct Config {
query: String,
file_path: String,
ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("Not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("CASE_INSENSITIVE").is_ok();
Ok(Config { query, file_path, ignore_case })
}
}
}
And in main, we decide based on it:
#![allow(unused)]
fn main() {
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
}
![[Illustration: A cartoon control panel with two switches: “Case-Sensitive (ON)” and “Case-Insensitive (OFF)”. A glowing environment variable label “CASE_INSENSITIVE=1” flips the switch. Ferris adjusts a dial with a smile. Style: clean, educational infographic, bright colors.]](assets/images/12.6.png)
12.7. Summary & Challenge
12.7.1. What You Learned
In this chapter, you discovered:
✅ How to get command-line arguments with std::env::args.
✅ How to organize settings neatly with struct Config.
✅ How to read a file with std::fs::read_to_string.
✅ How to search text with lines() and contains().
✅ How to write tests with #[cfg(test)] and assert_eq!.
✅ How to use environment variables for advanced settings.
✅ How to manage errors cleanly with Result and unwrap_or_else.
✅ Building a command-line tool means you can command the computer – that’s what a real computer wizard does! 🧙
🧠 Sometimes things are hard, and that’s okay!
Theminigrepproject is one of the first serious projects in learning Rust. Some parts (like lifetimes in thesearchfunction) might seem fuzzy at first. Don’t worry – what matters is that the program works. With time and more practice, these concepts will become clearer.
12.7.2. Challenge: Search for Multiple Words
Now make the program one step more professional! Let the user enter multiple words separated by |, and have the program find lines containing at least one of those words.
Example run:
cargo run -- "dinosaur|spaceship|star" poem.txt
💡 Hint: You can split the string with split('|') and then use any() to check if the line contains at least one of the words.
💡 Sample Answer (main part):
#![allow(unused)]
fn main() {
fn search_multiple<'a>(queries: &[&str], contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if queries.iter().any(|q| line.contains(q)) {
results.push(line);
}
}
results
}
}
To use it in main, call it like this:
#![allow(unused)]
fn main() {
let query_list: Vec<&str> = config.query.split('|').collect();
let results = search_multiple(&query_list, &contents);
}
Now you’ve built a real, usable, tested command-line tool! You can share it with friends or even use it in your future projects. 🛠️🚀
In the next chapter, we’ll explore Iterators and Closures – tools that make your code cleaner, shorter, and more professional, just like a space Swiss Army knife! 🔪✨
Land of Rust: Ferris the Space Crab’s Adventures
Chapter 13: The Magic Train and the Transformation Factory (Iterators & Closures)
📑 Chapter Index
13.1. The Cargo Train (Iterator)
13.1.1. Story: The Train of Stone Wagons
13.1.2. What Is an Iterator?
13.1.3. Getting an Iterator from a Collection
13.1.4. The next Method
13.1.5. The for Loop and Iterator
13.1.6. Exercise: Traversing with while let
13.2. Machines Along the Track (Adapter Methods)
13.2.1. map: Transforming Each Element
13.2.2. filter: Selecting Based on a Condition
13.2.3. Chaining
13.2.4. collect: Gathering Results
13.2.5. Other Useful Methods (fold, any, all, sum)
13.2.6. Exercise: Squared Even Numbers
13.3. The Spy Backpack (Closures)
13.3.1. Story: Ferris’s Secret Tool
13.3.2. Defining a Closure
13.3.3. Capturing Variables from the Environment
13.3.4. Capture Types (Fn, FnMut, FnOnce)
13.3.5. Moving Ownership with move
13.3.6. Exercise: Multiplier Closure
13.4. Combining Iterator and Closure
13.4.1. map with a Closure
13.4.2. filter with a Closure
13.4.3. Example: Reading Lines from stdin and Converting to Numbers
13.4.4. filter_map for Combining Two Jobs
13.5. Project: Processing a Log File
13.5.1. Structure of Log Lines
13.5.2. Reading a File with Iterator
13.5.3. Filtering ERROR Lines
13.5.4. Counting Errors with a HashMap
13.6. Summary and Challenge
13.6.1. Concept Review
13.6.2. Challenge: Implementing Iterator for Fibonacci
13.1. The Cargo Train (Iterator)
13.1.1. Story: The Train of Stone Wagons
Ferris has a big cargo train whose wagons are filled with sparkling stones from different planets. 🚂💎 He doesn’t want to dump all the stones at once and make a mess on the ground. He prefers the train to move slowly and open only one wagon at a time, delivering one stone at a time. This is called Iteration.
In programming, when we work with lists or collections, we often need to look at each member one by one. The tool that does this for us in an orderly way is called an Iterator.
Here we learn how to traverse data without writing long loops – a big step toward professional coding! 🧙♂️
👨👩👧 Note for parents and teachers
Iterators and Closures are powerful Rust features that enable clean, efficient code. This chapter might be challenging for some children – don’t worry, they will be used many times in later projects. The official Rust book has a whole chapter on Iterators:
doc.rust-lang.org/book/ch13-00-functional-features.html
13.1.2. What Is an Iterator?
An Iterator is a smart being that knows how to hand out the members of a collection one by one, in order, and only when asked. In Rust, the Iterator trait is standard; its most important tool is the next method:
#![allow(unused)]
fn main() {
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
}
🔹 Every time you call next, it gives you the next member inside Some(member).
🔹 When there are no more members, it returns None.
💡 Golden tip: Iterators are lazy! They don’t do any work until you ask for the next item or collect the results. That means they don’t waste memory or speed! ⏱️
13.1.3. Getting an Iterator from a Collection
To get an Iterator from a vector or array, you have three main ways:
| Method | Output type | Simple explanation |
|---|---|---|
.iter() | &T | Only looks (immutable borrow). The collection stays untouched. |
.iter_mut() | &mut T | Looks and allows modification. |
.into_iter() | T | Takes ownership. After it finishes, you can’t use the collection anymore. |
#![allow(unused)]
fn main() {
let v = vec![10, 20, 30];
let iter1 = v.iter(); // only looks
let iter2 = v.iter_mut(); // can modify
let iter3 = v.into_iter(); // takes ownership (v cannot be used after this line)
}
13.1.4. The next Method
You can manually call next to see how it works:
#![allow(unused)]
fn main() {
let v = vec!["apple", "banana", "orange"];
let mut iter = v.iter(); // must be mutable because the internal position changes
assert_eq!(iter.next(), Some(&"apple"));
assert_eq!(iter.next(), Some(&"banana"));
assert_eq!(iter.next(), Some(&"orange"));
assert_eq!(iter.next(), None); // nothing left!
}
13.1.5. The for Loop and Iterator
The good news is that you don’t have to deal with next yourself! The for loop in Rust automatically creates an Iterator behind the scenes and continues until None is returned:
#![allow(unused)]
fn main() {
let names = vec!["Ferris", "Bill", "Luna"];
for name in &names { // &names is exactly the same as names.iter()
println!("Hello {}!", name);
}
}
⚠️ If you write for name in names (without &), the loop takes ownership of names, and you cannot use it after the loop.
13.1.6. Exercise: Traversing with while let
Using while let and next(), traverse the vector ["a", "b", "c"] and print each element.
💡 Answer:
fn main() {
let v = vec!["a", "b", "c"];
let mut iter = v.iter();
while let Some(item) = iter.next() {
println!("{}", item);
}
}
while let means: “As long as next returns Some, run the code. When it returns None, stop automatically.” Very clean! ✨
![[Illustration: A friendly cartoon crab (Ferris) standing on a platform next to a colorful train. Each train car has a glowing gem inside. A conveyor arm lifts one gem at a time as the train moves slowly. Background: starry sky with a control panel showing “next() → Some(gem)”. Style: vibrant children’s book illustration, playful tech metaphor, soft lighting, 16:9.]](assets/images/13.1.png)
13.2. Machines Along the Track (Adapter Methods)
13.2.1. map: Transforming Each Element
On the train track there are machines that take stones, change their shape or colour, and send them to the next wagon. In Rust, these are called Adapters. The most famous one is map. It takes a Closure and applies it to each element:
#![allow(unused)]
fn main() {
let numbers = vec![1, 2, 3];
let doubled: Vec<_> = numbers.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6]
}
⚠️ Note: map by itself does nothing! It just creates a new Iterator that says: “When I am asked, I will perform this transformation.”
13.2.2. filter: Selecting Based on a Condition
There’s also a machine that only lets stones pass if they satisfy a certain condition. If the condition is true, the stone passes; if false, it falls off the track. To make working with references easier, we use .copied():
#![allow(unused)]
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let evens: Vec<i32> = numbers.iter().copied().filter(|&x| x % 2 == 0).collect();
println!("{:?}", evens); // [2, 4]
}
.copied() turns &i32 into actual i32 values, so we don’t have to deal with |&&x|.
13.2.3. Chaining
The beauty of Iterators is that you can chain machines one after another:
#![allow(unused)]
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let result: Vec<_> = numbers.iter()
.copied()
.filter(|&x| x % 2 == 0) // first keep only evens
.map(|x| x * 10) // then multiply by 10
.collect(); // finally collect them
println!("{:?}", result); // [20, 40]
}
This chain does no computation until it hits .collect() or .sum(). Rust is smart enough to optimize the whole pipeline! 🧠⚡
13.2.4. collect: Gathering Results
collect is the final station. It takes an Iterator and gathers all the results into a new Collection (like Vec or HashMap). You usually need to specify the type: Vec<_> or ::<Vec<i32>>.
13.2.5. Other Useful Methods
| Method | Use | Example |
|---|---|---|
sum() | Sum of numbers | numbers.iter().sum::<i32>() |
fold(init, |acc, x| ...) | General accumulator | fold(0, |acc, x| acc + x) |
any(|&x| ...) | At least one satisfies the condition | any(|&x| x > 10) |
all(|&x| ...) | All satisfy the condition | all(|&x| x > 0) |
13.2.6. Exercise: Squared Even Numbers
Create a vector of numbers from 1 to 10. Using filter and map, select only the even numbers, compute their squares, and collect them into a new vector.
💡 Answer:
fn main() {
let result: Vec<i32> = (1..=10)
.filter(|&x| x % 2 == 0)
.map(|x| x * x)
.collect();
println!("{:?}", result); // [4, 16, 36, 64, 100]
}
(1..=10) is a Range that already implements Iterator – no need for vec!!
![[Illustration: A cartoon factory conveyor belt with labeled machines: “filter”, “map”, “collect”. Glowing cubes travel through, changing shape and color at each station. Ferris operates a control panel with a big green “Run” button. Style: educational vector, bright, clean, 16:9.]](assets/images/13.2.png)
13.3. The Spy Backpack (Closures)
13.3.1. Story: Ferris’s Secret Tool
Ferris has a magical backpack that can store a secret operation. For example, he tells it: “Whatever number you get, multiply it by 3.” Later, whenever Ferris wants, the backpack performs that operation. The real magic is that the backpack can remember things from the surrounding environment. In Rust, this tool is called a Closure. 🎒✨
13.3.2. Defining a Closure
The syntax is very simple: |parameters| { body }. If the body is only one line, you can omit the curly braces {}:
#![allow(unused)]
fn main() {
let add_one = |x| x + 1;
println!("{}", add_one(5)); // 6
let multiply = |a, b| a * b;
println!("{}", multiply(4, 5)); // 20
}
The types of parameters and return value are usually inferred by the compiler.
13.3.3. Capturing Variables from the Environment
A Closure can use variables that are defined outside of it:
#![allow(unused)]
fn main() {
let factor = 3;
let multiply_by_factor = |x| x * factor; // captures `factor` from the environment
println!("{}", multiply_by_factor(10)); // 30
}
But how does the Closure behave with these variables? There are three possibilities, chosen automatically by the compiler:
13.3.4. Capture Types (Fn, FnMut, FnOnce)
| Type | Behavior | Example |
|---|---|---|
Fn | Only reads (immutable borrow &). Can be called many times. | ` |
FnMut | Can modify (mutable borrow &mut). Called multiple times. | ` |
FnOnce | Takes ownership and consumes it. Can be called only once. | ` |
FnMut example:
#![allow(unused)]
fn main() {
let mut count = 0;
let mut increment = || {
count += 1;
count
};
println!("{}", increment()); // 1
println!("{}", increment()); // 2
}
13.3.5. Moving Ownership with move
If you put the keyword move before the Closure, you tell it: “Please transfer ownership of the captured variables into the Closure.” This is useful when you want to send the Closure to another thread:
#![allow(unused)]
fn main() {
let s = String::from("Hello space!");
let consume = move || {
println!("{}", s);
};
consume();
// println!("{}", s); // ❌ Error! ownership moved to `consume`
}
13.3.6. Exercise: Multiplier Closure
Write a function called make_multiplier that takes a number factor and returns a Closure. The Closure should multiply any number given to it by factor. Use move.
💡 Answer:
fn make_multiplier(factor: i32) -> impl Fn(i32) -> i32 {
move |x| x * factor
}
fn main() {
let double = make_multiplier(2);
let triple = make_multiplier(3);
println!("{}", double(5)); // 10
println!("{}", triple(5)); // 15
}
![[Illustration: A cartoon crab wearing a futuristic backpack with glowing memory chips. Around the crab float variables like “factor = 3” and “x” being pulled into the backpack. A “move” tag is stamped on the backpack. Style: playful sci-fi educational, bright colors, 16:9.]](assets/images/13.3.png)
13.4. Combining Iterator and Closure
13.4.1. map with a Closure
Adapters like map and filter take Closures as input:
#![allow(unused)]
fn main() {
let names = vec!["Ferris", "Bill"];
let greetings: Vec<_> = names.iter()
.map(|name| format!("Hello {}!", name))
.collect();
println!("{:?}", greetings); // ["Hello Ferris!", "Hello Bill!"]
}
13.4.2. filter with a Closure
#![allow(unused)]
fn main() {
let numbers = vec![5, 15, 25, 8];
let big_numbers: Vec<_> = numbers.iter()
.copied()
.filter(|&x| x > 10)
.collect();
println!("{:?}", big_numbers); // [15, 25]
}
13.4.3. Example: Reading Lines from stdin and Converting to Numbers
Suppose the user enters several numbers, one per line, and we want to compute their sum:
use std::io::{self, BufRead};
fn main() {
let stdin = io::stdin();
let sum: i32 = stdin.lock().lines()
.map(|line| line.unwrap().trim().parse::<i32>().unwrap())
.sum();
println!("Sum: {}", sum);
}
Here stdin.lock().lines() gives an Iterator over lines. map takes each line, trims it, and parses it into a number. Finally sum() adds them all up.
13.4.4. filter_map for Combining Two Jobs
Sometimes you want to transform and filter at the same time. filter_map takes a Closure that returns an Option<T>. If it returns Some, keep it; if None, discard it:
#![allow(unused)]
fn main() {
let strings = vec!["3", "seven", "8", "10"];
let numbers: Vec<i32> = strings.iter()
.filter_map(|s| s.parse().ok()) // if parsing fails, returns None and it’s dropped
.collect();
println!("{:?}", numbers); // [3, 8, 10]
}
.ok() is a helper method that turns a Result into an Option. Very handy for cleaning up data! 🧼
![[Illustration: A cartoon sorting machine with a “filter_map” label. As items pass through, some are converted and drop into a “Collected” box while invalid items slide into a “Rejected” bin. Ferris watches the process. Style: educational, playful, bright colors, 16:9.]](assets/images/13.4.png)
13.5. Project: Processing a Log File
Now it’s time to put it all together in a real project. Suppose you have a file log.txt that records system status:
INFO: System initialized
ERROR: Disk is full
WARNING: Memory running low
ERROR: Network connection lost
INFO: Task finished
We want to find the lines that contain ERROR and see how many times each error occurs.
13.5.1. Structure of Log Lines
Each line starts with a label like ERROR:. We only care about real errors.
13.5.2. Reading a File with Iterator
use std::fs::File;
use std::io::{BufRead, BufReader};
fn main() {
let file = File::open("log.txt").expect("log.txt not found");
let reader = BufReader::new(file);
// ... more code
}
13.5.3. Filtering ERROR Lines
#![allow(unused)]
fn main() {
let error_lines: Vec<String> = reader.lines()
.filter_map(|line| line.ok()) // if a line read fails, ignore it
.filter(|line| line.starts_with("ERROR"))
.collect();
println!("Found errors:");
for line in &error_lines {
println!(" {}", line);
}
}
13.5.4. Counting Errors with a HashMap
Now let’s see how many times each error message appears:
#![allow(unused)]
fn main() {
use std::collections::HashMap;
let mut counts = HashMap::new();
for line in reader.lines().filter_map(|l| l.ok()) {
if let Some(msg) = line.strip_prefix("ERROR: ") {
*counts.entry(msg.to_string()).or_insert(0) += 1;
}
}
println!("\n📊 Error statistics:");
for (msg, count) in &counts {
println!(" - {} : {} times", msg, count);
}
}
strip_prefix returns the rest of the line only if it starts with "ERROR: ". Then we use entry and or_insert to count occurrences. The code is clean, fast, and nice! ⚡
![[Illustration: A cartoon control desk with a scrolling screen of log lines. A glowing filter machine catches only “ERROR” tags and drops them into a counting jar. Ferris adjusts a dial labeled “filter_map”. Style: cozy tech workspace, educational vector, warm lighting, 16:9.]](assets/images/13.5.png)
13.6. Summary and Challenge
13.6.1. Concept Review
In this chapter you learned:
✅ Iterator: an object for step‑by‑step traversal of collections. Core method: next.
✅ .iter() / .iter_mut() / .into_iter(): ways to get an Iterator with different access levels.
✅ Adapters: map (transform), filter (select), collect (gather), fold/sum/any/all. All of them are lazy.
✅ Closure: an anonymous function that captures environment variables. Syntax: |x| x * 2.
✅ Closure types: Fn (read‑only), FnMut (modify), FnOnce (consume).
✅ move: transfer ownership of captured variables into the Closure.
✅ filter_map: clever combination of filtering and transformation.
✅ These tools allow you to write short, fast, human‑readable code – like a real wizard! 🧙
🧠 Sometimes things are hard, and that’s okay!
Closures and chained Iterators might feel confusing at first. Even professional programmers sometimes try several times to build a complex chain. Don’t worry – with practice, these tools will become natural and writing beautiful code will become a joy.
13.6.2. Challenge: Implementing Iterator for Fibonacci
Create a struct called Fibonacci that implements the Iterator trait. Each time next is called, it should return the next number in the Fibonacci sequence (0, 1, 1, 2, 3, 5, 8, …). Then use .take(10) to print the first ten numbers.
💡 Sample answer:
struct Fibonacci {
current: u64,
next: u64,
}
impl Fibonacci {
fn new() -> Self {
Fibonacci { current: 0, next: 1 }
}
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let new_next = self.current + self.next;
let result = self.current;
self.current = self.next;
self.next = new_next;
Some(result)
}
}
fn main() {
let fib = Fibonacci::new();
for num in fib.take(10) {
println!("{}", num);
}
}
take(10) is another Adapter that returns only the first 10 elements of an Iterator. The combination of struct + Trait + Adapter is where Rust truly shines! 🌟
Now you know how to write cleaner, shorter, and more professional code using Iterators and Closures. In the next chapter we’ll visit the big Rust supermarket (Crates.io) and learn how to use ready‑made libraries to build even more powerful programs. 📦🚀
Land of Rust: Ferris the Space Crab’s Adventures
Chapter 14: The Rust Toy Supermarket (Crates.io)
📑 Chapter Index
14.1. Borrowing Other People’s Toys
14.1.1. Story: The Big Toy Store
14.1.2. What is crates.io?
14.1.3. Adding a Dependency to Cargo.toml
14.1.4. Using a Crate in Code
14.2. Searching and Choosing a Crate
14.2.1. Going to crates.io
14.2.2. Reading Documentation on docs.rs
14.2.3. Criteria for Choosing a Good Crate
14.2.4. Example: Using rand
14.3. Version Management (SemVer)
14.3.1. Semantic Versioning
14.3.2. Version Operators in Cargo.toml
14.3.3. Locking Versions with Cargo.lock
14.4. Updating Dependencies
14.4.1. cargo update
14.4.2. cargo outdated (External Tool)
14.5. Creating Our Own Crate
14.5.1. Creating a Library Project
14.5.2. Writing Code in lib.rs
14.5.3. Documentation with /// and //!
14.5.4. Building Local Documentation with cargo doc
14.5.5. Preparing for Publishing (Optional)
14.6. Project: Using ferris-says
14.6.1. Adding ferris-says
14.6.2. Writing a Program with a Crab Frame
14.7. Summary and Challenge
14.7.1. Concept Review
14.7.2. Challenge: Building a Small Crate Called pig_latin
14.1. Borrowing Other People’s Toys
14.1.1. Story: The Big Toy Store
One day Ferris got tired of having to write everything from scratch for every little task (like generating a random number or drawing a simple shape). His friend Bill said: “Why don’t you go to the big toy store for programming? It’s full of ready‑made tools that other people built and shared for free. You can borrow them and get your work done much faster!” 🛍️✨
In the Rust world that store is called crates.io. Each of those ready‑made tools is called a Crate.
Learning to use other people’s libraries means you can stand on the shoulders of giants and build bigger software – that’s the power of a computer wizard! 🧙♂️
👨👩👧 Note for parents and teachers
crates.io is the official repository of Rust libraries. This chapter shows how to use dependencies and manage versions – an essential real‑world skill. The official Rust book has a full chapter on crates.io:
doc.rust-lang.org/book/ch14-00-more-about-cargo.html
14.1.2. What is crates.io?
crates.io is a big website where programmers from all over the world put their code so others can use it. Cargo is like a smart assistant: when you tell it which tool you want, it goes there, downloads it, and attaches it to your project. You don’t have to copy‑paste files yourself! 📦🤖
14.1.3. Adding a Dependency to Cargo.toml
To borrow a Crate, you need to write its name in Cargo.toml. For example, if we want to use rand (which we already learned), open Cargo.toml and under the [dependencies] section write:
[dependencies]
rand = "0.9.0"
Now when you run cargo build, Cargo will go to crates.io, download version 0.9.0, and get it ready for use.
14.1.4. Using a Crate in Code
Now in main.rs we can use it:
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let secret_number = rng.gen_range(1..=100);
println!("Secret number: {}", secret_number);
}
💡 The line use rand::Rng; is very important! It tells Rust: “Please bring the instructions for generating random numbers as well.” If you don’t write this line, the compiler won’t recognise gen_range.
![[Illustration: Cartoon illustration of a friendly crab (Ferris) pushing a shopping cart in a giant digital supermarket. The shelves are filled with glowing boxes labeled with crate names like “rand”, “serde”, “tokio”. A friendly cargo robot scans a box and adds it to the cart. Style: vibrant children’s book, playful tech metaphor, soft lighting, 16:9.]](assets/images/14.1.png)
14.2. Searching and Choosing a Crate
14.2.1. Going to crates.io
Open your browser and go to crates.io. In the search bar type anything you want (e.g., json or image). You will see a list of Crates.
14.2.2. Reading Documentation on docs.rs
Every Crate has a link to its Documentation (Docs), which usually goes to https://docs.rs/crate-name. That is like the instruction manual for a toy: it explains how to install it, what functions it has, and includes ready‑made examples. Always take a look at docs.rs before using a crate! 📖
14.2.3. Criteria for Choosing a Good Crate
Anyone can publish a Crate, so some are better than others. Pay attention to these things:
🔹 Number of downloads: the more downloads, the more people are happy with it.
🔹 Last update: if it hasn’t been updated for years, it might not work with new Rust versions.
🔹 Complete documentation: does it have a simple example? Are functions explained?
🔹 License: make sure the license is open (e.g., MIT or Apache-2.0).
14.2.4. Example: Using rand
Let’s try another example: generating a random RGB colour:
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let red = rng.gen_range(0..=255);
let green = rng.gen_range(0..=255);
let blue = rng.gen_range(0..=255);
println!("Random colour: rgb({}, {}, {})", red, green, blue);
}
Every time you run it, you’ll see a new colour! 🎨
![[Illustration: A cartoon computer screen showing the docs.rs website for the “rand” crate. A magnifying glass hovers over a “Examples” section. Ferris stands beside taking notes with a pencil. Style: clean educational vector, bright colors, clear UI focus, 16:9.]](assets/images/14.2.png)
14.3. Version Management (SemVer)
14.3.1. Semantic Versioning
Version numbers in Rust have three parts: MAJOR.MINOR.PATCH (e.g., 1.2.3). This is a worldwide convention:
🔸 MAJOR: when the first number changes, it means big, incompatible changes have been made (like a game v1 where the rules are completely different from v2).
🔸 MINOR: when the middle number changes, it means new features have been added but old code still works.
🔸 PATCH: when the last number changes, it means only bugs have been fixed – nothing breaks. 🐛➡️✅
14.3.2. Version Operators in Cargo.toml
In Cargo.toml you can be more precise about which versions you accept:
| Writing in TOML | Meaning |
|---|---|
"0.9.0" | exactly version 0.9.0 (same as ^0.9.0) |
"^0.9.0" | any compatible version (i.e., 0.9.x with x >= 0). This is Cargo’s default. |
"~0.9.0" | only 0.9.x (doesn’t allow MINOR bumps). |
"*" | any version! (not recommended – it might suddenly break your program). |
💡 To start, just using "0.9.0" or "0.9" is enough and safe.
14.3.3. Locking Versions with Cargo.lock
The first time you run cargo build, Cargo creates a file called Cargo.lock. This file writes down exactly which version of each Crate was installed today.
If you give your project to a friend, and they also have the Cargo.lock file, they will install exactly the same versions you installed. That way nobody’s program breaks for no reason! 🔒📝
![[Illustration: A cartoon receipt labeled “Cargo.lock” with exact version numbers listed (rand = 0.9.0, ferris-says = 0.3.0). A friendly robot stamps it “LOCKED”. Ferris holds the receipt next to a shopping bag. Style: playful metaphor, educational, soft lighting, 16:9.]](assets/images/14.3.png)
14.4. Updating Dependencies
14.4.1. cargo update
After a while, the Crates you use might get updated. To make Cargo check for and install newer compatible versions, run:
cargo update
This command updates Cargo.lock but does not change Cargo.toml.
14.4.2. cargo outdated (External Tool)
If you’re curious which Crates have new major versions available, you can install a helper tool:
cargo install cargo-outdated
cargo outdated
It will show you a nice table of which dependencies are out‑of‑date. 📊
![[Illustration: A cartoon terminal window showing a colorful table with crate names, current versions, and newer available versions. Ferris points to a row with an upward arrow indicating an update. Style: modern tech illustration, clear and bright, 16:9.]](assets/images/14.4.png)
14.5. Creating Our Own Crate
14.5.1. Creating a Library Project
Now that we’ve learned how to borrow, why don’t we build our own tool? Let’s make a library project (not an executable):
cargo new my_math --lib
cd my_math
This time Cargo creates a src/lib.rs file instead of main.rs. That is the starting point of your library! 📚
14.5.2. Writing Code in lib.rs
In lib.rs we write a few simple functions:
#![allow(unused)]
fn main() {
//! This is a simple library for math operations.
/// Adds two integers together.
///
/// # Example
///
/// ```
/// let result = my_math::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// Subtracts one integer from another.
pub fn subtract(a: i32, b: i32) -> i32 {
a - b
}
}
14.5.3. Documentation with /// and //!
🔹 //! at the beginning of the file: used to describe the whole library.
🔹 /// before a function/struct: used to describe that specific item.
💡 The text inside /// can be Markdown. Even code inside ````rustblocks is tested bycargo test`, so we can be sure our examples always work!
14.5.4. Building Local Documentation with cargo doc
To see your documentation in a browser, just type:
cargo doc --open
A professional‑looking page similar to docs.rs will open, containing your own explanations! Look how nice it is? 🌟
14.5.5. Preparing for Publishing (Optional)
If one day you want to put your library on crates.io so everyone can use it:
- Create an account on crates.io and get an API token.
- In the terminal run
cargo loginand paste the token. - Make sure your
Cargo.tomlhas the necessary information (description,license,authors). - Run the magic command:
cargo publish🚀
(You don’t have to actually publish for practice!)
![[Illustration: A cozy desk setup with a laptop showing a beautiful documentation page generated by cargo doc. Floating around are markdown symbols, function signatures, and a glowing “cargo doc –open” badge. Ferris proudly holds a printed manual. Style: warm, educational children’s book illustration, inviting atmosphere, 16:9.]](assets/images/14.5.png)
14.6. Project: Using ferris-says
14.6.1. Adding ferris-says
Let’s try a fun crate that prints a picture of Ferris with a custom message. In a new project’s Cargo.toml write:
[dependencies]
ferris-says = "0.3"
14.6.2. Writing a Program with a Crab Frame
Write the following code in main.rs:
use ferris_says::say;
use std::io::{stdout, BufWriter};
fn main() {
let message = String::from("Hello friends! I am Ferris 🦀");
let width = message.chars().count();
let mut writer = BufWriter::new(stdout());
say(&message, width, &mut writer).unwrap();
}
When you run cargo run, the output will look something like this:
____________________________
< Hello friends! I am Ferris 🦀 >
----------------------------
\
\
_~^~^~_
\) / o o \ (/
'_ - _'
/ '-----' \
Isn’t that cute? Now you can replace the message with anything you like! 🎤
![[Illustration: A cartoon terminal window displaying the exact ASCII art output of the ferris-says crate. Ferris the crab sits next to the screen waving, with speech bubbles containing the same message. Style: playful, tech-meets-art, bright and cheerful, 16:9.]](assets/images/14.6.png)
14.7. Summary and Challenge
14.7.1. Concept Review
In this chapter you learned:
✅ crates.io: the big store of Rust libraries.
✅ Cargo.toml: where you write your dependencies ([dependencies]).
✅ SemVer: the MAJOR.MINOR.PATCH versioning rule.
✅ Cargo.lock: locking versions to ensure consistent builds.
✅ cargo update and cargo outdated: managing updates.
✅ Building a library: using cargo new --lib and the lib.rs file.
✅ Documentation: using /// and cargo doc.
✅ Sharing your code with others means you become part of the big Rust family – a huge step toward becoming a computer wizard! 🧙
🧠 Sometimes things are hard, and that’s okay!
Managing dependencies and choosing the right version might feel a bit confusing at first. Don’t worry – Cargo does most of the work for you. With a little practice, adding a new crate will become as easy as drinking water.
14.7.2. Challenge: Building a Small Crate Called pig_latin
Create a library named pig_latin that has a public function to_pig_latin(word: &str) -> String. This function should convert an English word to Pig Latin:
🔸 If the word starts with a consonant, move the first letter to the end and add "ay". ("hello" → "ellohay")
🔸 If it starts with a vowel (a, e, i, o, u), just add "hay" to the end. ("apple" → "applehay")
Then create a separate executable project and add your library as a local dependency, then test it with a few words.
💡 Local dependency hint: In the executable’s Cargo.toml write:
[dependencies]
pig_latin = { path = "../pig_latin" }
💡 Sample function answer:
#![allow(unused)]
fn main() {
pub fn to_pig_latin(word: &str) -> String {
let vowels = ['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'];
let mut chars = word.chars();
if let Some(first_char) = chars.next() {
if vowels.contains(&first_char) {
format!("{}-hay", word)
} else {
format!("{}-{}ay", chars.as_str(), first_char)
}
} else {
String::new()
}
}
}
Now you both know how to use ready‑made libraries and how to create your own library to share with others. In the next chapter we’ll meet Smart Pointers – tools like Box, Rc, and RefCell that help us manage data more intelligently. 📦🧠✨
![[Illustration: Ferris wearing a graduation cap and holding a glowing “Chapter 14 Master” badge. Floating around him are a shopping cart, a Cargo.lock receipt, a lib.rs file, and a documentation book. Encouraging, vibrant children’s book illustration, celebratory mood, 16:9.]](assets/images/14.7.png)
Land of Rust: Ferris the Space Crab’s Adventures
Chapter 15: The Secret of Nested Boxes (Smart Pointers)
📑 Chapter Index
15.1. The Gift Box (Box
15.1.1. Story: A Big Present in a Small Box
15.1.2. What is Box
15.1.3. Simple Use of Box
15.1.4. Use Case: Recursive Structures (Linked List)
15.1.5. Exercise: Box for a Trait Object
15.2. The Library’s Shared Book (Rc
15.2.1. Story: A Book Several People Read at the Same Time
15.2.2. What is Rc
15.2.3. Creating Rc and Cloning
15.2.4. Counting References
15.2.5. Limitations: Read‑Only and Single‑Threaded
15.2.6. Exercise: Rc with Multiple Owners
15.2.7. The Memory Leak Problem and the Weak Solution
15.3. The Group Notebook (RefCell
15.3.1. Story: A Notebook Everyone Can Write In
15.3.2. What is RefCell
15.3.3. borrow and borrow_mut
15.3.4. Breaking the Rules at Runtime
15.3.5. Combining Rc and RefCell
15.3.6. Exercise: Rc<RefCell
15.4. Project: A Simple Graph System
15.4.1. Defining a Node
15.4.2. Building a Graph with Rc<RefCell
15.4.3. Adding an Edge
15.4.4. Simple Traversal and Cycle Warning
15.5. Summary and Challenge
15.5.1. Concept Review
15.5.2. Challenge: Doubly‑Linked List with Rc<RefCell
15.1. The Gift Box (Box)
15.1.1. Story: A Big Present in a Small Box
Ferris has a very large, heavy gift that is hard to carry around. 🎁 But he finds a small, light cardboard box. He puts the gift inside the box, and now he only needs to carry that light box!
Inside a computer there are also two storage places:
🔹 Stack (the workbench): very fast, but has limited space. It only holds small things.
🔹 Heap (the big warehouse): plenty of space, but getting in and out is a little slower.
When our data is large, we send it to the Heap and keep only a small address (like a barcode) on the Stack. Box<T> is exactly that cardboard box that does this for us! 📦✨
15.1.2. What is Box?
Box<T> is a smart pointer that takes data of type T and stores it in the Heap. When the Box goes out of scope (for example, when we leave a function), the data inside the Heap is automatically cleaned up.
It has three main uses:
- Storing large data without cluttering the Stack.
- Building recursive structures (like linked lists).
- Holding Trait Objects (which we’ll see later).
15.1.3. Simple Use of Box
fn main() {
let b = Box::new(5); // the number 5 goes to the Heap, its address stays on the Stack
println!("Value inside the box: {}", b); // we use it like a normal number
}
15.1.4. Use Case: Recursive Structures (Linked List)
Suppose we want to build a train where each wagon connects to the next. In Rust we cannot write a struct that directly contains itself (because its size would be infinite!). But with Box (which has a fixed size) there is no problem:
enum List {
Cons(i32, Box<List>), // a number + a pointer to the rest of the train
Nil, // last wagon (empty)
}
use List::{Cons, Nil};
fn main() {
let train = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
15.1.5. Exercise: Box for a Trait Object
Create a trait called Draw with a method draw(&self). Write two structs Circle and Square that implement it. Then create a Vec<Box<dyn Draw>>, put the shapes in it, and call draw on each one.
💡 Answer:
trait Draw { fn draw(&self); }
struct Circle;
impl Draw for Circle { fn draw(&self) { println!("○ Circle drawn!"); } }
struct Square;
impl Draw for Square { fn draw(&self) { println!("□ Square drawn!"); } }
fn main() {
let shapes: Vec<Box<dyn Draw>> = vec![Box::new(Circle), Box::new(Square)];
for shape in shapes { shape.draw(); }
}
![[Illustration: A friendly cartoon crab (Ferris) holding a small glowing gift box labeled “Box<T>”. Inside the box, a larger, heavy golden item sits safely. A conveyor belt moves the box to a large warehouse labeled “Heap”, while a small barcode tag stays on a desk labeled “Stack”. Style: vibrant children’s book illustration, educational metaphor, soft lighting, 16:9.]](assets/images/15.1.png)
15.2. The Library’s Shared Book (Rc)
15.2.1. Story: A Book Several People Read at the Same Time
In Ferris’s library there is a very famous book. Several people want to borrow it at the same time. In normal Rust each value has only one owner, but with Rc<T> (short for Reference Counted) we can have multiple owners at the same time. Rc counts how many people are holding the book. When the last person returns the book, it automatically goes back to the shelf (the memory is freed). 📚🔢
15.2.2. What is Rc?
Rc<T> is a smart pointer with reference counting. Every time you create a new Rc from it, a hidden counter increases by one. When an Rc goes out of scope, the counter decreases. When it reaches zero, the data is freed.
To use it we must bring it into scope: use std::rc::Rc;
15.2.3. Creating Rc and Cloning
#![allow(unused)]
fn main() {
let a = Rc::new(5); // counter = 1
let b = Rc::clone(&a); // counter = 2 (notice: the data is not copied, only the counter increases)
let c = Rc::clone(&a); // counter = 3
}
15.2.4. Counting References
We can see how many borrows exist:
#![allow(unused)]
fn main() {
let book = Rc::new(String::from("Magic Book"));
println!("Number of borrowers: {}", Rc::strong_count(&book)); // 1
let reader = Rc::clone(&book);
println!("Number of borrowers: {}", Rc::strong_count(&book)); // 2
}
15.2.5. Limitations: Read‑Only and Single‑Threaded
⚠️ Rc only allows you to read the data (immutable borrow). If you want to change it, you must combine it with RefCell.
⚠️ Also, Rc is only safe for single‑threaded programs. For multi‑threaded programs we use its cousin Arc.
15.2.6. Exercise: Rc with Multiple Owners
Create a struct called Book with a title field. Create three Rc<Book> that all point to the same book. Print the title and show the reference count.
💡 Answer:
use std::rc::Rc;
struct Book { title: String }
fn main() {
let book = Rc::new(Book { title: String::from("Ferris’s Adventures") });
let r1 = Rc::clone(&book);
let r2 = Rc::clone(&book);
println!("Title: {}", book.title);
println!("Readers: {}", Rc::strong_count(&book)); // 3
}
15.2.7. The Memory Leak Problem and the Weak Solution
Up to here everything is fine, but there is a trap! If we create a cycle with Rc (for example, node A points to B and B points back to A), the counters will never drop to zero. That means the memory of those nodes leaks until the program ends. 😟
To solve this problem, Rust has another smart pointer called Weak<T>. Weak is like Rc but it does not increase the strong count (it has its own weak count). To turn an Rc into a Weak we use Rc::downgrade.
In projects like bidirectional graphs or doubly‑linked lists, we use Weak for one of the directions to break the cycle. Don’t worry – we’ll mention it in the challenge at the end of the chapter and see more of it in later advanced chapters.
![[Illustration: A cartoon library desk with a single glowing book. Three children hold transparent “Rc” cards connected by dotted lines to the book. A small digital counter on the book shows “3”. Ferris stands nearby holding a clipboard, smiling. Style: clean educational vector, bright colors, clear metaphor, 16:9.]](assets/images/15.2.png)
15.3. The Group Notebook (RefCell)
15.3.1. Story: A Notebook Everyone Can Write In
The children in the library have a shared notebook. Everyone can read it, but when someone wants to write in it, they must ensure nobody else is reading or writing at that same moment. In Rust this rule is usually checked at compile time, but RefCell<T> moves that check to runtime. If someone breaks the rule, the program politely stops with a panic!. 📓✍️
15.3.2. What is RefCell?
RefCell<T> is a smart pointer that allows you to mutably borrow what appears to be an immutable piece of data, as long as at runtime there is only one writer at a time.
To use it: use std::cell::RefCell;
15.3.3. borrow and borrow_mut
🔹 .borrow() → returns a normal reference (&T) (read‑only).
🔹 .borrow_mut() → returns a mutable reference (&mut T) (read+write).
#![allow(unused)]
fn main() {
let x = RefCell::new(5);
{
let mut y = x.borrow_mut(); // write lock opened
*y += 10;
} // lock closed
println!("Value: {}", x.borrow()); // 15
}
15.3.4. Breaking the Rules at Runtime
If you try to have two borrow_mut at the same time, the program stops:
#![allow(unused)]
fn main() {
let x = RefCell::new(5);
let a = x.borrow_mut();
let b = x.borrow_mut(); // ❌ panic! two writers at the same time
}
The compiler won’t give an error here, so you have to be careful yourself!
15.3.5. Combining Rc and RefCell
Real magic happens when we combine the two: Rc<RefCell<T>>. Now multiple owners can see the same data and each of them (one at a time) can change it:
#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::cell::RefCell;
use std::ops::AddAssign;
let shared = Rc::new(RefCell::new(0));
Rc::clone(&shared).borrow_mut().add_assign(5); // +5
Rc::clone(&shared).borrow_mut().add_assign(3); // +3
println!("Final: {}", shared.borrow()); // 8
}
15.3.6. Exercise: Rc<RefCell>
Create a number 10 of type Rc<RefCell<i32>>. Write two functions add_two and multiply_by_three that each work on that same number. Finally print the value.
💡 Answer:
fn add_two(num: &Rc<RefCell<i32>>) { *num.borrow_mut() += 2; }
fn multiply_by_three(num: &Rc<RefCell<i32>>) { *num.borrow_mut() *= 3; }
fn main() {
let n = Rc::new(RefCell::new(10));
add_two(&n);
multiply_by_three(&n);
println!("Result: {}", n.borrow()); // 36
}
![[Illustration: A cartoon notebook on a table with a glowing lock icon. One hand holds a blue key labeled “borrow”, another hand waits with a golden key labeled “borrow_mut”. A traffic light shows green for reading, red for simultaneous writing. Ferris explains with a pointer. Style: playful educational illustration, clear visual rules, bright, 16:9.]](assets/images/15.3.png)
15.4. Project: A Simple Graph System
Now let’s put all these boxes together and build a space map! A graph is a set of nodes that can be connected to each other. 🌌🔗
15.4.1. Defining a Node
#![allow(unused)]
fn main() {
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
name: String,
neighbors: Vec<Rc<RefCell<Node>>>,
}
}
15.4.2. Building a Graph with Rc<RefCell>
fn main() {
let a = Rc::new(RefCell::new(Node { name: "Planet A".into(), neighbors: vec![] }));
let b = Rc::new(RefCell::new(Node { name: "Planet B".into(), neighbors: vec![] }));
let c = Rc::new(RefCell::new(Node { name: "Planet C".into(), neighbors: vec![] }));
// A connects to B and C
a.borrow_mut().neighbors.push(Rc::clone(&b));
a.borrow_mut().neighbors.push(Rc::clone(&c));
// B also connects back to A (two‑way)
b.borrow_mut().neighbors.push(Rc::clone(&a));
println!("A’s neighbors: {:?}", a.borrow().neighbors.iter().map(|n| &n.borrow().name).collect::<Vec<_>>());
}
15.4.3. Adding an Edge
We can write a helper function to make connecting nodes easier:
#![allow(unused)]
fn main() {
fn add_edge(from: &Rc<RefCell<Node>>, to: &Rc<RefCell<Node>>) {
from.borrow_mut().neighbors.push(Rc::clone(to));
}
}
15.4.4. Simple Traversal and Cycle Warning
⚠️ Important warning: If the graph contains a cycle, a simple traversal might run forever! For safe traversal we must keep a HashSet of already‑visited nodes.
⚠️ Memory leak: If the graph has a cycle and all connections use Rc, the memory of those nodes will never be freed (the same problem as in section 15.2.7). To build professional graphs we should use Weak for some of the edges. Don’t worry – this chapter is only an introduction to these ideas.
![[Illustration: A star map with glowing planet nodes labeled A, B, C. Glowing lines connect them bidirectionally. Small floating boxes labeled “Rc” and “RefCell” hover near each node, showing data flow. Ferris points to a connection with a space pen. Style: cozy sci-fi workspace, educational vector, soft glow, 16:9.]](assets/images/15.4.png)
15.5. Summary and Challenge
15.5.1. Concept Review
| Tool | Main Use | Important Limitation |
|---|---|---|
Box<T> | Sending data to the Heap, recursive structures | Has only one owner |
Rc<T> | Multiple owners (single‑threaded) | Read‑only; risk of leaks if cycles exist |
Weak<T> | Breaking Rc cycles | Must be upgraded with upgrade to access |
RefCell<T> | Changing data through an immutable reference | Borrow rules checked at runtime |
Rc<RefCell<T>> | Multiple owners + (turn‑based) mutability | Panic on rule violation + risk of cyclic leaks |
15.5.2. Challenge: Doubly‑Linked List with Rc<RefCell> and an Introduction to Weak
Build a doubly‑linked list. Each node should have prev and next fields. Use Option<Rc<RefCell<Node>>>.
💡 Professional tip: In the real world, if you use Rc for both prev and next, you will create a memory leak. The standard solution is to use Weak<RefCell<Node>> for the prev field. The final shape would be:
#![allow(unused)]
fn main() {
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
next: Option<Rc<RefCell<Node>>>,
prev: Option<Weak<RefCell<Node>>>,
}
}
For now, it’s okay to practice with only Rc – but know that for real‑world code you should use Weak. If you feel adventurous, you can replace it right now and access the data with upgrade. 🧠
![[Illustration: Ferris wearing a graduation cap and safety goggles, holding a glowing “Chapter 15 Master” badge. Floating around him are a gift box (Box), a shared book (Rc), a notebook with a lock (RefCell), and a small star map graph. Encouraging, bright lighting, children’s book style, 16:9.]](assets/images/15.5.png)
Land of Rust: Ferris the Space Crab’s Adventures
Chapter 16: Swarms of Ants (Concurrency)
📑 Chapter Index
16.1. How a Legion of Ants Moves a Giant Leaf (Threads)
16.1.1. Story: Ants and the Giant Leaf
16.1.2. What is a Thread?
16.1.3. Creating a Thread with thread::spawn
16.1.4. Waiting with join
16.1.5. Exercise: Two Simultaneous Threads
16.2. Sending Data Between Threads
16.2.1. Transferring Ownership with move
16.2.2. Channel (mpsc) for Sending Messages
16.2.3. Sending Multiple Messages
16.2.4. Non‑blocking Receive with try_recv
16.2.5. Exercise: Producer and Consumer
16.3. The Narrow Hallway (Mutex)
16.3.1. Story: The One‑Way Hallway
16.3.2. What is Mutex<T>?
16.3.3. Locking and Access
16.3.4. Sharing Between Threads with Arc
16.3.5. Exercise: Shared Counter
16.4. Project: Concurrent Counter with Threads
16.4.1. Without a Mutex: Data Race
16.4.2. With a Mutex: The Correct Solution
16.4.3. Run and Observe the Result
16.5. Summary and Challenge
16.5.1. Concept Review
16.5.2. Challenge: Producer‑Consumer with a Queue
16.1. How a Legion of Ants Moves a Giant Leaf (Threads)
16.1.1. Story: Ants and the Giant Leaf
Have you ever noticed that a single ant cannot move a giant leaf? 🍃 But when thousands of ants cooperate, the leaf glides like a light boat on their backs. Each ant grabs a corner and pulls together with the others.
In the computer world, those hard‑working ants are called Threads. Using threads we can do several things at the same time and make our programs faster and more powerful.
Learning concurrency means you can use the real power of multi‑core computers – a huge step toward becoming a computer wizard! 🧙♂️
16.1.2. What is a Thread?
A thread is like a small worker inside your program. Your main program (the main function) is itself a thread (we call it the main thread). You can tell the main thread: “Go hire some new workers and split the work among them!” All these workers can work simultaneously.
In Rust, the std::thread module provides all the tools we need. 🛠️
16.1.3. Creating a Thread with thread::spawn
To create a new thread we use thread::spawn. It takes a Closure (the magic backpack from Chapter 13) and runs it in a separate thread:
use std::thread;
use std::time::Duration;
fn main() {
// hire a new worker
thread::spawn(|| {
for i in 1..10 {
println!("🐜 Ant number {} is working", i);
thread::sleep(Duration::from_millis(100)); // a short rest
}
});
// the main thread works as well
for i in 1..5 {
println!("👑 Main thread: {}", i);
thread::sleep(Duration::from_millis(50));
}
}
thread::sleep makes the thread sleep for a few milliseconds so you can see how they run concurrently.
16.1.4. Waiting with join
If the main thread finishes its work too early, the program might exit and leave the other threads unfinished! To prevent that we use join. join means: “Wait until this thread finishes its work, then move to the next step.”
#![allow(unused)]
fn main() {
let handle = thread::spawn(|| {
println!("New thread started working...");
// long‑running work
});
handle.join().unwrap(); // wait here for the thread to finish
println!("All work done!");
}
16.1.5. Exercise: Two Simultaneous Threads
Create two separate threads, each printing numbers 1 to 5 at different speeds. Use join for both so that the main thread waits for them.
💡 Sample answer:
use std::thread;
use std::time::Duration;
fn main() {
let h1 = thread::spawn(|| {
for i in 1..=5 {
println!("🐜 Ant 1: {}", i);
thread::sleep(Duration::from_millis(80));
}
});
let h2 = thread::spawn(|| {
for i in 1..=5 {
println!("🐜 Ant 2: {}", i);
thread::sleep(Duration::from_millis(50));
}
});
h1.join().unwrap();
h2.join().unwrap();
println!("✅ All ants finished their work!");
}
![[Illustration: Cartoon illustration of a giant leaf being carried by a team of cute worker ants. One ant wears a crown labeled “Main Thread”, others wear small hard hats labeled “Thread 1”, “Thread 2”. Above them, a progress bar fills up. Background: grassy field with soft sunlight. Style: vibrant children’s book, educational metaphor, friendly characters, 16:9.]](assets/images/16.1.png)
👨👩👧 Note for parents and teachers
Concurrency is one of the most advanced topics in programming. This chapter only introduces the basic concepts. If the child struggles withArcorMutex, don’t worry – these tools are used in professional projects and mastering them takes time. The official Rust book has a full chapter on concurrency:
doc.rust-lang.org/book/ch16-00-concurrency.html
16.2. Sending Data Between Threads
16.2.1. Transferring Ownership with move
For ants to know where to carry the leaf, they need to talk to each other. In Rust, when we give a Closure to a thread, we must transfer ownership of its variables to it. We do that with the keyword move:
#![allow(unused)]
fn main() {
let food = vec!["sugar", "bread", "honey"];
let handle = thread::spawn(move || {
println!("The thread is carrying food: {:?}", food);
});
handle.join().unwrap();
// println!("{:?}", food); // ❌ Error! food now belongs to the thread
}
With move, the thread becomes the owner of the data, and the main thread can no longer use it.
16.2.2. Channel (mpsc) for Sending Messages
A great way for threads to talk to each other is a Channel. A channel is like an underground pipe: you can drop something in on one side (the sender) and pick it up on the other side (the receiver).mpsc stands for multiple producer, single consumer – several threads can send messages, but only one thread receives them.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel(); // create the communication pipe
thread::spawn(move || {
let message = String::from("Hello from the ant!");
tx.send(message).unwrap(); // drop into the pipe
});
let received = rx.recv().unwrap(); // take out of the pipe
println!("Message received: {}", received);
}
🔹 tx (transmitter): you can clone it and give it to several threads.
🔹 rx (receiver): the recv method waits until a message arrives.
16.2.3. Sending Multiple Messages
You can send several messages one after another and receive them all with a for loop on the receiver side:
#![allow(unused)]
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let messages = vec!["go up", "go down", "stop!"];
for msg in messages {
tx.send(msg.to_string()).unwrap();
thread::sleep(Duration::from_millis(200));
}
});
for received in rx {
println!("📢 Command: {}", received);
}
}
The for loop on rx continues until all senders are closed.
16.2.4. Non‑blocking Receive with try_recv
If you don’t want the receiver to block (wait) and instead want to see immediately whether a message is available, use try_recv:
#![allow(unused)]
fn main() {
match rx.try_recv() {
Ok(msg) => println!("Message arrived: {}", msg),
Err(_) => println!("No message yet, doing something else..."),
}
}
16.2.5. Exercise: Producer and Consumer
Create a thread that sends numbers 1 through 10 through a channel. The main thread should receive them and compute their sum.
💡 Answer:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
for i in 1..=10 {
tx.send(i).unwrap();
}
});
let sum: i32 = rx.iter().sum();
println!("Sum of numbers: {}", sum); // 55
}
![[Illustration: Cartoon underground pipe system connecting two ant hills. One hill has a sender ant dropping glowing message bottles into the pipe. The other hill has a receiver ant catching them. Labels “tx” and “rx” float above the hills. Style: playful educational vector, bright colors, clear technical metaphor, 16:9.]](assets/images/16.2.png)
16.3. The Narrow Hallway (Mutex)
16.3.1. Story: The One‑Way Hallway
Imagine several ants trying to pass through a very narrow hallway at the same time. If they all go together, they get stuck! What’s the solution? They put a lock on the door. Only one ant can enter at a time, lock the door behind it, do its work, and unlock the door when it leaves so the next one can enter.
In programming, a Mutex (short for Mutual Exclusion) is exactly that hallway lock.
16.3.2. What is Mutex<T>?
Mutex<T> is a smart box that locks its data. To access the data, you must first call lock(). When you’re done, the lock is automatically released.
use std::sync::Mutex;
fn main() {
let counter = Mutex::new(0);
{
let mut num = counter.lock().unwrap(); // get the lock key
*num += 1; // modify the value
} // the lock is released here automatically
println!("Final value: {:?}", counter); // 1
}
16.3.3. Locking and Access
If one thread holds the lock and another thread calls lock(), the second thread blocks (waits) until the lock becomes free. This way, two threads can never modify the same data at the same time, and no data corruption happens. 🛡️
16.3.4. Sharing Between Threads with Arc
To allow multiple threads to access the same Mutex, we need to share ownership of it. In Chapter 15 we met Rc for single‑threaded use. For multiple threads, we use its safe cousin Arc (Atomic Reference Counting – atomic reference counting). Arc works safely in a multi‑threaded environment.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0)); // shared locked notebook
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter); // a copy of the pointer
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap(); // take your turn to write
*num += 1;
});
handles.push(handle);
}
for h in handles { h.join().unwrap(); }
println!("Result: {}", *counter.lock().unwrap()); // 10
}
📌 Golden formula: Arc = sharing ownership between threads, Mutex = turn‑taking for writing. Their combination: Arc<Mutex<T>>!
16.3.5. Exercise: Shared Counter
Write a program that creates 1000 threads. Each thread should increment a shared counter 1000 times. Use Arc<Mutex<u32>>. At the end, the result should be exactly 1,000,000.
💡 Sample answer:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..1000 {
let c = Arc::clone(&counter);
let h = thread::spawn(move || {
for _ in 0..1000 {
let mut val = c.lock().unwrap();
*val += 1;
}
});
handles.push(h);
}
for h in handles { h.join().unwrap(); }
println!("Final result: {}", *counter.lock().unwrap());
}
![[Illustration: A narrow cartoon hallway with a large padlock on the door. One ant holds a golden key labeled “Mutex Guard”, walking inside. Other ants wait politely in line with numbered tickets. A glowing clipboard labeled “Arc” hangs on the wall. Style: clear educational metaphor, bright, friendly, children’s book, 16:9.]](assets/images/16.3.png)
16.4. Project: Concurrent Counter with Threads
16.4.1. Without a Mutex: Data Race
In Rust, if you try to modify shared data between threads without a Mutex, the compiler simply will not let the code compile! For example, Rc is not safe for multi‑threading and using it gives a compile error.
This is one of Rust’s superpowers: it prevents data races at compile time. So you don’t have to worry about weird, hard‑to‑find bugs! 🛡️✨
16.4.2. With a Mutex: The Correct Solution
The code from exercise 16.3.5 is the standard, safe solution. Just make sure to use Arc::clone correctly and store all join handles before waiting for them.
16.4.3. Run and Observe the Result
Run the program several times. You’ll see that it always prints exactly 1000000. The number never changes. That means Rust is doing its job perfectly! 🎉
![[Illustration: A split illustration. Left side: two ants trying to write on the same small notebook at the same time, causing a chaotic mess labeled “Data Race ❌”. Right side: ants taking turns using a clipboard with a lock, result shows a perfect checkmark “1,000,000 ✅”. Ferris stands in the middle giving a thumbs up. Style: educational vector, clear contrast, bright colors, 16:9.]](assets/images/16.4.png)
16.5. Summary and Challenge
16.5.1. Concept Review
| Concept | Use | Emoji |
|---|---|---|
thread::spawn | Create a new thread for concurrent work | 🧵 |
join | Wait for a thread to finish | ⏳ |
move | Transfer ownership of variables into a thread | 🎒 |
mpsc::channel | Safe communication pipe between threads | 📮 |
Mutex<T> | Lock for turn‑based access to data | 🔒 |
Arc<T> | Safe ownership sharing across threads | 🤝 |
🧠 Sometimes things are hard, and that’s okay!
Concurrency (especially combiningArcandMutex) is one of the most challenging topics in programming. Even professional programmers sometimes get stuck with locks and deadlocks. If not everything is clear yet, don’t worry – with practice on small projects you will gradually become comfortable. The important thing is that Rust, with its tools, protects you from the most terrifying concurrency bugs.
16.5.2. Challenge: Producer‑Consumer with a Queue
Write a program that creates 3 producer threads. Each one generates 10 random numbers and sends them through a shared channel. The main thread (the consumer) sums all the numbers and prints the final result.
💡 Hint:
- Clone
txbefore starting the loops. - After all threads are created,
dropthe originaltxto close the channel so therxloop ends. - Add the
randdependency inCargo.toml:[dependencies] rand = "0.9.0"
💡 Sample answer:
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use rand::Rng;
fn main() {
let (tx, rx) = mpsc::channel();
let mut handles = vec![];
for id in 0..3 {
let tx_clone = tx.clone();
let h = thread::spawn(move || {
let mut rng = rand::thread_rng();
for _ in 0..10 {
let num = rng.gen_range(1..100);
tx_clone.send(num).unwrap();
thread::sleep(Duration::from_millis(10));
}
});
handles.push(h);
}
drop(tx); // close the main sender
let sum: i32 = rx.iter().sum();
println!("Final sum: {}", sum);
for h in handles { h.join().unwrap(); }
}
Now you know how to use the power of ants (threads) to do several things at once, how to send messages between them, and how to protect your data with locks. 🐜⚡
In the next chapter we’ll see how Rust implements object‑oriented ideas in its own unique way. Ready? 🦀✨
![[Illustration: Ferris wearing a graduation cap and safety goggles, holding a glowing “Chapter 16 Master” badge. Floating around him are thread spools, a channel pipe, a mutex lock, and an Arc clipboard. Encouraging, bright lighting, children’s book style, 16:9.]](assets/images/16.5.png)
Land of Rust: Ferris the Space Crab’s Adventures
Chapter 17: Is Rust a Transformer Robot? (Object‑Oriented Programming, Rust Style)
📑 Chapter Index
17.1. Traditional Robots (Inheritance)
17.1.1. Story: Robot Father and Robot Son
17.1.2. Inheritance in Other Languages (e.g., Java)
17.1.3. Problems with Inheritance
17.2. The Rust Way (Composition & Traits)
17.2.1. Composition Instead of Inheritance
17.2.2. Sharing Behaviour with Traits
17.2.3. How Rust Differs from Traditional Inheritance
17.2.4. Exercise: The Sound Trait and Implementing It for Several Types
17.3. Project: Simulating a Simple Game
17.3.1. Defining the Attack Trait
17.3.2. Defining the Health Struct
17.3.3. Defining the Warrior, Mage, Archer Structs
17.3.4. Implementing Attack for Each
17.3.5. Using Trait Objects (Box<dyn Attack>)
17.4. Summary and Challenge
17.4.1. Concept Review
17.4.2. Challenge: The Movable Trait
17.1. Traditional Robots (Inheritance)
17.1.1. Story: Robot Father and Robot Son
In many toy factories, when they want to build a new robot, they first create a “father robot” that knows basic actions like walking and talking. Then they say: “The son robot knows all those things as well – it just also has wings!” This way, the child inherits all the father’s features. In programming this is called Inheritance. 🤖👨👦
17.1.2. Inheritance in Other Languages (e.g., Java)
In languages like Java or C++, this is very common:
// hypothetical Java example
class Robot {
void walk() { /* walking */ }
}
class FlyingRobot extends Robot {
void fly() { /* flying */ }
}
Now FlyingRobot has both walk (because it inherited from its parent) and fly. At first glance it looks great, but…
17.1.3. Problems with Inheritance
Inheritance has several big problems, and Rust deliberately avoids them:
🔸 Rigid hierarchy: If later we want a robot that both flies and swims, we would need to inherit from two parents, which causes confusion (the diamond problem!).
🔸 Unwanted inheritance: If a “Penguin” inherits from “Bird”, it also gets the fly method, even though penguins cannot fly!
🔸 Fragility: If the engineer makes a small change to the father robot, the son robot’s code might suddenly break.
Rust says: “Instead of a rigid hierarchy, let’s work with Lego blocks (Composition) and skill certificates (Traits)!” 🧩✨
This approach means that instead of blind copying, you can combine behaviours like pieces from a toolbox – that’s what a computer wizard does. 🧙♂️
![[Illustration: A split cartoon comparison. Left side: a rigid, tangled family tree of robots labeled “Inheritance (Messy)”. Right side: neat Lego blocks labeled “Composition” being snapped together. Ferris the crab points happily at the Lego side. Style: educational vector illustration, bright colors, clear visual metaphor, 16:9.]](assets/images/17.1.png)
17.2. The Rust Way (Composition & Traits)
17.2.1. Composition Instead of Inheritance
In Rust, instead of saying “A combat robot is a kind of robot”, we say “A combat robot has an engine, a sword, and a shield.” This is called Composition:
#![allow(unused)]
fn main() {
struct Engine; // engine
struct Sword; // sword
struct Shield; // shield
struct CombatRobot {
engine: Engine,
weapon: Sword,
shield: Shield,
}
}
If later we want a flying robot, we simply add a Wing component. There is no need to change a whole hierarchy. Composition is like building toys with Lego – complete freedom! 🧱
17.2.2. Sharing Behaviour with Traits
But what if several different robots need to perform a common action? For example, all of them should be able to sound an alarm? That’s where Traits come in. A trait is like a skill certificate that says: “This robot knows how to sound an alarm.”
#![allow(unused)]
fn main() {
trait MakeSound {
fn make_sound(&self);
}
struct Dog;
impl MakeSound for Dog {
fn make_sound(&self) { println!("Woof! Woof!"); }
}
struct Car;
impl MakeSound for Car {
fn make_sound(&self) { println!("Beep beep!"); }
}
}
Now each of them makes a sound in its own way, but both have the MakeSound certificate! 🏅
17.2.3. How Rust Differs from Traditional Inheritance
🔹 In Rust, you can implement as many traits as you want for a single type.
🔹 There is no mandatory hierarchy.
🔹 It is always clear what capability each piece has, instead of inheriting a large, confusing bundle.
17.2.4. Exercise: The Sound Trait and Implementing It for Several Types
Define a trait called Sound with a method make_sound(&self). Implement it for Cat, Cow, and Phone. Then put them in a Vec<Box<dyn Sound>> and make them all make their sounds.
💡 Sample answer:
trait Sound { fn make_sound(&self); }
struct Cat; impl Sound for Cat { fn make_sound(&self) { println!("Meow! 🐱"); } }
struct Cow; impl Sound for Cow { fn make_sound(&self) { println!("Moooo! 🐮"); } }
struct Phone; impl Sound for Phone { fn make_sound(&self) { println!("Ring ring! 📱"); } }
fn main() {
let things: Vec<Box<dyn Sound>> = vec![
Box::new(Cat),
Box::new(Cow),
Box::new(Phone),
];
for thing in things { thing.make_sound(); }
}
![[Illustration: A cartoon quality control desk. A friendly robot inspector stamps glowing badges labeled “Sound”, “Fly”, “Swim” onto different cute characters (cat, cow, phone). Ferris stands beside holding a checklist, smiling. Style: clean educational cartoon, bright colors, clear visual metaphor, 16:9.]](assets/images/17.2.png)
17.3. Project: Simulating a Simple Game
Now let’s test all this in a small role‑playing game! 🎮⚔️
17.3.1. Defining the Attack Trait
First we create a certificate that says “anything that has this can attack”:
#![allow(unused)]
fn main() {
trait Attack {
fn attack(&self, target: &mut Health);
}
}
17.3.2. Defining the Health Struct
A simple structure for health (Health) that holds an hp number and can take damage:
#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Health { hp: i32 }
impl Health {
fn new(hp: i32) -> Self { Health { hp } }
fn take_damage(&mut self, damage: i32) {
self.hp -= damage;
if self.hp < 0 { self.hp = 0; }
}
fn is_alive(&self) -> bool { self.hp > 0 }
}
}
17.3.3. Defining the Warrior, Mage, Archer Structs
We create three different heroes:
#![allow(unused)]
fn main() {
struct Warrior { power: i32 }
struct Mage { mana: i32 }
struct Archer { arrows: i32 }
}
17.3.4. Implementing Attack for Each
Each one attacks in its own style:
#![allow(unused)]
fn main() {
impl Attack for Warrior {
fn attack(&self, target: &mut Health) {
println!("⚔️ Warrior strikes with {} power!", self.power);
target.take_damage(self.power);
}
}
impl Attack for Mage {
fn attack(&self, target: &mut Health) {
let damage = self.mana / 2;
println!("🔮 Mage casts a spell for {} damage!", damage);
target.take_damage(damage);
}
}
impl Attack for Archer {
fn attack(&self, target: &mut Health) {
let damage = self.arrows;
println!("🏹 Archer shoots {} arrows!", damage);
target.take_damage(damage);
}
}
}
17.3.5. Using Trait Objects (Box<dyn Attack>)
Now we want to have an army of different heroes and attack an enemy. Because the sizes of Warrior, Mage, and Archer are different, we cannot put them directly into a single array. The solution? Use a Trait Object and Box:
fn main() {
let mut enemy = Health::new(80); // enemy with 80 HP
let heroes: Vec<Box<dyn Attack>> = vec![
Box::new(Warrior { power: 25 }),
Box::new(Mage { mana: 40 }),
Box::new(Archer { arrows: 15 }),
];
for hero in heroes {
hero.attack(&mut enemy);
println!("Enemy status: {:?}", enemy);
if !enemy.is_alive() {
println!("💀 Enemy defeated!");
break;
}
}
}
📌 Important note: dyn Attack means “in this box you can put anything that implements the Attack trait”. Box moves it to the heap so its size no longer matters. This way we can keep different types in one list and loop over them! 🎉
![[Illustration: Cartoon RPG battle scene. Three heroes (Warrior, Mage, Archer) stand on one side, each holding a glowing “Box<dyn Attack>” badge. On the other side, a cartoon monster has a health bar showing 80 → 55 → 35. Ferris acts as a referee holding a flag. Style: vibrant, dynamic children’s book illustration, 16:9.]](assets/images/17.3.png)
17.4. Summary and Challenge
17.4.1. Concept Review
✅ Rust does not have class‑based inheritance.
✅ Instead, it uses Composition for data and Traits for behaviour.
✅ Box<dyn Trait> allows us to store different types that share a common trait together in one collection.
✅ This approach makes programs more flexible, safer, and cleaner than traditional inheritance.
🧠 Sometimes things are hard, and that’s okay!
Understanding the difference between composition and inheritance can feel a bit abstract at first. Even experienced developers sometimes debate which approach is better. The important thing is that Rust gives you freedom without forcing you into rigid hierarchies. With practice on small projects, this concept will become clear.
17.4.2. Challenge: The Movable Trait
Define a trait called Movable with a method move_forward(&self). Implement this trait for three structs: Bicycle, Car, and Boat (e.g., a bicycle spins its wheels, a car starts its engine, a boat rows its oars). Then write a function start_journey that takes a slice of &dyn Movable and makes each one move forward.
💡 Sample answer:
trait Movable { fn move_forward(&self); }
struct Bicycle; impl Movable for Bicycle { fn move_forward(&self) { println!("🚲 Wheels are spinning..."); } }
struct Car; impl Movable for Car { fn move_forward(&self) { println!("🚗 Engine started, let's go!"); } }
struct Boat; impl Movable for Boat { fn move_forward(&self) { println!("⛵ Oars dip into the water..."); } }
fn start_journey(things: &[&dyn Movable]) {
for thing in things { thing.move_forward(); }
}
fn main() {
let bike = Bicycle; let car = Car; let boat = Boat;
let travelers: [&dyn Movable; 3] = [&bike, &car, &boat];
start_journey(&travelers);
}
Now you understand how Rust implements object‑oriented ideas in a safe and flexible way, without traditional inheritance. In the next chapter we will meet advanced pattern matching and learn how to extract data from nested structures like a professional. 🕵️♂️✨
![[Illustration: Ferris wearing a graduation cap and holding a glowing “Chapter 17 Master” badge. Floating around him are Lego blocks (composition), trait certificates, a Box<dyn Trait>, and a small RPG sword. Encouraging, bright lighting, children’s book style, 16:9.]](assets/images/17.4.png)
Land of Rust: Ferris the Space Crab’s Adventures
Chapter 18: Complex Treasure Maps (Advanced Pattern Matching)
📑 Chapter Index
18.1. Difficult Puzzles: Extracting from Inside Structures
18.1.1. Story: Nested Boxes
18.1.2. Destructuring Structs
18.1.3. Destructuring Enums
18.1.4. Destructuring Tuples
18.1.5. Destructuring in for Loops
18.1.6. Exercise: Destructuring a Nested Struct
18.2. Negation Patterns and Conditions
18.2.1. Ignoring with _ and ..
18.2.2. Conditions in Patterns (Match Guards)
18.2.3. The @ Operator for Binding
18.2.4. | Patterns (OR)
18.2.5. Exercise: Match Guard for Numbers
18.3. Project: Parsing Game Commands
18.3.1. Defining the Command Enum
18.3.2. The parse_command Function
18.3.3. Executing Commands with match and Guards
18.4. Summary and Challenge
18.4.1. Concept Review
18.4.2. Challenge: Extracting from Option<Vec<i32>>
18.1. Difficult Puzzles: Extracting from Inside Structures
18.1.1. Story: Nested Boxes
Ferris has found an old, dusty treasure map. 🗺️✨ But the map has a secret: the treasure is inside a box, that box is inside a chest, the chest is in a big vault, and the key to the vault is with a space bird! Ferris cannot open all those layers one by one – it would take too much time. Luckily, Ferris has a magical power: Pattern Matching. With one swift move, he can break through the whole structure and pull the treasure straight out of the boxes. In Rust, this is called Destructuring. Don’t worry – nothing is destroyed! It just means we open up a structure to reach its inner data.
This means you can act like a professional detective, take apart any complicated structure, and reach the treasure inside – that’s the power of a computer wizard! 🧙♂️
![[Illustration: A cartoon treasure map on a wooden desk. Glowing nested boxes (small box inside medium box inside big chest) float above the map. A pair of glowing magic scissors labeled “Destructuring” cuts through the layers, revealing a shiny gold coin inside. Ferris the crab watches with excited eyes, holding a magnifying glass. Style: whimsical children’s book illustration, bright, adventurous, 16:9.]](assets/images/18.1.png)
👨👩👧 Note for parents and teachers
Advanced patterns are one of Rust’s most powerful features, helping children elegantly extract data from structures. If the child struggles with@or_, don’t worry – they will be used many times in later projects. The official Rust book has a whole chapter on patterns:
doc.rust-lang.org/book/ch18-00-patterns.html
18.1.2. Destructuring Structs
Suppose you have a struct called Point that holds the coordinates of a point. You can unpack its fields into new variables in a single line:
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 10, y: 20 };
// full syntax: field name, colon, new variable name
let Point { x: a, y: b } = p;
println!("a = {}, b = {}", a, b);
// shorthand: if the variable name matches the field name
let Point { x, y } = p;
println!("x = {}, y = {}", x, y);
}
If you only need one field and want to ignore the rest, you can use ..:
#![allow(unused)]
fn main() {
let Point { x, .. } = p; // takes only x, ignores y
println!("only x = {}", x);
}
18.1.3. Destructuring Enums
In Chapter 6 you learned that an enum can have different variants. match is the best tool for opening these variants. Let’s look at a space‑message enum:
#![allow(unused)]
fn main() {
enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // an anonymous struct
Write(String), // a string
ChangeColor(u8, u8, u8), // a tuple
}
fn process(msg: Message) {
match msg {
Message::Quit => println!("👋 Quit program"),
Message::Move { x, y } => println!("🚀 Move to ({}, {})", x, y),
Message::Write(text) => println!("📝 Writing: {}", text),
Message::ChangeColor(r, g, b) => {
println!("🎨 Change colour to ({}, {}, {})", r, g, b);
}
}
}
}
Look how easily we extracted the nested data! match understands the shape of each variant and gives you exactly what is inside.
18.1.4. Destructuring Tuples
Tuples are also very easy to destructure:
#![allow(unused)]
fn main() {
let t = (1, 2, 3);
let (x, y, z) = t;
println!("x={}, y={}, z={}", x, y, z);
}
You can even destructure a tuple directly in a function’s parameters:
#![allow(unused)]
fn main() {
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Coordinates: ({}, {})", x, y);
}
}
18.1.5. Destructuring in for Loops
This feature is very useful in for loops, especially when working with HashMap:
#![allow(unused)]
fn main() {
let points = vec![(0, 0), (1, 2), (3, 4)];
for (x, y) in points {
println!("Next point: ({}, {})", x, y);
}
}
Each iteration pulls a tuple from the list, and (x, y) neatly assigns its values. It’s not magic, just Rust being smart! 😉
18.1.6. Exercise: Destructuring a Nested Struct
Define a struct called Person with fields name: String and address: Address. Address itself is a struct with fields city: String and zip: u32. Create an instance of Person and in one destructuring line extract both name and city directly, then print them.
💡 Sample answer:
struct Address {
city: String,
zip: u32,
}
struct Person {
name: String,
address: Address,
}
fn main() {
let person = Person {
name: String::from("Ferris"),
address: Address {
city: String::from("Crab City"),
zip: 12345,
},
};
// nested destructuring: take name directly, and from address only take city
let Person { name, address: Address { city, .. } } = person;
println!("{} lives in the city of {}.", name, city);
}
💡 Notice: address: Address { city, .. } means “go into the address field, treat it as an Address, unpack it, but only keep city, ignore the rest.”
![[Illustration: A 3D puzzle cube being taken apart layer by layer. Each layer reveals a smaller cube inside, finally revealing a tiny star. Ferris stands on a stack of books, wearing a detective hat and holding a magnifying glass. Style: educational vector, bright colors, clear visual metaphor, 16:9.]](assets/images/18.2.png)
18.2. Negation Patterns and Conditions
Sometimes during pattern matching, we want to ignore certain things or match only when an extra condition holds. This is where the magic of match becomes complete! 🪄
18.2.1. Ignoring with _ and ..
🔹 _ (underscore): acts like a trash can for patterns – anything placed there is ignored.
🔹 .. : ignores the rest of the fields in a struct or the rest of the elements in a tuple/array.
#![allow(unused)]
fn main() {
struct Point3D { x: i32, y: i32, z: i32 }
let p = Point3D { x: 1, y: 2, z: 3 };
let Point3D { x, .. } = p; // only x matters, y and z are ignored
let numbers = (1, 2, 3, 4, 5);
let (first, .., last) = numbers; // only the first and last
println!("first: {}, last: {}", first, last);
}
18.2.2. Conditions in Patterns (Match Guards)
Sometimes a pattern alone is not enough. For example, you want to say “if the number is even” or “if the string length is greater than 5”. We do this with a Match Guard: an if that comes right after the pattern.
#![allow(unused)]
fn main() {
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("{} is an even number!", x),
Some(x) => println!("{} is an odd number.", x),
None => println!("There is no number."),
}
}
⚠️ Order matters! If you write Some(x) without the if first, that arm will always match and the guard will never be checked.
18.2.3. The @ Operator for Binding
The @ sign is a super tool: while checking a pattern, it also stores the whole matched value in a variable.
For example, we want to know if a number is in the range 1..=10, and at the same time keep the number itself:
#![allow(unused)]
fn main() {
let x = 5;
match x {
num @ 1..=10 => println!("{} is in the range 1 to 10.", num),
_ => println!("{} is out of range.", x),
}
}
Here num becomes the value of x, but only if it falls inside the range 1..=10.
18.2.4. | Patterns (OR)
If several patterns should execute the same code, you don’t have to write separate arms. Combine them with | (vertical bar):
#![allow(unused)]
fn main() {
let x = 2;
match x {
1 | 2 => println!("One of the numbers 1 or 2."),
3 => println!("Number 3."),
_ => println!("Something else."),
}
}
18.2.5. Exercise: Match Guard for Numbers
Write a program that reads a number from the user. Using match and a guard, determine:
- If the number is between 1 and 10 → print “small”.
- If the number is between 11 and 20 → print “medium”.
- Otherwise → print “big or out of range”.
💡 Sample answer:
use std::io;
fn main() {
let mut input = String::new();
println!("Enter a number:");
io::stdin().read_line(&mut input).unwrap();
let num: i32 = input.trim().parse().unwrap();
match num {
n if (1..=10).contains(&n) => println!("Number {} is small. 👶", n),
n if (11..=20).contains(&n) => println!("Number {} is medium. 👦", n),
_ => println!("Number {} is big or not in range. 🌳", num),
}
}
💡 (1..=10).contains(&n) is a clean way to check membership in a range!
18.3. Project: Parsing Game Commands
Now it’s time to test all these tools in a real project! 🎮 Let’s build a simple command system for a text‑based game. The user types commands like /go north or /attack dragon 50, and the program understands and executes them.
18.3.1. Defining the Command Enum
First we define an enum for the possible command types:
#![allow(unused)]
fn main() {
enum Command {
Go { direction: String },
Attack { target: String, power: u32 },
Quit,
}
}
18.3.2. The parse_command Function
We write a function that splits the input string (by whitespace) and returns a Command. If the input is empty or has the wrong format, we treat it as Quit.
#![allow(unused)]
fn main() {
fn parse_command(input: &str) -> Command {
let parts: Vec<&str> = input.trim().split_whitespace().collect();
// if the user didn't enter anything
if parts.is_empty() {
return Command::Quit;
}
// if the user typed "/go north"
if parts[0] == "/go" && parts.len() >= 2 {
Command::Go {
direction: parts[1].to_string(),
}
}
// if the user typed "/attack dragon 50"
else if parts[0] == "/attack" && parts.len() >= 3 {
let power = parts[2].parse().unwrap_or(10); // if not a number, default to 10
Command::Attack {
target: parts[1].to_string(),
power,
}
}
// otherwise, "/quit" or anything else
else {
Command::Quit
}
}
}
18.3.3. Executing Commands with match and Guards
Now in main we run a loop to read commands and execute them. We use match with a guard to show a special message for very powerful attacks:
use std::io;
use std::io::Write; // for flush
fn main() {
println!("🎮 Welcome to Ferris’s text game!");
loop {
print!("\nType your command (/go, /attack, /quit): ");
io::stdout().flush().unwrap(); // make sure the message prints immediately
let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();
let cmd = parse_command(&input);
match cmd {
Command::Go { direction } => {
println!("🚀 Ferris moves towards {}.", direction);
}
Command::Attack { target, power } if power > 30 => {
println!("💥 Devastating attack! {} is obliterated with {} power!", target, power);
}
Command::Attack { target, power } => {
println!("⚔️ Attacked {} with {} power. (Still alive!)", target, power);
}
Command::Quit => {
println!("👋 Goodbye! The game is over.");
break;
}
}
}
}
Look how clean it became! No more nested if/else. match opens everything like a treasure map. 🗝️
![[Illustration: A retro computer terminal screen showing text commands: “/go north”, “/attack dragon 50”, “/quit”. Next to the screen, a cartoon joystick with buttons labeled “Match”, “Guard”, “Enum”. Ferris sits at the keyboard wearing a gaming headset, smiling confidently. Style: cozy gaming setup, vibrant, children’s book illustration, 16:9.]](assets/images/18.3.png)
18.4. Summary and Challenge
18.4.1. Concept Review
In this chapter you learned:
✅ Destructuring: extracting fields from structs, enums, and tuples using patterns.
✅ _ and ..: ignoring parts of a pattern when you don’t need them.
✅ Match Guards: adding an if condition to a match arm.
✅ @ Binding: storing a matched value in a variable while checking the pattern.
✅ | (OR): combining several patterns in one arm to avoid repetition.
✅ These tools turn you into a data detective – a true computer wizard! 🧙
🧠 Sometimes things are hard, and that’s okay!
Nested destructuring and match guards might feel a bit complex at first. Don’t worry – the more you use them in your code, the more natural they become. Even professional programmers sometimes try several times to write a complicated pattern. The important thing is that Rust always tells you where you made a mistake.
18.4.2. Challenge: Extracting from Option<Vec<i32>>
Write a function called sum_first_two that takes an Option<Vec<i32>> as input and:
- If it is
Someand the vector has at least 3 elements, return the sum of the first two elements asSome(i32). - If it is
Somebut has fewer than 3 elements, returnNone. - If it is
None, returnNone.
💡 Hint: You can use a nested match or a match guard.
💡 Sample answer:
fn sum_first_two(opt: Option<Vec<i32>>) -> Option<i32> {
match opt {
Some(vec) if vec.len() >= 3 => Some(vec[0] + vec[1]),
_ => None, // covers both Some with short length and None
}
}
fn main() {
let v1 = Some(vec![10, 20, 30, 40]);
let v2 = Some(vec![5, 15]);
let v3 = None;
println!("{:?}", sum_first_two(v1)); // Some(30)
println!("{:?}", sum_first_two(v2)); // None
println!("{:?}", sum_first_two(v3)); // None
}
🔚 End of Chapter 18
Now you are a pattern‑matching master! You can unpack the most complex data structures like a pro. 🧩✨
In the next chapter we’ll visit the engine room of Rust and explore unsafe and macros – tools used only by professional, adventurous heroes! Are you ready for some (safe) black magic? 🌙🔧
![[Illustration: Ferris wearing a graduation cap and holding a glowing “Chapter 18 Master” badge. Floating around him are nested puzzle boxes, match guard shields, and a joystick controller. Background: a starry night sky with a subtle map overlay. Encouraging, vibrant children’s book style, 16:9.]](assets/images/18.4.png)
Land of Rust: Ferris the Space Crab’s Adventures
Chapter 19: Opening the Spaceship Engine (Unsafe Rust and Macros)
📑 Chapter Index
19.1. Serious Warning: This Is the Engine Room (unsafe)
19.1.1. Story: The Hot Spaceship Engine
19.1.2. What Is unsafe?
19.1.3. What Can You Do in unsafe?
19.1.4. Example: Raw Pointers
19.1.5. Calling an unsafe Function
19.1.6. Exercise: Modifying a Raw Pointer
19.2. Making Your Own Magic (Macros)
19.2.1. Story: Ferris’s Magic Word
19.2.2. What Is a Macro and How Is It Different from a Function?
19.2.3. The Simplest Macro with macro_rules!
19.2.4. Macros with Parameters (ident)
19.2.5. Macros with a Variable Number of Arguments
19.2.6. Commonly Used Macros in Rust
19.2.7. Exercise: The easy_vec! Macro
19.3. Project: Building the repeat! Macro
19.3.1. Goal
19.3.2. Implementing repeat!
19.3.3. Testing the Macro
19.4. Summary and Challenge
19.4.1. Concept Review
19.4.2. Challenge: The create_function! Macro
19.1. Serious Warning: This Is the Engine Room (unsafe)
19.1.1. Story: The Hot Spaceship Engine
Deep inside Ferris’s spaceship there is a heavy metal door with red writing: ⛔ Danger! Senior engineers only. Behind this door works the main engine. The temperature is extremely high, and if someone enters without training, everything might explode! 💥
In the Rust world, most of the time we work in the safe parts (Safe Rust), where the compiler acts like a kind guard and checks all the rules so we don’t make mistakes. But sometimes, for very special tasks (like talking directly to hardware or using old C libraries), we have to enter the unsafe room.
Learning unsafe means you understand where Rust’s real power lies – but like a wise wizard, you only use it when absolutely necessary. 🧙♂️
👨👩👧 Note for parents and teachers
unsafeis one of the most advanced topics in Rust and is rarely used in ordinary programs. The goal of this chapter is to introduce its existence, not to encourage its use. The official Rust book has a full chapter onunsafe:
doc.rust-lang.org/book/ch19-01-unsafe-rust.html
19.1.2. What Is unsafe?
unsafe is a keyword that tells the compiler: “Friend, I’ll take care of memory safety here myself. Please turn off your checks so I can do my job.”
⚠️ Golden tip: unsafe does not mean “unsafe”! It means the compiler won’t enforce its usual rules, but you must act like a professional engineer and make sure you don’t corrupt the data. Only use it when you really have no other choice!
#![allow(unused)]
fn main() {
unsafe {
// here we can do low‑level things
}
}
19.1.3. What Can You Do in unsafe?
Inside an unsafe block, five things become allowed that are forbidden in normal code:
🔹 Dereferencing raw pointers (*const T and *mut T)
🔹 Calling unsafe functions (usually C functions)
🔹 Modifying mutable static variables (static mut)
🔹 Implementing unsafe traits
🔹 Accessing fields of a union
19.1.4. Example: Raw Pointers
In normal Rust we use & and &mut, which are always safe and valid. But raw pointers (*const T and *mut T) can point to any address (even zero or null). The compiler cannot check whether that address is valid, so we are only allowed to read or write through them inside unsafe:
fn main() {
let mut num = 5;
// creating raw pointers (this is not unsafe, we are just taking addresses)
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// reading or writing through a raw pointer must be inside unsafe
unsafe {
println!("Value of r1: {}", *r1); // 5
*r2 = 10; // change the value through the pointer
println!("New value of r2: {}", *r2); // 10
}
}
19.1.5. Calling an unsafe Function
Some functions in Rust are marked unsafe. To call them you must be inside an unsafe block. A common use is communicating with C code (called FFI):
extern "C" {
fn abs(input: i32) -> i32; // absolute value function from the C library
}
fn main() {
let x = -5;
unsafe {
println!("The absolute value of {} is {}", x, abs(x)); // 5
}
}
19.1.6. Exercise: Modifying a Raw Pointer
Create a mutable i32 variable with the value 42. Create a raw pointer *mut i32 to it. Inside an unsafe block, change its value to 100 and print it.
💡 Sample answer:
fn main() {
let mut value = 42;
let raw_ptr = &mut value as *mut i32;
unsafe {
*raw_ptr = 100;
println!("New value: {}", *raw_ptr);
}
}
![[Illustration: Cartoon spaceship engine room with glowing warning signs labeled “unsafe”. Ferris wears a protective engineer suit and helmet, carefully turning a glowing valve labeled “*mut T”. Steam and soft blue energy fill the room. Style: dramatic but child-friendly, high contrast, vibrant children’s book illustration, 16:9.]](assets/images/19.1.png)
19.2. Making Your Own Magic (Macros)
19.2.1. Story: Ferris’s Magic Word
Ferris got tired of having to write long repetitive commands every day. For example, every time he wants to build a vector with a few numbers, he has to write Vec::new() and call push several times. One day he found a magic spellbook that taught him how to create his own spells. In Rust these spells are called Macros! ✨📖
19.2.2. What Is a Macro and How Is It Different from a Function?
| Feature | Function | Macro |
|---|---|---|
| When it runs | at runtime | at compile‑time |
| Main job | perform operations and calculations | automatically generate Rust code |
| Number of inputs | fixed and known | can be variable and arbitrary |
| Mark | plain name | ends with ! (println!) |
Macros are like a smart copy‑paste factory that write code for you before your program is built!
19.2.3. The Simplest Macro with macro_rules!
To create a macro we use macro_rules!:
macro_rules! say_hello {
() => {
println!("Hello from the magic macro! 🦀");
};
}
fn main() {
say_hello!(); // at compile time this becomes println!
}
() means “if you call the macro with no arguments, replace it with the code on the right”.
19.2.4. Macros with Parameters (ident)
We can give input to a macro. For example, take a function name and let the macro build that function:
macro_rules! create_function {
($name:ident) => {
fn $name() {
println!("Function {} was called! ✨", stringify!($name));
}
};
}
create_function!(foo);
create_function!(bar);
fn main() {
foo(); // prints: Function foo was called!
bar(); // prints: Function bar was called!
}
$name:ident means “take a valid identifier (like a variable or function name)”. stringify! turns the name into a string.
19.2.5. Macros with a Variable Number of Arguments
This is where the real magic of macros lies: they can take any number of inputs! With $($x:expr),* we say “zero or more expressions separated by commas”:
macro_rules! sum {
($($x:expr),*) => {
{
let mut total = 0;
$(total += $x;)* // this line repeats for each input
total
}
};
}
fn main() {
let s = sum!(1, 2, 3, 4);
println!("Sum: {}", s); // 10
}
$(...)* means “repeat the pattern inside the parentheses as many times as there are inputs”.
19.2.6. Commonly Used Macros in Rust
| Macro | Use |
|---|---|
println!(...) | print to the terminal |
vec![...] | quickly build a vector |
format!(...) | build a formatted string |
assert!(...) | check a condition in tests |
dbg!(...) | quickly print a value for debugging |
19.2.7. Exercise: The easy_vec! Macro
Create a macro that works exactly like vec!: it takes a list of values and returns a Vec.
💡 Answer:
macro_rules! easy_vec {
($($x:expr),* $(,)?) => {
{
let mut v = Vec::new();
$(v.push($x);)*
v
}
};
}
fn main() {
let v = easy_vec![10, 20, 30];
println!("{:?}", v); // [10, 20, 30]
}
💡 $(,)? means “it’s okay if you leave an extra comma at the end of the list”.
![[Illustration: A cartoon wizard crab (Ferris) holding a glowing spellbook labeled “macro_rules!”. Floating magical symbols like “$x:expr” and “=>” transform into actual Rust code blocks. Background: cozy library with starry windows. Style: whimsical, educational children’s book illustration, soft lighting, 16:9.]](assets/images/19.2.png)
19.3. Project: Building the repeat! Macro
19.3.1. Goal
We will build a macro that repeats a command several times. For example:
#![allow(unused)]
fn main() {
repeat!(println!("Hello! 🦀"), 3);
}
and get the output:
Hello! 🦀
Hello! 🦀
Hello! 🦀
19.3.2. Implementing repeat!
To allow the macro to repeat full statements (like let x = 5;), we use stmt instead of expr:
#![allow(unused)]
fn main() {
macro_rules! repeat {
($cmd:stmt, $count:expr) => {
for _ in 0..$count {
$cmd;
}
};
}
}
Very simple! $cmd is the statement and $count is the number of repetitions. The macro turns it into a for loop.
19.3.3. Testing the Macro
fn main() {
repeat!(println!("Ferris is cool!"), 3);
repeat!(let x = 5; println!("x = {}", x), 2);
}
The output is exactly what we wanted. See how with a few lines of code we built a custom tool? 🛠️✨
19.4. Summary and Challenge
19.4.1. Concept Review
✅ unsafe: the part of Rust where memory safety becomes the programmer’s responsibility. Only for low‑level, essential work.
✅ Raw pointers (*const T, *mut T): unchecked by the compiler; must be used inside unsafe.
✅ Macro (macro_rules!): code generator at compile time. Ends with !.
✅ Macro patterns: $name:ident (identifier), $($x:expr),* (list of expressions).
✅ $(...)*: repeat code for each input.
✅ You have travelled to the deepest layers of Rust – from the unsafe engine to the magic of macros. A true computer wizard now knows what tools are available! 🧙
🧠 Sometimes things are hard, and that’s okay!
unsafeand macros are among the most advanced topics in Rust. Even professional programmers rarely useunsafeand write complicated macros with caution. If you still feel you don’t understand everything, don’t worry – this chapter is about awareness, not mastery. The important thing is that you know how and where you can use these tools.
19.4.2. Challenge: The create_function! Macro
Create a macro that takes a function name, a start number, and an end number. It should generate a function that prints all numbers from start to end. Use stringify! to show the function name.
💡 Sample answer:
macro_rules! create_function {
($name:ident, $start:expr, $end:expr) => {
fn $name() {
println!("Function {} is running: 👇", stringify!($name));
for i in $start..=$end {
println!(" number: {}", i);
}
}
};
}
create_function!(count_to_five, 1, 5);
fn main() {
count_to_five();
}
🔚 End of Chapter 19
Now you know how to visit the engine room when necessary and how to create your own custom spells with macros. In Chapter 20, our final adventure, we will build a small network among Ferris’s friends to send secret messages and create a space chat room! Ready for the end of this space journey? 🌌📡🦀
![[Illustration: Ferris wearing a graduation cap and safety goggles, holding a glowing “Chapter 19 Master” badge. Floating around him are an unsafe engine core, macro spell symbols, and a repeating loop arrow. Encouraging, vibrant children’s book illustration, celebratory mood, 16:9.]](assets/images/19.3.png)
Land of Rust: Ferris the Space Crab’s Adventures
Chapter 20: FerrisNet – The Friends’ Secret Network (Final Network Project)
📑 Chapter Index
20.1. How Can Ferris Talk to His Friend on Another Computer? (Sockets)
20.1.1. Story: Space Telephones
20.1.2. Basic Concepts: IP, Port, Client and Server
20.1.3. The Networking Toolbox: std::net
20.2. Building a Simple Messenger (Ferris Messenger)
20.2.1. Server: Listening on a Port
20.2.2. Client: Connecting and Sending a Message
20.2.3. Exercise: Sending a Reply from the Server
20.3. Improving the Messenger: Two‑Way Chat
20.3.1. The Problem: Taking Turns Is Boring
20.3.2. Using Two Threads to Read and Write
20.4. Final Project: FerrisNet Group Chat
20.4.1. Idea: Broadcasting Messages to Everyone
20.4.2. A Shared List of Guests
20.4.3. Full Group Chat Server Code
20.4.4. Group Chat Client
20.4.5. Challenge: Adding a Username
20.5. Farewell and the Road Ahead
20.5.1. Congratulations! You Are a True Rustacean
20.5.2. Next Steps
20.5.3. Ferris’s Final Words
20.1. How Can Ferris Talk to His Friend on Another Computer? (Sockets)
20.1.1. Story: Space Telephones
Ferris misses his friend Bill, who lives in the spaceship next door. But the metal walls of the ships are too thick – sound cannot pass through them. Ferris has a great idea: “Why not use computers to talk?”
Computers can send data to each other through wires or wireless waves. But how do they know which program the message is for? A web browser? A game? A chat program? That’s where addresses and ports come in! 📡✨
This is your final step toward becoming a computer wizard – building a connection between two computers! 🧙♂️
👨👩👧 Note for parents and teachers
This project combines networking, concurrency, and error handling, making it a great achievement for the end of the book. If the child struggles with the group chat project, you can start with the simple (single client) version first and then move on to the multi‑client version. The official Rust book does not have a dedicated chapter on networking, but thestd::netdocumentation is a good resource:
doc.rust-lang.org/std/net/index.html
20.1.2. Basic Concepts: IP, Port, Client and Server
To understand networking, you only need to know three things:
🔹 IP address: like a house address. Every computer on a network has a unique number. 127.0.0.1 is a special address that always points to “your own computer” (it’s called localhost).
🔹 Port: like an apartment number. A single computer can run several network programs at the same time. Each program listens on a specific port. We will use port 7878.
🔹 Server and Client: The server is like a hotel receptionist – it waits for someone to come. The client is like a guest – it knocks and connects.
![[Illustration: Cartoon illustration of two friendly space crabs in separate spaceship cabins, talking via glowing walkie-talkies. Between them, a floating holographic map shows an IP address “127.0.0.1” and a port number “7878” connected by a dotted light beam. Style: vibrant children’s book, playful tech metaphor, soft lighting, 16:9.]](assets/images/20.1.png)
20.1.3. The Networking Toolbox: std::net
The good news is that Rust has a ready‑made module called std::net that contains all the necessary networking tools. In this chapter we will use the TCP protocol. TCP is like a reliable phone call: first a connection is established, then messages are sent and received in order, without getting lost. 📞
20.2. Building a Simple Messenger (Ferris Messenger)
20.2.1. Server: Listening on a Port
First, let’s create a new project. Our server will act like a guard standing at the door, waiting for someone to arrive:
use std::io::{Read, Write};
use std::net::TcpListener;
fn main() -> std::io::Result<()> {
// 1. Create a listener on localhost, port 7878
let listener = TcpListener::bind("127.0.0.1:7878")?;
println!("🦀 Ferris server is listening on port 7878...");
// 2. Wait for someone to connect
for stream in listener.incoming() {
let mut stream = stream?;
println!("✅ A client connected!");
// 3. Create an empty buffer (1024 bytes) to read the message
let mut buffer = [0; 1024];
let n = stream.read(&mut buffer)?;
// 4. Convert the bytes to text
let message = String::from_utf8_lossy(&buffer[..n]);
println!("📩 Message received: {}", message);
// 5. Send back a reply
let response = "Got your message! 👋";
stream.write_all(response.as_bytes())?;
}
Ok(())
}
🔹 TcpListener::bind creates a listening port.
🔹 listener.incoming() returns an iterator of incoming connections.
🔹 stream.read reads data and write_all sends a reply.
20.2.2. Client: Connecting and Sending a Message
Now let’s create another file called client.rs that acts like the guest knocking on the door:
use std::io::{Read, Write};
use std::net::TcpStream;
fn main() -> std::io::Result<()> {
// 1. Connect to the server
let mut stream = TcpStream::connect("127.0.0.1:7878")?;
println!("🔗 Connected to the server!");
// 2. Send a message
let msg = "Hello Ferris! This is a message from Bill.";
stream.write_all(msg.as_bytes())?;
println!("📤 Message sent: {}", msg);
// 3. Wait for a reply
let mut buffer = [0; 1024];
let n = stream.read(&mut buffer)?;
let response = String::from_utf8_lossy(&buffer[..n]);
println!("📩 Server reply: {}", response);
Ok(())
}
If you run the server in one terminal and the client in another terminal, you will see that the messages are successfully exchanged! 🎉
20.2.3. Exercise: Sending a Reply from the Server
Modify the server so that instead of sending a fixed text, it sends back the same client message in uppercase (to_uppercase()).
💡 Hint: Simply change the response line to let response = message.to_uppercase();.
![[Illustration: Split-screen educational graphic. Left: a cartoon server rack with a glowing “Listening…” sign. Right: a friendly laptop with a “Connecting…” progress bar. A glowing message bubble travels between them. Ferris watches with a checklist, smiling. Style: clean vector illustration, bright, educational metaphor, 16:9.]](assets/images/20.2.png)
20.3. Improving the Messenger: Two‑Way Chat
20.3.1. The Problem: Taking Turns Is Boring
So far our communication is like a one‑way SMS. Real chat must be two‑way! Both sides must be able to type and see the other person’s messages at the same time.
20.3.2. Using Two Threads to Read and Write
We solve this problem by using two Threads (remember Chapter 16?):
🔸 Thread 1: constantly reads from the user’s keyboard and sends messages to the server.
🔸 Thread 2: constantly reads from the server and prints messages on the screen.
This way nobody has to wait for their turn! ⚡
use std::io::{self, BufRead, BufReader, Write};
use std::net::TcpStream;
use std::thread;
fn main() -> std::io::Result<()> {
let mut stream = TcpStream::connect("127.0.0.1:7878")?;
println!("🔗 Connected to Ferris’s chat room! (type ‘quit’ to exit)");
// Make a clone of the socket for the receiving thread
let mut stream_clone = stream.try_clone()?;
// Receiver thread: listens to the server
let receiver = thread::spawn(move || {
let reader = BufReader::new(&mut stream_clone);
for line in reader.lines() {
match line {
Ok(msg) => println!("📩 {}", msg),
Err(_) => {
println!("❌ Connection to the server was lost.");
break;
}
}
}
});
// Main thread (sender): listens to the keyboard
let stdin = io::stdin();
for line in stdin.lock().lines() {
let line = line?;
if line.trim() == "quit" { break; }
stream.write_all(line.as_bytes())?;
stream.write_all(b"\n")?;
}
receiver.join().unwrap();
println!("👋 Goodbye!");
Ok(())
}
📌 stream.try_clone() is very important! Sockets are like keys. Two threads cannot use the same socket at the same time unless you make a copy of it.
![[Illustration: Cartoon scene showing a split pathway. Left side: a user typing on a keyboard, messages flowing UP to a server. Right side: messages flowing DOWN from the server to a screen. Two glowing threads labeled “Thread 1” and “Thread 2” weave together smoothly. Ferris stands in the middle conducting traffic like an orchestra leader. Style: dynamic, educational children’s book, bright colors, 16:9.]](assets/images/20.3.png)
20.4. Final Project: FerrisNet Group Chat
20.4.1. Idea: Broadcasting Messages to Everyone
Now we want to build a real chat room where whenever someone connects, their message is sent to everyone else. It’s just like a classroom – when one person speaks, everyone hears them! 🗣️🌍
20.4.2. A Shared List of Guests
The server must keep a list of all connected clients. When a message arrives, the server copies it and sends it to every client on the list.
Because several threads will access this list at the same time, we must use Arc (for sharing ownership) and Mutex (to prevent write conflicts):
#![allow(unused)]
fn main() {
type Clients = Arc<Mutex<Vec<TcpStream>>>;
}
This means: “a safe, shared list of sockets”.
20.4.3. Full Group Chat Server Code
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use std::thread;
type Clients = Arc<Mutex<Vec<TcpStream>>>;
fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:7878")?;
println!("🦀 Group chat server is running on port 7878!");
let clients: Clients = Arc::new(Mutex::new(Vec::new()));
for stream in listener.incoming() {
let mut stream = stream?;
println!("✅ A new user connected!");
// Add the client to the list
clients.lock().unwrap().push(stream.try_clone()?);
let clients_clone = Arc::clone(&clients);
// Spawn a new thread for each user
thread::spawn(move || {
handle_client(stream, clients_clone);
});
}
Ok(())
}
fn handle_client(mut stream: TcpStream, clients: Clients) {
let reader = BufReader::new(&mut stream);
for line in reader.lines() {
match line {
Ok(msg) => {
let formatted = format!("[User]: {}\n", msg);
println!("📨 {}", formatted.trim());
// Broadcast the message to everyone
let mut clients_guard = clients.lock().unwrap();
for client in clients_guard.iter_mut() {
// If writing fails, ignore it so the server doesn’t crash
let _ = client.write_all(formatted.as_bytes());
}
}
Err(_) => break, // user disconnected
}
}
println!("❌ A user left the chat.");
}
💡 Professional tip: let _ = client.write_all(...) means “if writing fails (for example, that client left), ignore the error and continue”. This is very important for real‑world servers!
20.4.4. Group Chat Client
The client is the same two‑way code from the previous section. To run it, create two separate binary files in a single project or two separate projects. You can define two binaries in Cargo.toml:
[[bin]]
name = "server"
path = "src/server.rs"
[[bin]]
name = "client"
path = "src/client.rs"
Then run cargo run --bin server and cargo run --bin client. Open several terminals, start the server, and run cargo run --bin client multiple times. Now anything you type in one terminal will appear in all the others! 🎊
20.4.5. Challenge: Adding a Username
Right now every message starts with [User]. Can you change the program so that it first asks for a username, and the server shows that username instead?
💡 Hint: The first message the client sends could be its name. The server could read it and use it for all later messages. Or, even simpler, the client could add [username]: to every message before sending it.
![[Illustration: A cozy cartoon chat room with floating speech bubbles connecting three different laptops. Each screen shows a friendly avatar and messages flowing in real-time. In the center, a glowing server hub labeled “Arc<Mutex<Vec>>” safely routes messages. Ferris sits at a control desk giving a thumbs up. Style: warm, inviting children’s book illustration, clear network metaphor, 16:9.]](assets/images/20.4.png)
20.5. Farewell and the Road Ahead
20.5.1. Congratulations! You Are a True Rustacean
Twenty chapters ago, you didn’t know what fn main() meant. Today, you have built a simple social network, understood concurrency, worked with smart pointers, and applied advanced concepts like Traits and Generics. You are now officially a Rustacean! 🦀🎉
You have gone from a beginner who wrote “Hello World” to a wizard who can connect computers together. That is real power! 🧙✨
🧠 Sometimes things are hard, and that’s okay!
If you run into trouble while running the group chat project (for example, messages don’t reach everyone), don’t worry. Debugging network programs can be a little tricky. Go step by step: first make sure the server and client work on127.0.0.1alone, then move on to multiple clients. Every problem you solve is a big step forward.
20.5.2. Next Steps
Learning Rust does not end here. This is only the beginning! Try these things after the book:
📖 The official Rust book: read “The Rust Programming Language” online (it’s free!).
🧩 Daily exercises: websites like rustlings and exercism are full of small, fun challenges.
🌐 The Rust community: join the official Rust forums or Discord. They are full of kind people who love to help.
🛠️ A personal project: the best way to learn is to build something new! Make a simple game, a command‑line tool, or a small website using a framework like Axum.
⚡ Async programming: learn about async/await and the tokio library to write super‑fast network programs.
20.5.3. Ferris’s Final Words
“My friend, you are amazing! You have mastered the hardest concepts and shown that with a little patience and practice, anything is possible. I, Ferris, am proud of you. Now your spaceship is ready to fly into the far galaxies of programming. 🚀
Remember: the compiler is your friend, not your enemy. Making mistakes means you are learning. The simplest code that works is better than the most complicated code that doesn’t.
Go ahead, code, build, break, and build again. The world of software needs people like you. Goodbye, dear Rustacean. Until the next adventure… 🦀💙”
![[Illustration: Ferris the crab standing proudly on a small wooden crate labeled “Chapter 20 Complete”. He holds a glowing Rust gear in one claw and waves with the other. Behind him, a starry galaxy forms the shape of a heart. Floating text: “Thank You for Coding!”. Style: emotional, celebratory children’s book illustration, warm sunset lighting, 16:9.]](assets/images/20.5.png)
Appendix A: The Story of Colored Stickers (Lifetimes)
Age Rating: 🚀 (Ages 15+)
📑 Table of Contents
- A.1. The Yogurt Expiration Date Story
- A.2. Rule #1: Every Reference Has an Expiration Date
- A.3. Helping the Compiler with
'a - A.4. Lifetime Elision (When the Compiler Guesses for You)
- A.5. Lifetimes in Structs
- A.6. Exercises
- A.7. Summary
A.1. The Yogurt Expiration Date Story
Ferris buys a local space yogurt. On the cup it says: “Expiration date: April 20, 2026.” Until that date, he can eat it safely. After that, the yogurt goes bad and will give him a stomach ache. 🤢
In Rust, every reference also has an “expiration date”. We call this a lifetime. It tells the compiler how long the reference remains valid.
👨👩👧 Note for parents and educators
Lifetimes are one of Rust’s most unique – and sometimes confusing – concepts. This appendix deepens the brief introduction from Chapter 10. The goal is not mastery, but familiarity. If your child doesn’t understand everything the first time, that’s perfectly fine – most adult programmers also need practice. The official Rust book has an entire chapter on lifetimes:
doc.rust-lang.org/book/ch10-03-lifetime-syntax.html
A.2. Rule #1: Every Reference Has an Expiration Date
The Rust compiler always checks that no reference outlives the data it points to.
#![allow(unused)]
fn main() {
let r; // r is an empty reference (doesn't point to anything yet)
{
let x = 5; // x is created
r = &x; // r borrows x
} // x dies here (the yogurt has expired!)
println!("{}", r); // ❌ Error! r points to expired data
}
The compiler calmly says: “No friend, r lives longer than x. That’s dangerous. I won’t allow it.”
🛡️ This is one of the reasons Rust is one of the safest programming languages.
A.3. Helping the Compiler with 'a
Sometimes the compiler cannot guess which input the output of a function belongs to. For example, a function that returns the longer of two strings:
#![allow(unused)]
fn main() {
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
}
The compiler gets confused: “I don’t know if the output refers to x or y. So I can’t determine its expiration date.”
We help it with a lifetime label 'a (apostrophe + a letter):
#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
}
Meaning: “Dear compiler, x and y are valid at least as long as 'a. The output will also be valid for 'a. Whichever input lives shorter, the output will live exactly that long.”
🔹 'a is just a name. You could also write 'fruit or 'crab. The only rule is that it starts with an apostrophe.
A.4. Lifetime Elision (When the Compiler Guesses for You)
Good news: In 95% of cases, you don’t need to write 'a at all! The compiler has three simple rules:
-
Rule 1: Every reference parameter gets its own hidden lifetime.
(e.g.,fn foo(x: &str)→fn foo<'a>(x: &'a str)) -
Rule 2: If there’s exactly one input reference parameter, the output gets that same lifetime.
(e.g.,fn first_word(s: &str) -> &str→ the output lives exactly as long ass) -
Rule 3: If there are multiple input references and one of them is
&selfor&mut self(i.e., methods), the output gets the lifetime ofself.
Thanks to these rules, you wrote many programs in this book without ever typing a single 'a. Rust handled everything behind the scenes. 🧠
A.5. Lifetimes in Structs
If a struct wants to hold a reference, we must write the lifetime explicitly:
#![allow(unused)]
fn main() {
struct Excerpt<'a> {
part: &'a str,
}
}
This means: “This struct holds a &str that is valid at least for 'a. You can never create an Excerpt that outlives the original string.”
fn main() {
let novel = String::from("Once upon a time...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = Excerpt { part: first_sentence };
println!("{}", excerpt.part);
} // novel dies here, and excerpt dies with it – correct.
If you try to cheat, the compiler yells:
#![allow(unused)]
fn main() {
let excerpt;
{
let novel = String::from("Short story");
excerpt = Excerpt { part: &novel };
} // novel dies here
println!("{}", excerpt.part); // ❌ Error! excerpt points to dead data
}
A.6. Exercises
Exercise 1: Find the Lifetime Problem
Why does the following code fail? Can you fix it by adding 'a?
#![allow(unused)]
fn main() {
fn dangerous() -> &i32 {
let x = 42;
&x
}
}
Answer: x is created inside the function and dies when the function returns. You cannot return a reference to it. No lifetime annotation can fix this because the data doesn’t exist outside the function. Solution: return the value itself (return x), not a reference.
Exercise 2: Fix the Function with Lifetimes
Rewrite the following function so that it has a correct lifetime annotation and compiles:
#![allow(unused)]
fn main() {
fn first_or_second(a: &str, b: &str, choice: bool) -> &str {
if choice { a } else { b }
}
}
Answer:
#![allow(unused)]
fn main() {
fn first_or_second<'a>(a: &'a str, b: &'a str, choice: bool) -> &'a str {
if choice { a } else { b }
}
}
Exercise 3: Struct with Lifetime
Create a struct named BookReference with one field page: &str. Add the correct lifetime annotation. Then in main, create a String and a BookReference that points to it.
Answer:
struct BookReference<'a> {
page: &'a str,
}
fn main() {
let text = String::from("Page 42");
let ref_page = BookReference { page: &text };
println!("{}", ref_page.page);
}
A.7. Summary
- ✅ Every reference has a lifetime (expiration date).
- ✅ The compiler automatically prevents references from outliving their data.
- ✅ Sometimes we help the compiler with
'a. - ✅ Three elision rules make most lifetimes invisible.
- ✅ Structs that hold references must declare a lifetime.
🧠 Sometimes it’s hard, and that’s okay! Lifetimes can be confusing – even for experienced programmers. If you don’t grasp everything today, don’t worry. With practice, they will become natural. Rust is always there to protect you from memory mistakes. 🛡️
🖼️ Illustration Prompt (for artist or AI):A cute cartoon yogurt cup with a glowing expiration date sticker labeled "lifetime 'a". A friendly compiler robot holding a magnifying glass checks the sticker and gives a green checkmark. Ferris the crab points at the cup, explaining. Background: a simple kitchen shelf with other dairy products. Style: warm, educational children's book, soft lighting, 16:9
🎁 Bonus Chapter: Ferris’s Pearl Farm – Async Programming Made Simple
Age Rating: 🚀 (Ages 15+) – An extra chapter for curious advanced readers
📑 Table of Contents
- Introduction: The Underwater Pearl Farm
- 1.
Future: An Empty Shell That Promises a Pearl - 2.
.await: Waiting for the Pearl to Be Ready - 3. Runtime (Executor): Ferris Caring for Thousands of Shells
- 4.
join!: Waiting for Multiple Pearls at Once - 5.
select!: Take the First Ready Pearl - 6. A Simple Real‑World Example with
tokio - 7. Exercises & Challenge
- Summary: You Are Now a Pearl Farmer
Introduction: The Underwater Pearl Farm
Ferris runs an underwater farm. He has planted thousands of oyster shells. Each shell will produce a shiny pearl in the future (maybe in one year).
But Ferris cannot sit next to every shell and wait – he would lose all the other shells. Instead, he has a small boat. Every day he rows past all the shells and checks: “Which one is ready?”
This way of working is called asynchronous programming. Unlike threads (Chapter 16), where you split work among many workers, asynchronous programming uses one worker (Ferris) that rotates between many tasks (the shells) and does whichever one is ready. This model is extremely efficient for tasks that are “slow” (like downloading from the internet, reading from a database, or waiting for pearls). 🐚✨
👨👩👧 Note for parents and educators
Asynchronous programming is an advanced topic. This bonus chapter is optional – it will not appear in the main 20‑chapter story. It’s meant for older teens (15+) who have completed the book and want to peek into async Rust. The official Tokio documentation is excellent for deeper dives:
tokio.rs
1. Future: An Empty Shell That Promises a Pearl
In Rust, any operation that might produce a value in the future is called a Future. A Future is like an empty oyster shell: it has nothing now, but one day it will deliver a value (a pearl).
#![allow(unused)]
fn main() {
use std::future::Future;
// This function returns a Future that will produce an i32
async fn get_pearl() -> i32 {
42 // one day this pearl will be ready
}
}
🔹 An async fn does not run immediately. It returns a Future. Only a promise, not a value yet.
2. .await: Waiting for the Pearl to Be Ready
To actually wait for the result, we use .await. It’s like Ferris looking at a specific shell and saying: “Stay here until the pearl is ready.”
async fn main() {
let pearl = get_pearl().await;
println!("Pearl: {}", pearl);
}
⚠️ But main itself cannot use .await unless we have a runtime (next section).
3. Runtime (Executor): Ferris Caring for Thousands of Shells
In Rust, Futures do not run by themselves. They need a runtime (also called an executor) that rotates between them and checks which one is ready. The most popular runtime is tokio.
To run an async function, we put it inside a runtime:
use tokio;
#[tokio::main]
async fn main() {
let pearl = get_pearl().await;
println!("{}", pearl);
}
The #[tokio::main] macro starts a runtime and runs the async main function. It’s exactly like Ferris turning on his boat and starting his daily tour.
4. join!: Waiting for Multiple Pearls at Once
Ferris can watch several shells at the same time. For example, he can wait for all three shells to produce pearls:
use tokio::join;
async fn pearl_1() -> u32 { 10 }
async fn pearl_2() -> u32 { 20 }
async fn pearl_3() -> u32 { 30 }
#[tokio::main]
async fn main() {
let (p1, p2, p3) = join!(pearl_1(), pearl_2(), pearl_3());
println!("Sum: {}", p1 + p2 + p3); // 60
}
join! means: “Start all these Futures at the same time and wait until every one of them finishes.”
5. select!: Take the First Ready Pearl
Sometimes Ferris is in a hurry. He says: “Among these three shells, whichever gives me a pearl first – I’ll take it and ignore the rest.” This is select!:
use tokio::select;
use tokio::time::{sleep, Duration};
async fn fast_pearl() -> &'static str {
sleep(Duration::from_secs(1)).await;
"fast pearl"
}
async fn slow_pearl() -> &'static str {
sleep(Duration::from_secs(3)).await;
"slow pearl"
}
#[tokio::main]
async fn main() {
let winner = select! {
pearl = fast_pearl() => pearl,
pearl = slow_pearl() => pearl,
};
println!("Winner: {}", winner); // always "fast pearl"
}
select! is like a race: the first Future that finishes wins.
6. A Simple Real‑World Example with tokio
Let’s write a small program that simulates two downloads running concurrently:
use tokio::time::{sleep, Duration};
async fn download_file(name: &str, seconds: u64) -> String {
println!("Downloading {} ...", name);
sleep(Duration::from_secs(seconds)).await;
format!("Content of {}", name)
}
#[tokio::main]
async fn main() {
let (file1, file2) = tokio::join!(
download_file("space_photos", 2),
download_file("galaxy_music", 1)
);
println!("Received: {}", file1);
println!("Received: {}", file2);
}
Sample output:
Downloading space_photos ...
Downloading galaxy_music ...
(after 1 second) Received: Content of galaxy_music
(after another second) Received: Content of space_photos
Both downloads started almost at the same time, but they finished according to their delays. This is the power of async – no threads, no locks, just one worker efficiently switching between tasks.
7. Exercises & Challenge
Exercise 1: Simple async function
Write an async function named compute_square that takes a u32, sleeps for 2 seconds (using sleep), and returns the square. In main, call it with .await and print the result.
Answer:
use tokio::time::{sleep, Duration};
async fn compute_square(n: u32) -> u32 {
sleep(Duration::from_secs(2)).await;
n * n
}
#[tokio::main]
async fn main() {
let result = compute_square(5).await;
println!("Square of 5: {}", result);
}
Challenge: Three‑way download race with select!
Run three downloads with delays of 3, 2, and 4 seconds. Use select! to pick the fastest one and print which one won.
Answer:
use tokio::select;
use tokio::time::{sleep, Duration};
async fn download_with_time(name: &str, secs: u64) -> String {
sleep(Duration::from_secs(secs)).await;
format!("{} finished in {} seconds", name, secs)
}
#[tokio::main]
async fn main() {
let winner = select! {
res = download_with_time("A", 3) => res,
res = download_with_time("B", 2) => res,
res = download_with_time("C", 4) => res,
};
println!("Winner: {}", winner);
}
Summary: You Are Now a Pearl Farmer
In this bonus chapter, you learned:
- ✅
Future= an async operation that may produce a value later. - ✅
.await= wait for the value without blocking the whole program. - ✅ Runtime (e.g., Tokio) = the executor that polls Futures.
- ✅
join!= wait for all Futures to complete. - ✅
select!= take the first Future that finishes. - ✅ Async is perfect for I/O‑bound tasks (network, files, databases).
🧠 This chapter is just a small window into the world of async Rust. If you are curious, follow the official Tokio book and the Rust async documentation. Your pearl farm adventure has only begun. 🐚💎
🖼️ Illustration Prompt (for artist or AI):A cute underwater scene with Ferris the crab in a small boat, holding a clipboard. He floats past hundreds of glowing oysters. Some oysters have a little clock icon (Future sleeping). One oyster pops open with a shining pearl (Future ready). A soft "tokio" logo glows on the side of the boat. Style: magical, educational, children's book, bright colors, 16:9
Acknowledgements
Audrius Meškauskas (GitHub | LinkedIn)
wrote the bonus chapter “Ferris’s Pearl Farm – Async Programming Made Simple”.
We are very grateful for his clear explanations, creative metaphors, and for making async Rust accessible to young learners.
Thank you, Audrius, for contributing to The Land of Rust! 🦀✨