Udaiy’s Blog

Daily Rust Bytes 1 : Ownership, Borrowing, & Type System

12 minutes read

So I've been putting off learning Rust for... honestly, probably two years now. Every time someone mentioned it, I'd nod along and think "yeah, I should really check that out" and then immediately go back to whatever Python project was on fire.

But then I spent three hours last Tuesday chasing a memory leak in a PyTorch C++ extension that was randomly crashing our training jobs. After the fourth time restarting everything, I rage-quit and decided it was finally time to see what the Rust cult was actually about.

First Contact: Hello World and Reality Check

Started with the usual:

fn main() {
    println!("Hello, world!");
}

Compiled fine with rustc main.rs. Cool. Then I tried to get fancy:

fn main() {
    let name = "Sarah";
    println!("Hello, {}!", name);
}

Still worked. I was feeling pretty confident until I tried this:

fn main() {
    let name = "Sarah";
    name = "Bob";  // Compiler error: cannot assign twice to immutable variable
    println!("Hello, {}!", name);
}

Wait, what? I need to explicitly make things mutable? Coming from Python where everything's mutable by default, this felt backwards. Had to add mut:

fn main() {
    let mut name = "Sarah";
    name = "Bob";
    println!("Hello, {}!", name);
}

Actually, this is kind of nice. Forces you to think about what you're actually planning to change.

The Ownership Mind Melt

This is where things got weird. I wrote what seemed like perfectly reasonable code:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}", s1); // Error: borrow of moved value
}

The compiler was NOT having it. Apparently when I assigned s1 to s2, the ownership "moved" and now s1 is... gone?

I spent about an hour reading about this. The idea is that each value has exactly one owner, and when you assign it somewhere else, ownership transfers. No copying, no sharing - just one owner at a time.

This actually makes sense for preventing double-free errors (been there, debugged that), but it felt really restrictive coming from garbage-collected languages.

Borrowing: The "Aha!" Moment

Okay, so if I can't copy values around willy-nilly, how do I actually use them in functions? Enter borrowing:

fn print_length(s: &String) {
    println!("Length: {}", s.len());
}

fn main() {
    let s = String::from("hello");
    print_length(&s);
    print_length(&s); // This works now!
    println!("{}", s); // s is still valid
}

The & creates a reference - basically saying "let this function borrow the value temporarily, but I'm keeping ownership."

Then I tried to modify borrowed values:

fn make_uppercase(s: &String) {
    s.push_str("!"); // Error: cannot borrow as mutable
}

Nope. Need mutable references for that:

fn make_uppercase(s: &mut String) {
    s.push_str("!");
}

fn main() {
    let mut s = String::from("hello");
    make_uppercase(&mut s);
    println!("{}", s); // "hello!"
}

The rule seems to be: either many immutable borrows OR one mutable borrow, but not both at the same time. Makes sense for preventing data races.

When The Borrow Checker Wins

I was working on a simple user struct and hit this pattern:

struct User {
    name: String,
    email: String,
}

fn main() {
    let user = User {
        name: String::from("Alice"),
        email: String::from("alice@example.com"),
    };
    
    print_user(&user);
    update_email(&mut user); // Error: cannot borrow as mutable
}

fn print_user(user: &User) {
    println!("{}: {}", user.name, user.email);
}

fn update_email(user: &mut User) {
    user.email = String::from("alice.new@example.com");
}

Right, user needs to be mutable. Fixed that:

let mut user = User {
    name: String::from("Alice"),
    email: String::from("alice@example.com"),
};

But then I tried to do something stupid:

fn main() {
    let mut user = User {
        name: String::from("Alice"),
        email: String::from("alice@example.com"),
    };
    
    let name_ref = &user.name;
    update_email(&mut user); // Error: cannot borrow as mutable
    println!("Name: {}", name_ref);
}

The compiler caught me trying to hold an immutable reference while also trying to mutably borrow the whole struct. This would be a data race waiting to happen in other languages.

No More Null Nightmares

Coming from languages with null/None, Rust's approach is refreshing:

fn find_user(id: u32) -> Option<User> {
    if id == 1 {
        Some(User {
            name: String::from("Alice"),
            email: String::from("alice@example.com"),
        })
    } else {
        None
    }
}

fn main() {
    match find_user(1) {
        Some(user) => println!("Found: {}", user.name),
        None => println!("User not found"),
    }
}

No more unexpected null pointer exceptions at runtime. The compiler forces you to handle both cases.

Same with error handling using Result:

use std::fs;

fn read_config() -> Result<String, std::io::Error> {
    fs::read_to_string("config.txt")
}

fn main() {
    match read_config() {
        Ok(content) => println!("Config: {}", content),
        Err(e) => println!("Error reading config: {}", e),
    }
}

What I'm Still Struggling With

Actually Useful Things I've Learned

  1. Explicit mutability makes you think about your data flow
  2. No null eliminates a whole class of runtime errors
  3. Pattern matching with match is way more powerful than switch statements
  4. Error handling with Result forces you to deal with failures upfront
  5. The compiler is genuinely helpful - error messages actually explain what's wrong

Next Steps

I'm planning to rewrite one of our smaller Python services in Rust to see how it performs in practice. Also need to actually learn proper async patterns since most of our services are I/O bound.

Will update this as I discover more ways Rust makes me question my life choices.


PS: If anyone has good resources for learning lifetimes that don't make my eyes glaze over, please send them my way.