Skip to content

← Ground Rules: Values, Types, Control Flow step 2 of 24

Easy Primitives

Shadowing: rebinding is not mutation

Count the whitespace-separated words in a string.

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

" a b c " is 3. "" is 0. "single" is 1. Runs of whitespace count as one separator, and leading or trailing whitespace does not create empty words.

The starter does not compile. Fixing it is the lesson.

Two different things that look the same

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

let x = 5;
let x = "five";    // shadowing: a NEW binding, which may have a new type

The second form is called shadowing, and it is completely idiomatic Rust. It is not a typo, not a lint, and not a code smell. It is the normal way to express “the same conceptual thing, one step further along in its processing.”

The critical difference: mut keeps the type. let again does not have to. A let mut raw: &str can never hold a SplitWhitespace, and the compiler says so with E0308: mismatched types. A second let raw = … creates a brand new binding that happens to reuse the name, and its type is inferred fresh.

Why this is worth teaching immediately after mut

Someone who has met mut but not shadowing will reach for mut whenever a value needs to change at all — including when what they actually want is a retyped rebinding. That produces mut bindings that did not need to be mutable and, worse, a habit of forcing everything into one type early.

The idiomatic pipeline for this problem is three bindings with one name:

let raw = raw.trim();                  // &str  -> &str, whitespace stripped
let raw = raw.split_whitespace();      // &str  -> an iterator over words
raw.count()                            // iterator -> usize

Each let shadows the previous one. Each has a different type. Nothing is mutated. The parameter raw is itself shadowed by the first line — parameters are bindings too.

The bits of the standard library you need

  • str::trim returns a &str with leading and trailing whitespace removed. It does not allocate: the result borrows the same bytes, just a shorter range of them.
  • str::split_whitespace returns a lazy iterator over the non-empty whitespace-separated chunks. Because it is lazy, nothing has been computed yet — the work happens when you consume it.
  • Iterator::count consumes the iterator and returns how many items it produced, as a usize.

Note that split_whitespace already skips empty chunks, which is why the leading and trailing spaces in the examples do not produce phantom words. Its cousin split(' ') does not do that, and would give you 5 for " a b c ". Choosing between them is a real decision, not a detail.

A word of honesty about the gate

Every clippy lint about shadowing — shadow_unrelated, shadow_same, shadow_reuse — lives in the restriction group, which is off here and which clippy itself warns contains lints that contradict one another. So on this problem the tools will give you no feedback at all about whether you have shadowed too much.

That is deliberate, and the community is genuinely split on where the line is. The workable rule: shadowing is good when the new binding is the same idea further refined (raw the input, raw trimmed, raw split). It is bad when the new binding is a different idea that happens to be nearby, because then the reader who scrolls up finds the wrong definition. Judgement, not lint.