Skip to content

← Ownership I: Moves, Copy, Clone, Drop step 3 of 20

Easy Primitives

Move semantics: the relay

Consume a Vec<String> from the back and produce one route string.

pub fn relay(mut stations: Vec<String>) -> String

["a", "b", "c"] becomes "c -> b -> a" — last station first. An empty vector becomes "". A single station becomes itself, with no arrow.

The one-sentence summary of Rust

Assignment moves.

Every error in this track is a corollary of that sentence, so it is worth being precise about what it covers. All four of these are moves:

let route = other_route;      // let-binding
route = other_route;          // assignment
takes_ownership(other_route); // passing by value
return other_route;           // returning by value

They are the same operation: the value’s bytes are copied to a new location and the old name stops being usable. Rust does not distinguish “passing an argument” from “assigning to a variable” here, because at the level ownership cares about they are the same event — a value changed owner.

The corollary that trips people up is that a loop body runs more than once. A move inside a loop is fine on the first iteration and a disaster on the second, and rustc says exactly that:

error[E0382]: use of moved value: `route`
  |
  |     while let Some(stop) = stations.pop() {
  |     ------------------------------------- inside of this loop
  |         let _ = link(route, stop);
  |                      ^^^^^ value moved here, in previous iteration of loop

“in previous iteration of loop” is the compiler telling you it checked your code at every program point, not just line by line. It is not guessing.

The fixed helper

fn link(head: String, tail: String) -> String {
    format!("{head} -> {tail}")
}

You may not change this signature. It eats both strings and produces a new one. That is the shape of an enormous amount of real Rust: a function that consumes its inputs and hands you a result. The question every such call forces on you is “do I still need what I just gave away?” — and if the answer is yes, you have exactly three outs, the same three as the previous problem.

::: question The starter’s loop is let _ = link(route, stop); and it fails with E0382. Which of the three outs actually applies? Have the function hand it back — except that here it already does. link returns a String; the starter throws it away with let _ =. Catch it:

route = link(route, stop);

Now route is moved out on every iteration and refilled on every iteration, so the next turn has something to give. This “move it in, get it back, put it in the same slot” pattern is the ownership shape of every fold, every builder chain, and every state machine written in safe Rust.

Cloning would also compile — link(route.clone(), stop) — and it would be wrong: the result is discarded, so the loop would build nothing and you would pay for an allocation per station to achieve it. Worth noticing that the compiler cannot tell you this. clippy::redundant_clone is a nursery lint, off by default, so a gratuitous clone here passes -D warnings silently. The tests are what catch it. :::

Notes on the shape

stations.pop() removes and returns the last element as an Option<String>, which is why the output comes out reversed. The vector is declared mut in the signature for exactly this reason — pop needs to modify it. mut on a parameter is a statement about the local binding inside the function; it says nothing to the caller, who gave the vector away anyway.

The empty case is worth handling before the loop, and let ... else does it without a nested if:

let Some(mut route) = stations.pop() else {
    return String::new();
};

Read that as: bind the Some payload to route, or run the else block, which must diverge. It is the idiomatic Rust for “get me the value or get me out of here”, and clippy’s manual_let_else will nudge you towards it once pedantic lints switch on later in the course.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…