Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

🦀Tutorial: Rust Part 2

Welcome to the second tutorial in our Rust series! This guide builds on the foundational knowledge from Tutorial 1, diving into more topics that are central to writing robust, efficient, and idiomatic Rust code.

Work through the exercises below. Each question includes a sample solution and an explanation in the associated video.


Rust has several ways to loop. The while loop is perfect for when you want a loop to run as long as a condition is true.

  1. Create a mutable variable counter and set it to 5.
  2. Write a while loop that continues as long as the counter is not equal to 0.
  3. Inside the loop, print the value of the counter and then subtract 1 from it.
  4. After the loop finishes, print “Liftoff!”.

When you are finished, change the code to use a for instead. Note: you can use the .rev() function to reverse a range.


Question 2. String vs. String Slices (&str)

Section titled “Question 2. String vs. String Slices (&str)”

Understanding the difference between the String type and a string slice (&str) is important in Rust.

  • String: An owned, heap-allocated, growable text type.
  • &str: An immutable view or slice into a string. This is a reference.

Task:

  1. In main, create an owned String named my_string with the value “Hello, Rust!”.
  2. Create a function print_slice that takes one argument: a string slice named s of type &str.
  3. The print_slice function should just print the slice it receives.
  4. From main, call print_slice and pass it a slice of my_string.
  5. Pass an arbitrary string literal — e.g., "Some String" (i.e., &'static str).
  6. Pass a substring slice — e.g., &my_string[0..5].

Try rewriting print_slice to take String instead of &str. Which calls still work, and why might the compiler complain?


Question 3. Loops with Iterators (for loop)

Section titled “Question 3. Loops with Iterators (for loop)”

(a) The most common and idiomatic way to loop in Rust is with a for loop, which uses iterators.

  1. Create a Vec (vector) or an array named animals containing three strings: “Dog”, “Cat”, and “Mouse”.
  2. Write a for loop to iterate over the animals vector.
  3. Inside the loop, print a message for each animal, such as “I love my Pet: [animal]”.

(b) We can modify this code using .enumerate() to show how to access the index and value. Adapt your code to display the index and value.

(c) Finally, modify the code to use .iter_mut() that lets you modify the elements to reset all of the animals in the vector to be “unknown”. Display all the elements once again.


Enums (enumerations) let you define a type with a few possible variants. The match statement is the perfect way to handle each variant.

  1. Define an enum called TrafficLight with three variants: Red, Yellow, and Green.
  2. Create a function get_action that takes a TrafficLight (by reference) and returns a &'static str (a static string slice).
  3. Inside get_action, use a match statement to check the TrafficLight variant:
    • Red should return “Stop!”
    • Yellow should return “Caution!”
    • Green should return “Go!”
  4. In main, create a TrafficLight and print the action for it.

A struct (structure) lets you group related data together. An impl (implementation) block is where you define methods associated with that struct.

  1. Define a struct named Student with the following fields:
    • name: String
    • student_id: u32
    • grade: f32
  2. Create an impl block for Student.
  3. Inside the impl block, create an associated function (like a constructor) called new that takes a name, ID, and grade, and returns a new Student instance.
  4. Inside the impl block, create a method has_honors that takes &self (a reference to the instance) and returns a bool (true if grade is greater than 40, false otherwise).
  5. In main, create a new student using Student::new() and then print whether or not they have honors by calling the has_honors method.

Enums (enumerations) allow you to define a type by enumerating its possible variants. They are incredibly powerful when combined with the match control flow operator for pattern matching.

  1. Define an enum called WebEvent with the following variants:
    • PageLoad
    • KeyPress(char)
    • Click { x: i32, y: i32 }
  2. Create a function inspect_event that takes a WebEvent and prints a different message for each variant.
  3. In main, create a Vec<WebEvent> containing at least one of each variant and loop through it, calling inspect_event for each event.

Question 7. Error Handling with Result<T, E>

Section titled “Question 7. Error Handling with Result<T, E>”

Rust handles errors by returning a Result<T, E> enum, which has two variants: Ok(T) for success and Err(E) for failure. This makes error handling explicit and robust.

  1. Write a function parse_number that takes a string slice (&str) and tries to parse it into an i32.
  2. The function should return Ok(i32) if parsing is successful and Err(String) with an error message if it fails.
  3. In main, call this function with both a valid number string and an invalid one, and use a match statement to print either the successful result or the error.

The HashMap<K, V> collection stores key-value pairs. It’s a common and useful data structure for lookups.

  1. Create a mutable HashMap to store the scores of two teams, “Blue” and “Red”.
  2. Insert initial scores: Blue starts with 10 and Red starts with 50. Use the HashMap insert() function.
  3. Use entry and or_insert to add 100 points to the “Blue” team’s score. These are combined in the form scores.entry(...).or_insert(0); where the entry() call returns an Entry enum and the .or_insert(0) call on this enum returns a mutable reference to the existing value. If the key does not exist it inserts the value provided, which is 0 and gives you a mutable reference to that.
  4. Loop through the HashMap and print each team’s name and score.

Generics allow you to write code that operates on abstract data types, avoiding code duplication. We are going to write a simple function to get the largest value in a slice — for example, the largest integer in a slice: vec![10, 20, 30, 40, 50, 35]

  1. Create a generic function get_largest<T: PartialOrd>(list: &[T]) -> &T that finds the largest element in a slice of any type that implements the PartialOrd trait.
  2. In main, call this function with both a slice of integers and a slice of characters and print the results.

Note: The PartialOrd trait in Rust is used to define partial ordering for types—meaning that some values can be compared (e.g. using <, >, <=, >=), but not necessarily all. It’s typically implemented alongside PartialEq, since ordering implies equality checks. Unlike Ord, which requires a total order where every pair of values is comparable, PartialOrd allows comparisons that may return None to indicate that two values cannot be meaningfully compared (as with floating-point NaN).


Closures are anonymous functions you can save in a variable or pass as arguments to other functions. Iterators provide a sequence of values that can be manipulated using methods like map, filter, and collect.

The closure |&x| x * 2 is a short, inline anonymous function in Rust. Here’s what it means step by step:

  • The part between the vertical bars |&x| defines the parameter list — in this case, it takes a reference to an integer as its input.
  • The &x means the closure expects to receive a reference, not a plain value. So if you pass this closure to something that iterates over references (like for_each on a vector using .iter()), it will match correctly.
  • Inside the closure body, x * 2 uses the dereferenced value of x (since it was a reference) and doubles it. This is very similar to a lambda function we saw in Chapter 6 of the notes on C++.

Steps:

  1. Create a Vec<i32> of numbers from 1 to 10. You will need to use the .collect() call on the range to turn a range into a collection of Vec<i32>
  2. Use the Debug trait to print out the values in the Vec<i32>.
  3. Use an iterator, a closure, and the map method to create a new vector where each number is doubled. Use the closure explained above.
  4. Use an iterator, a closure, and the filter method to create another new vector containing only the even numbers from the original vector. You can use the modulo operator for this. You will also have to clone() the value into the new vector.
  5. Print both new vectors.

Here are the video solutions — please do not watch these solutions without having attempted the questions first. Please note that these solutions are somewhat warts and all in that I make errors and correct them live without edits.

Alternatively, you can view the video directly on YouTube: https://www.youtube.com/watch?v=DQ1lWDVXPzk