Skip to content

Instantly share code, notes, and snippets.

@nizhunt
Created May 20, 2025 07:17
Show Gist options
  • Save nizhunt/472dab0c7f11895563f528eb3417e63f to your computer and use it in GitHub Desktop.
Save nizhunt/472dab0c7f11895563f528eb3417e63f to your computer and use it in GitHub Desktop.
rust challenges

Rust Challenges

Challenge 1: Ownership Woes

Question: Sarah is building a chat application and needs to pass messages between functions. She's confused why her code won't compile. What's wrong with Sarah's approach?

fn main() {
    let message = String::from("Hello, world!");
    send_message(message);
    log_message(message); // Sarah expects this to work
}

fn send_message(msg: String) {
    println!("Sending: {}", msg);
}

fn log_message(msg: String) {
    println!("Logging: {}", msg);
}

Answer: Sarah's code fails because of Rust's ownership rules. When message is passed to send_message(), ownership is transferred, and the variable is no longer valid in main(). To fix this, Sarah could either:

  1. Clone the string: send_message(message.clone());
  2. Use references: Change both functions to accept &String instead of String
  3. Return ownership: Make send_message return the String after use

Challenge 2: Borrowing Puzzle

Question: Max is writing a function to modify a customer's account balance. His code compiles but behaves strangely. What's the issue with his approach?

fn main() {
    let mut account = String::from("Max: $100");
    let balance_ref = &account;
    update_balance(&mut account);
    println!("Updated info: {}", balance_ref);
}

fn update_balance(info: &mut String) {
    info.push_str(" -> $150");
}

Answer: Max is creating a mutable borrow (&mut account) while an immutable borrow (balance_ref) is still in scope. Rust's borrowing rules prevent having both mutable and immutable references to the same data simultaneously. The fix is to move the println! statement before creating the mutable borrow, or to create balance_ref after the call to update_balance().

Challenge 3: Lifetime Mystery

Question: Jamie is developing a text processing tool and wants to return the longer of two string slices, but the compiler is complaining. What's missing in Jamie's code?

fn main() {
    let text1 = "short";
    let text2 = "longer text";
    let result = find_longer(text1, text2);
    println!("The longer text is: {}", result);
}

fn find_longer(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

Answer: Jamie's code is missing lifetime annotations. The compiler can't determine the relationship between the lifetimes of the input parameters and the return value. The function should be defined with explicit lifetime parameters:

fn find_longer<'a>(x: &'a str, y: &'a str) -> &'a str {
    // function body remains the same
}

This tells the compiler that the returned reference will have the same lifetime as both input parameters.

Challenge 4: Thread Communication Conundrum

Question: Alex is building a multi-threaded application where one thread generates data and another processes it. The code compiles but gets stuck. What's wrong?

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();
    
    thread::spawn(move || {
        let val = String::from("important data");
        tx.send(val).unwrap();
    });
    
    thread::sleep(std::time::Duration::from_millis(100));
    let received = rx.recv().unwrap();
    println!("Got: {}", received);
}

Answer: There's nothing wrong with this code! Alex's confusion comes from misunderstanding the behavior of recv(). The recv() method will block the main thread until a message is received, but in this case, the spawned thread should have already sent the message by the time recv() is called (especially with the sleep). The program should work correctly. If Alex wants non-blocking behavior, they could use try_recv() instead.

Challenge 5: Smart Pointer Puzzle

Question: Priya is working on a data structure that needs to be shared between different parts of her code. She's confused about why her RefCell approach isn't working as expected.

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let data = Rc::new(RefCell::new(vec![1, 2, 3]));
    
    let data_clone = Rc::clone(&data);
    
    {
        let mut borrowed_data = data.borrow_mut();
        borrowed_data.push(4);
        
        let another_borrow = data_clone.borrow();
        println!("Vector: {:?}", another_borrow);
    }
}

Answer: Priya's code will panic at runtime because she's trying to create an immutable borrow (another_borrow) while a mutable borrow (borrowed_data) is still active. Unlike compile-time borrowing rules, RefCell enforces borrowing rules at runtime. To fix this, Priya should drop the mutable borrow before creating the immutable one by moving the println! statement outside the scope where borrowed_data is defined.

Challenge 6: Trait Bound Mystery

Question: Omar is implementing a generic function to find the largest element in a collection, but his code won't compile for certain types. What's missing?

fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];
    
    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }
    
    largest
}

fn main() {
    let numbers = vec![34, 50, 25, 100, 65];
    let result = largest(&numbers);
    println!("The largest number is {}", result);
    
    let chars = vec!['y', 'm', 'a', 'q'];
    let result = largest(&chars);
    println!("The largest char is {}", result);
}

Answer: Omar's function is missing necessary trait bounds. To compare values with >, the type T must implement the PartialOrd trait. Additionally, to copy values from the slice, T must implement the Copy trait. The correct function signature should be:

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    // function body remains the same
}

Challenge 7: Pattern Matching Puzzle

Question: Lin is developing a command processing system using enums and pattern matching. She's confused why her code won't compile.

enum Command {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn process_command(command: Command) {
    match command {
        Command::Quit => println!("Quitting..."),
        Command::Move { x } => println!("Moving to x: {}", x),
        Command::Write(text) => println!("Text message: {}", text),
        Command::ChangeColor(r, g, b) => println!("Changing color to: {}, {}, {}", r, g, b),
    }
}

fn main() {
    let cmd = Command::Move { x: 10, y: 20 };
    process_command(cmd);
}

Answer: Lin's pattern matching is incomplete. In the Command::Move arm, she's only matching on the x field but ignoring the y field. Rust requires that all fields of a struct-like enum variant be accounted for in the pattern. She should either use Command::Move { x, y } to capture both fields or Command::Move { x, .. } to explicitly ignore the remaining fields.

Challenge 8: Unsafe Code Challenge

Question: Raj is experimenting with unsafe Rust to manipulate raw pointers. His code compiles but produces unexpected results. What's the issue?

fn main() {
    let mut num = 5;
    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;
    
    unsafe {
        *r2 += 1;
        println!("r1 points to: {}", *r1);
        *r2 += 1;
        println!("r1 still points to: {}", *r1);
    }
}

Answer: There's no actual bug in Raj's code! The confusion arises from expectations about pointer behavior. Both r1 and r2 point to the same memory location (num). When Raj modifies the value through r2, he's changing the value that r1 points to as well. This is actually demonstrating one of the key dangers of unsafe code: having multiple ways to access and modify the same memory. The output will show r1 reflecting the updated value of num after each modification through r2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment