Lecture: Rust Exercises

Dear Computer

Chapter 11: Managing Memory

Lecture: Rust Exercises

Dear students:

The chapter you most recently read was on memory management and higher-order functions. Rust takes a unique position in only allowing a chunk of memory to have one owner. This makes passing data around between functions and into and out of collections challenging. Today we will encounter these challenges by trying to solve some higher-order function and iterator exercises in small teams. But first, let's play a few rounds of Now What.

Now What

We programmers are used to bumbling from problem to problem. As soon as we fix one issue, another crops up. Now what? we ask ourselves. So, here are a few snippets of code that may or may not contain memory issues. Your task is to identify any issues.

  1. let mut s = "?l??d".to_string();
    f(s);
    println!("{}", s);
    Now what? Ownership of the String is transferred to f. We say it is moved. Variable s has lost ownership. A possible fix is to pass s as a reference or a mutable reference.
  2. let mut s = "?l??d".to_string();
    f(s);
    s = "bl??d".to_string();
    Now what? The original String allocation is still moved to f. Variable s is made the owner of a completely different string, which is just fine.
  3. let mut i = 7;
    f(i);
    let i = i + i;
    Now what? Most primitive types are copied rather than moved. This code is just fine.
  4. let i = 7;
    let j = &mut i;
    *j = 8;
    Now what? We get a mutable reference—not a move, not a copy—to i. But i itself is not declared to be mutable, so we can't get a mutable reference to it. The fix is to make i mutable.
  5. let mut i = 7;
    let j = &mut i;
    println!("{}", i);
    *j = 9;
    Now what? Variables i and j are both accessors to the integer, and both are active at the same time. Once you borrow mutably, the owner variable gets locked. The borrowed reference is the only legal way to access the memory until it reaches the end of its lifetime. When a program has two mutable accessors to memory, we get race conditions. The fix is to move the println! before the initialization of j.
  6. let mut i = 7;
    let j = &mut i;
    *j = 9;
    println!("{}", i);
    Now what? Variables i and j are both accessors to the integer, but their time of activity doesn't overlap. Variable j is over and done with before i becomes active again. This code is fine.
  7. let mut i = 7;
    let j = &mut i;
    let k = &i;
    *j = 8;
    Now what? If we have lent out a mutable reference to some memory, no other references are allowed. The fix is to eliminate k.
  8. let mut i = 7;
    let j = &i;
    let k = &i;
    println!("{} {}", j, k);
    Now what? We can have as many read-only references as we want. There's no issue with this code.
  9. fn ten() -> &i32 {
      let value = 10;
      &value
    }
    Now what? The lifetime of value is short. It will end with the function. Yet the function is trying to return a reference to this fleeting memory. The fix is to pass back an owned value.

Exercises

Now let's solve some higher-order function and iterator pipelines, which are a good way to manifest memory problems. I picked the problems below to test my own Rust knowledge, which definitely came up short. Here are some of the things I learned in solving them that may help you:

  1. If you have a reference to a value, you get at the value itself with the * operator.
  2. When you call iter, you get back an iterator that merely borrows the elements of the collection. If the original collection is of type T, each element pulled from the iterator will be of type &T. The original vector is still intact after the iteration.
  3. If you call filter or map after iter, the closures will receive references. In many situations, Rust will automatically dereference, so you may or may not need *.
  4. If you call into_iter, you get back an iterator that moves the elements out of the collection. If the original collection is of type T, each element pulled from the iterator will also be of type T. The original vector is spent after the iteration.
  5. The filter function yields an iteration of references, not owned values, no matter the iterator you start with. If you need an independent collection of the filtered values, call cloned as the next stage of the iteration pipeline. For example: xs.iter().filter(|&x| pred(x)).cloned().
  6. When sorting or computing extrema, the _by methods are easier to use than _by_key. The _by_key methods are more likely to manifest borrowing errors because the key has to outlive the closure.

Round 1

Complete one of the following exercises:

  1. Write function numbers_to_range that accepts a borrowed vector of i32. It returns an inclusive range that spans from the vector's minimum to its maximum. For example: numbers_to_range(&vec![9, 3, 8])3..=9.
  2. Write function middle_word that accepts a string slice that points to a string of the form apple|banana|cantaloupe|durian. It returns the word in the middle. If there's not an exact middle, it returns the first word of the second half. For example, middle_word("apple|banana|cantaloupe|durian")"cantaloupe".
  3. Write function number_lines that accepts a string slice. It prints the string but each line is preceded by its line number, starting at 1.

Round 2

Complete at least one of the following exercises. Do not use loops.

  1. Write function members that accepts a borrowed vector of i32 and a borrowed vector of inclusive ranges. It returns a vector containing the numbers that are in any of the ranges. For example, for the numbers [1, -5, 8] and the ranges 7..=9 and -6..=0, [-5, 8] is returned.
  2. Write function total that accepts a borrowed vector of inclusive ranges of i32. It returns the total count of integers spanned by these ranges, repeats included. For example, the total of 8..=10 and 0..=0 and 7..=8 is 6.
  3. Write function landscape_area that accepts a borrowed vector of pairs of u32. Each pair is the width and height of a rectangle. It returns the total area of just the landscape-oriented rectangles. Tall rectangles are not landscape-oriented, nor are squares.

Round 3

Complete at least one of the following exercises. Do not use loops.

  1. Write function blank_odds that accepts ownership of a vector of string slices. It builds a new vector like the first, but the odd elements have been blanked to the empty string. For example: blank_odds(vec!["those", "were", "the", "days"])vec!["those", "", "the", ""].
  2. Write function middle_half that accepts ownership of a vector of string slices. Suppose the vector has \(n\) elements. It returns a new vector containing the middle \(\lfloor\frac{n}{2}\rfloor\) elements. For example: middle_half(vec!["a", "b", "c", "d", "e"])vec!["b", "c"].
  3. Write function max_elements that accepts two borrowed vectors of i32 numbers. Assume the two vectors have the same number of elements. It returns a new vector in which each element is the maximum of the corresponding parameter elements. For example: max_elements(&vec![-8, 65, 3], &vec![3, 13, 20])vec![3, 65, 20].

TODO

Here's your list of things to do before we meet next:

Complete the middle quiz as desired.
One ready date remains. It is your final extension.

See you next time.

Sincerely,

P.S. It's time for a haiku!

Hi, I'm your grandson We used to fish and watch birds Hi, I'm your grandson
← Higher-order FunctionsLab: Data Jam →