Skip to content

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

Easy Primitives

let, immutability by default, and mut

For every position in a slice, report the largest value seen up to and including that position.

pub fn running_max(values: &[i32]) -> Vec<i32>

[3, 1, 4, 1, 5] becomes [3, 3, 4, 4, 5]. An empty slice gives an empty vector. [-1, -9] gives [-1, -1] — the running maximum never goes back down.

The starter does not compile, and the error it produces is the most important default in the language.

let binds; it does not declare a variable

let best = i32::MIN;
best = 7;            // error[E0384]: cannot assign twice to immutable variable

In C, Java, Python, Go and JavaScript, a name is mutable unless you go out of your way. In Rust it is the other way round. let x = … binds a value to a name once. To rebind it later you must ask, at the point of declaration:

let mut best = i32::MIN;
best = 7;            // fine

This is not a nanny rule about style. It is load-bearing, and here is why: the borrow checker’s whole job is to know, at every point in the program, who is allowed to change what. If everything were mutable by default, “who can change this?” would have no small answer anywhere, and the guarantees Rust sells — no data races, no use-after-free, without a garbage collector — would not be derivable. Immutability by default is the floor that ownership is built on. You are meeting it in Track 1 so that in Track 3 it is already familiar.

There is a practical payoff too. When you read a function and see let without mut, you know that name’s value never changes for the rest of the block. That is one less thing to hold in your head, on every line, forever.

And the reverse: unused_mut

The gate runs both ways. A let mut you never actually mutate is a warning — unused_mut — and warnings are errors here. So you cannot buy safety by sprinkling mut everywhere “just in case”. Each mut has to be earned by an actual assignment.

This problem needs exactly two mutable bindings: the running best, and the vector you are building. A third, added defensively, will fail the gate.

The shape of the loop

for &v in values {
    // v is an i32 here, copied out of the slice
}

values is a &[i32] — a borrowed view of a run of integers. Iterating it yields &i32, references to each element. The &v in the pattern position undoes that reference and gives you the i32 itself. That works because i32 is a Copy type: copying one is just moving four bytes, so Rust does it implicitly and nothing is lost.

Track 1 uses only Copy types for exactly this reason. The mental model “Rust behaves like C ints” is correct here, it is comfortable, and Track 2 will deliberately break it the moment you meet String and Vec. Enjoy it while it lasts.

Starting value

i32::MIN is the smallest i32, so any real element beats it. That is the standard trick for a running maximum, and it is why the empty-input case works without a special branch: the loop simply never runs.

Loading visualization…