Skip to content

← Rust: zero to hero step 5 of 342

Easy Primitives

Shadowing: rebinding is not mutation

Count the words in a string, where a word is a maximal run of non-whitespace characters.

pub fn normalize(raw: &str) -> usize

The contract

  • "Whitespace" means a character with the Unicode White_Space property, which is what char::is_whitespace and str::split_whitespace use. That includes space, tab, newline and carriage return, and also non-ASCII characters such as the no-break space U+00A0 and the ideographic space U+3000. A zero-width space (U+200B) is not whitespace, so it does not separate words.
  • Any run of one or more whitespace characters counts as a single separator. Leading and trailing whitespace does not create empty words.
  • Every other character, including accented letters, digits and punctuation, is part of a word. "héllo, wörld!" has 2 words.
  • Return the count as a usize. An empty string, or a string containing only whitespace, gives 0.

Examples: " a b c " gives 3, "" gives 0, "single" gives 1, and "a\tb\nc d" (with a tab and a newline) gives 4.

The starter does not compile. The task is to fix it by replacing an assignment with a new let binding and dropping the mut that is then unused.

Mutation and shadowing

let mut x = 5;
x = 6;             // mutation: same binding, new value, same type

let y = 5;
let y = "five";    // shadowing: a second binding named y, with its own type

The first pair is mutation. x is one binding declared mut, and the assignment x = 6 stores a new value in it. A binding has one type for its whole life, so the new value must have the same type as the old one.

The second pair is shadowing. The second let y declares a new binding that reuses the name. From that line on, y refers to the new binding, and its type is inferred from its own initializer. The first y was not changed; it is just no longer reachable by that name. Shadowing inside a { } block lasts only until the end of the block, after which the outer binding is visible again. Neither binding in the second pair needs mut, because neither is assigned to after it is created.

This is why the starter fails. let mut raw = raw.trim(); gives raw the type &str. The next line tries to assign a SplitWhitespace iterator to that same binding, and rustc rejects it with error[E0308]: mismatched types ("expected &str, found SplitWhitespace<'_>"). The starter also reports error[E0599] on the last line, because raw is still a &str there and &str is not an iterator, so it has no count method. Both errors go away together once the second line declares a new binding instead of assigning. Then remove the mut from the first line as well: nothing assigns to that binding any more, so rustc warns "variable does not need to be mutable", and the Clippy check run on submissions treats warnings as errors.

The pipeline

let raw = raw.trim();                  // &str -> &str, outer whitespace removed
let raw = raw.split_whitespace();      // &str -> SplitWhitespace, an iterator
raw.count()                            // iterator -> usize

Each let shadows the previous binding, and each has a different type. The function parameter raw is a binding too, so the first line shadows it. No binding here is mutated, so none needs mut.

The trim step is not required for the result: split_whitespace already ignores leading and trailing whitespace. It is kept here to show a chain of three shadowed bindings; raw.split_whitespace().count() returns the same number.

The standard library pieces

  • str::trim returns a &str with leading and trailing whitespace removed. The result borrows a shorter range of the same bytes, so no new string is allocated.
  • str::split_whitespace returns a SplitWhitespace iterator over the non-empty pieces of the string separated by whitespace. Iterators are lazy: creating it does no splitting, and the pieces are found as the iterator is consumed.
  • Iterator::count consumes the iterator and returns the number of items it produced, as a usize.

A similar-looking method gives different answers. split(' ') splits on every single space character and keeps the empty pieces between adjacent spaces and at the ends, so " a b c ".split(' ').count() is 7, not 3. It also does not split on tabs or newlines. split_ascii_whitespace splits only on ASCII whitespace, so it treats a no-break space as part of a word.

When to shadow

Clippy's lints about shadowing (shadow_same, shadow_reuse and shadow_unrelated) are all in the restriction group, which is not enabled by default. Clippy's documentation says that group is not meant to be enabled as a whole and that some of its lints contradict others. With the default lints used for this exercise, Clippy will not comment on how you use shadowing.

A reasonable guideline: shadow when the new binding is the same value at a later stage of processing, as with raw here (the input, then the trimmed input, then its words). Use a new name when the value means something different, because a reader looking for the earlier definition of the name could otherwise find the wrong one.