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.
-
Now what? Ownership of the
let mut s = "?l??d".to_string(); f(s); println!("{}", s);
String
is transferred tof
. We say it is moved. Variables
has lost ownership. A possible fix is to passs
as a reference or a mutable reference. -
Now what? The original
let mut s = "?l??d".to_string(); f(s); s = "bl??d".to_string();
String
allocation is still moved tof
. Variables
is made the owner of a completely different string, which is just fine. -
Now what? Most primitive types are copied rather than moved. This code is just fine.
let mut i = 7; f(i); let i = i + i;
-
Now what? We get a mutable reference—not a move, not a copy—to
let i = 7; let j = &mut i; *j = 8;
i
. Buti
itself is not declared to be mutable, so we can't get a mutable reference to it. The fix is to makei
mutable. -
Now what? Variables
let mut i = 7; let j = &mut i; println!("{}", i); *j = 9;
i
andj
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 theprintln!
before the initialization ofj
. -
Now what? Variables
let mut i = 7; let j = &mut i; *j = 9; println!("{}", i);
i
andj
are both accessors to the integer, but their time of activity doesn't overlap. Variablej
is over and done with beforei
becomes active again. This code is fine. -
Now what? If we have lent out a mutable reference to some memory, no other references are allowed. The fix is to eliminate
let mut i = 7; let j = &mut i; let k = &i; *j = 8;
k
. -
Now what? We can have as many read-only references as we want. There's no issue with this code.
let mut i = 7; let j = &i; let k = &i; println!("{} {}", j, k);
-
Now what? The lifetime of
fn ten() -> &i32 { let value = 10; &value }
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:
-
If you have a reference to a value, you get at the value itself with the
*
operator. -
When you call
iter
, you get back an iterator that merely borrows the elements of the collection. If the original collection is of typeT
, each element pulled from the iterator will be of type&T
. The original vector is still intact after the iteration. -
If you call
filter
ormap
afteriter
, the closures will receive references. In many situations, Rust will automatically dereference, so you may or may not need*
. -
If you call
into_iter
, you get back an iterator that moves the elements out of the collection. If the original collection is of typeT
, each element pulled from the iterator will also be of typeT
. The original vector is spent after the iteration. -
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, callcloned
as the next stage of the iteration pipeline. For example:xs.iter().filter(|&x| pred(x)).cloned()
. -
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:
-
Write function
numbers_to_range
that accepts a borrowed vector ofi32
. 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
. -
Write function
middle_word
that accepts a string slice that points to a string of the formapple|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"
. -
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.
-
Write function
members
that accepts a borrowed vector ofi32
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 ranges7..=9
and-6..=0
,[-5, 8]
is returned. -
Write function
total
that accepts a borrowed vector of inclusive ranges ofi32
. It returns the total count of integers spanned by these ranges, repeats included. For example, the total of8..=10
and0..=0
and7..=8
is 6. -
Write function
landscape_area
that accepts a borrowed vector of pairs ofu32
. 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.
-
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", ""]
. -
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"]
. -
Write function
max_elements
that accepts two borrowed vectors ofi32
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:
See you next time.
Sincerely,
P.S. It's time for a haiku!