Skip to content

← Closures and Iterators step 5 of 28

Easy Primitives

iter, iter_mut, into_iter: all three, one Vec

Take one Vec<String> and produce three results from it, in this order.

pub fn triple(words: Vec<String>) -> (usize, Vec<usize>, Vec<String>)
  1. .0: the total length in bytes of the words as they arrived, computed while the vector is still unchanged.
  2. .1: uppercase every word in place (replace each element of the vector with its uppercase form), and return each word's new length in bytes, in the original order.
  3. .2: the uppercased words from step 2, moved out of the vector as owned Strings, in reverse order.

For ["hello", "world"] the result is (10, [5, 5], ["WORLD", "HELLO"]). For [] it is (0, [], []).

Exact contract

  • The vector may be empty, and individual strings may be empty.
  • Every length is a UTF-8 byte count, the value returned by str::len, not a count of characters.
  • Uppercase with str::to_uppercase. It applies Unicode's default case mapping, which does not depend on a language or locale. Do not trim, normalize or otherwise change the strings.
  • .1 has one entry per input word, in input order. .2 contains every uppercased word, last input word first.

The three steps are ordered so that each one fits a different way of iterating over the vector: step 1 only reads, step 2 changes elements while the vector stays in place, and step 3 takes ownership of the strings.

Three ways to iterate over a Vec

A Vec offers three iterator methods. They differ in how they access the vector (shared borrow, mutable borrow or by value) and therefore in the type of each item the iterator yields, called Item:

Call Takes the vector as Item for a Vec<String> Afterwards
v.iter() &v (shared borrow) &String v is unchanged and usable
v.iter_mut() &mut v (mutable borrow) &mut String v is usable and may be modified
v.into_iter() v (by value, a move) String v has been moved and cannot be used

Arrays and VecDeque offer the same three. A slice [T] has iter and iter_mut, but it does not own its elements, so it cannot hand them out by value: calling into_iter on a &[T] yields &T. Not every collection offers all three: HashSet and BTreeSet have no iter_mut, because changing an element could invalidate where the set stored it, and HashMap::iter_mut yields (&K, &mut V), so only the values can be changed.

Choosing the wrong form produces several common errors. Some examples, all checked with Rust 1.98:

  • Calling .iter() on a Vec<String> and collecting into Vec<String> gives error[E0277]: a value of type Vec<String> cannot be built from an iterator over elements of type &String.
  • Writing .map(|s| *s) over .iter() tries to move a String out of a shared reference: error[E0507]: cannot move out of *s which is behind a shared reference.
  • In v.iter().filter(|x| ...), filter passes each item by reference, so x is a &&i32 for a Vec<i32>.

A useful question before writing a loop is whether you need to read the elements, change them, or take them. The answer selects the method, and the method determines the item type.

The starter does not compile

It begins with this loop:

for w in words {
    total += w.len();
}

and then uses words again. The compiler reports:

error[E0382]: borrow of moved value: `words`

A for loop calls IntoIterator::into_iter on the expression after in. For a Vec<String> value, that is Vec::into_iter, which takes the vector by value. Each w is therefore an owned String, and after the loop words has been moved and cannot be used.

To read without moving, iterate over a reference: for w in &words or words.iter(). These produce the same iterator type, because IntoIterator for &Vec<T> is implemented by calling iter(). Clippy's into_iter_on_ref lint, which warns by default, flags (&words).into_iter() and suggests words.iter() for the same reason.

Mutating in place needs a mutable binding

Step 2 uses iter_mut, which takes &mut self. A mutable borrow of a local variable requires the variable to be declared mut, and the parameter words: Vec<String> is not, so you will see:

error[E0596]: cannot borrow `words` as mutable, as it is not declared as mutable

Declare the parameter as mut words: Vec<String>. The mut on a by-value parameter is not part of the function's signature as seen by callers: they still pass a Vec<String> and nothing changes for them. It only makes the local binding mutable, like let mut. Because the function owns the vector, it may mutate it.

Inside the closure (or loop) that iter_mut drives, w has type &mut String. To replace the string it points to, assign through the reference: *w = w.to_uppercase();. Without the *, the assignment tries to store a String in the variable w, whose type is &mut String, and the compiler reports E0308 (mismatched types).

Uppercasing can change byte lengths

str::to_uppercase maps each character to its full Unicode uppercase form, which can contain more or fewer bytes than the original. The hidden tests include:

  • "fire": 5 bytes, because the fi ligature (U+FB01) is one 3-byte character. It uppercases to "FIRE", which is 4 bytes.
  • "straße": 7 bytes. ß (2 bytes) uppercases to SS (2 bytes), so "STRASSE" is also 7 bytes, but it has one more character.
  • "café": 5 bytes, uppercased to "CAFÉ", also 5 bytes.

Because of the first case, the step 1 total must be computed from the original strings and the step 2 lengths from the uppercased ones. Computing both from the same version of the strings compiles but gives a wrong answer.

Cloning is not needed

Cloning the vector so you can call into_iter on each copy would pass the tests, but it copies every string for no benefit. Borrowing for step 1, mutably borrowing for step 2 and moving for step 3 solves the task without cloning. (The uppercase strings and the two output vectors are still new allocations.) When a borrow error seems to call for clone, first check whether a different iteration form gives the access you need.

Grading compiles your code, runs the tests and runs cargo clippy -- -D warnings, so Clippy warnings count as failures.