Rust concepts in Limerick form
I asked ChatGPT to explain the fundamentals of Rust in limerick form. It helped me gain a quick grasp of the core concepts.
Chapters
Chapter 1: Ownership
The Rule of One
A value can only be claimed,
By one name that carries its flame.
If you try one more,
The compiler will roar—
“That thing has been moved! Shame, shame!”
Explanation: In Rust, every value has a single owner. Assigning it to a new variable moves ownership. The original is no longer valid.
let sword = String::from("Excalibur");
let weapon = sword;
println!("{}", sword); // ERROR
Cloning the Flame
If you want the first name to stay,
While the second still has its way,
Then clone what you hold—
It’s costly, but bold—
And both can now safely play.
Explanation: To keep using a value after passing it somewhere else, you can `.clone()` it.
let sword = String::from("Excalibur");
let weapon = sword.clone();
println!("{}", sword);
println!("{}", weapon);
Drop the Mic
When a value goes out of sight,
Rust drops it without a fight.
No leaks to be found,
No ghosts stick around,
It cleans up your mess every night.
Explanation: Rust automatically calls `drop` when a value goes out of scope.
{
let sword = String::from("Excalibur");
println!("Forged: {}", sword);
}
println!("Rust cleaned up the forge.");
Function Farewell
When Excalibur’s passed to a quest,
The knight holds it close to his chest.
But back in the town,
The blacksmith will frown—
For the sword is no longer his guest.
Explanation: Passing a value into a function moves ownership.
fn wield(weapon: String) {
println!("Wielding {}", weapon);
}
fn main() {
let sword = String::from("Excalibur");
wield(sword);
println!("{}", sword); // ERROR
}
Copy That
Some values are light as a breeze,
Like numbers and small things with ease.
They copy by trait,
No moves to debate—
Just pass them around as you please.
Explanation: Types like integers and bools implement `Copy`.
fn double(score: i32) -> i32 {
score * 2
}
fn main() {
let points = 42;
let doubled = double(points);
println!("{}", points); // OK
}
The Cost of Forgetting
You might think a clone is just neat,
When juggling your logic repeat.
But each duplication
Risks heap inflation—
Use wisely, or face your defeat.
Explanation: Cloning is safe but can be costly.
let name = String::from("Dawn");
let cloned = name.clone();
let shout = format!("{}!!!", cloned);
println!("{}", name);
Chapter 2: Borrowing
The Lender’s Creed
To borrow is kind, not to steal,
You get access, but not the whole deal.
You can look, you can read,
But to write? You need—
Permission that’s stamped with a seal.
Explanation: Immutable borrowing lets you access without ownership.
fn inspect(name: &String) {
println!("Inspecting: {}", name);
}
The Mutant’s Rule
If you wish to reshape or revise,
Then mutably borrow—be wise!
One at a time,
Is the borrower’s crime,
Or the compiler will swat you like flies.
Explanation: Mutable borrowing requires exclusivity.
fn rename(sword: &mut String) {
sword.push_str(" Mk II");
}
Mixed Signals
You can read or you write, not both,
For the compiler holds up an oath.
“No mixing these two!
That’s chaos to do—
Pick one path, or face wrath and loath’.”
Explanation: No mixing of mutable and immutable borrows.
let mut sword = String::from("Excalibur");
let name_view = &sword;
let rename = &mut sword; // ERROR
Return to Sender
If you lend for a while, that’s just fine,
But don’t cross the ownership line.
When the borrow is done,
You’re back to square one—
With your value still fully thine.
Explanation: Borrowing is temporary and scoped.
let mut blade = String::from("Excalibur");
{
let view = &blade;
println!("Peeking: {}", view);
}
blade.push_str(" X");
Chapter 3: Lifetimes
How Long Do You Live?
When borrowing starts, it’s all swell,
But how long that borrow will dwell?
If it stays past the source,
It’s a crash, of course—
So lifetimes must guide it as well.
Explanation: Lifetimes tell Rust how long a reference is valid.
fn get_name() -> &String {
let name = String::from("Dawn");
&name // ERROR
}
Same Lifespan, No Pain
If two things must live side by side,
Then tell Rust, so it won’t be snide.
Use lifetime tags right,
And the code will be tight—
No ghost refs will slither and hide.
Explanation: Use lifetime annotations for tied references.
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
Inferred, Not Implied
Most lifetimes, you don’t need to name,
The compiler’s quite good at this game.
But return something tied?
You’ll be denied—
Unless you spell out the lifetime’s frame.
Explanation: Rust can infer most lifetimes, except when returning references tied to inputs.
// Most simple cases work via lifetime elision.
fn first_word(s: &str) -> &str {
&s[..1]
}
Structs That Hold Borrows
If your struct holds a borrowed thing,
Then lifetime tags it must bring.
Attach `’a` with care,
Or you’ll get the glare—
Of errors that bite and sting.
Explanation: Structs with references must declare lifetimes.
struct Hero<'a> {
name: &'a str,
}
Chapter 4: Pattern Matching
Match Me If You Can
Patterns are how you branch clean,
With `match`, your intent is seen.
Each arm must be clear,
No silence in here—
Rust guards every path in between.
Explanation: Rust’s `match` forces you to cover every possible case. It’s exhaustive, readable, and safe.
let dir = "north";
match dir {
"north" => println!("You go up."),
"south" => println!("You go down."),
_ => println!("You stand still."),
}
The Optional Truth
When something might not be there,
`Option` will force you to care.
Unwrap it with grace,
Or `match` in its place—
Ignoring it? Don’t you dare.
Explanation: `Option
let maybe_name = Some("Dawn");
match maybe_name {
Some(n) => println!("Hello, {}!", n),
None => println!("Who goes there?"),
}
If Let Be Light
When you just want the “some” to be known,
But `match` feels too bulky, full-blown—
Then `if let` is neat,
It’s short and it’s sweet,
And you leave the `None` path alone.
Explanation: `if let` is a concise way to handle a single `Some(_)` case without full `match` syntax.
let maybe_weapon = Some("Axe");
if let Some(w) = maybe_weapon {
println!("You wield a {}!", w);
}
Chapter 5: Traits Generics
Traits Define What You Do
A trait is a set of the ways,
That a type can behave in your maze.
Like `Display` or `Clone`,
It’s not set in stone—
You can mix them in clever arrays.
Explanation: Traits are like interfaces or contracts. They define what methods a type must have.
trait Greeter {
fn greet(&self) -> String;
}
struct Bot;
impl Greeter for Bot {
fn greet(&self) -> String {
"Hello, world!".into()
}
}
Generic Delight
For types that are yet to be named,
Use generics—don’t feel ashamed!
Just write `T` or `U`,
Let Rust guide you through,
With safety still firmly proclaimed.
Explanation: Generics let your functions and structs work with any type.
fn identity(val: T) -> T {
val
}
Bounds Keep You Sane
If you want to constrain what T is,
Add a bound, or you’ll hear the quiz:
“Does T know this trait?”
Be clear—don’t tempt fate!
Or the compiler will throw you a whiz.
Explanation: Trait bounds like `T: Display` ensure the type supports required methods.
use std::fmt::Display;
fn shout(item: T) {
println!("{}!!!", item);
}
Impl on the House
When a type wants to join in the game,
You `impl` it to earn traitful fame.
Your struct can then do
What traits told it to—
And call methods that act just the same.
Explanation: To use a trait, you `impl` it for a type.
struct Dog;
trait Speak {
fn talk(&self);
}
impl Speak for Dog {
fn talk(&self) {
println!("Woof!");
}
}
Chapter 6: Result Option
The Result of Your Ways
A function that might go awry,
Returns `Result`—don’t just let it fly.
With `Ok` you are blessed,
But `Err` must be addressed—
Or the compiler won’t let you by.
Explanation: Rust encourages explicit error handling with `Result
fn divide(a: i32, b: i32) -> Result {
if b == 0 {
Err("Division by zero".into())
} else {
Ok(a / b)
}
}
The Questioning Mark
To bubble an error with grace,
Let `?` take the error’s place.
If all things go right,
Your path stays in sight—
Else it bails with an elegant face.
Explanation: The `?` operator propagates errors upward automatically.
fn get_first_char(s: &str) -> Result {
let first = s.chars().next().ok_or("Empty string")?;
Ok(first)
}
Beware the Unwrap
`unwrap()` is easy, it’s true,
But it bites when `None` comes through.
Use it when you’re sure,
But if not—secure!
Or your app might crash out of the blue.
Explanation: `unwrap()` will panic if used on an `Err` or `None`.
let maybe_name = Some("Dawn");
let name = maybe_name.unwrap(); // OK
let none: Option = None;
// let crash = none.unwrap(); // Panics!
Mapping the Optional
`Option` can change on the fly,
With `map()` you don’t even try.
It skips if it’s none,
Or transforms what’s done—
Without needing match to apply.
Explanation: `Option
let name = Some("dawn");
let loud = name.map(|s| s.to_uppercase());
println!("{:?}", loud); // Some("DAWN")
Chapter 7: Modules Visibility
The Modular Way
Your code can be neat, not a stew,
With `mod` you can split what you do.
Just write in a file,
With a function or style—
And `use` brings it back into view.
Explanation: Rust lets you organize code into modules (`mod`) across files or inline.
// main.rs
mod tools;
fn main() {
tools::hammer();
}
// tools.rs
pub fn hammer() {
println!("Bang!");
}
The Privacy Wall
By default, your functions are shy,
Hidden unless you say why.
With `pub` you unlock,
Let outsiders knock,
And the compiler will not bat an eye.
Explanation: Items in modules are private by default. Use `pub` to expose them.
// tools.rs
pub fn wrench() {
println!("Tightened!");
}
Super Powers
To reach out or up in the tree,
Use `super` or `self` as your key.
When paths get complex,
Or imports perplex—
These keywords will set your code free.
Explanation: `super::` lets you go up a module. `self::` references the current one.
// inside tools/mod.rs
pub mod bolts;
pub fn use_bolts() {
self::bolts::tighten();
}
// tools/bolts.rs
pub fn tighten() {
println!("Bolts secure!");
}