Skip to content

← Ownership II: Borrowing and the Borrow Checker step 11 of 24

Medium Primitives

ref, ref mut, and default binding modes in edition 2024

pub fn bump_present(opts: &mut [Option<i64>], by: i64) -> (usize, usize)

Walk the slice. Every slot that holds Some(v) gets by added to it, in place. Every None is left alone. Return (how many were bumped, how many were empty).

JSON null is None; any number is Some(n).

[],            by=5   -> (0,0), []
[null,null],   by=5   -> (0,2), [null,null]
[1,2,3],       by=10  -> (3,0), [11,12,13]
[1,null,-4],   by=-3  -> (2,1), [-2,null,-7]

The starter does not compile. The error is one line long and completely opaque until you know that default binding modes exist.

The invisible feature

Look at this and say what type v has:

if let Some(v) = slot { ... }

You cannot, from that line alone. It depends entirely on the type of slot:

type of slot type of v
Option<i64> i64
&Option<i64> &i64
&mut Option<i64> &mut i64

That is match ergonomics, stabilised in Rust 2018 and the reason almost all modern Rust reads the way it does. When you match a reference against a pattern that is not itself a reference pattern, the compiler dereferences for you and switches the default binding mode from move to ref or ref mut. Every binding inside inherits that mode.

It is enormously convenient and it is completely invisible. Nothing in if let Some(v) = &mut opt contains the characters &mut next to v. A learner who does not know the feature exists cannot explain why *v += 1 works in one place and v += 1 is a type error in another, and has no way to find out, because there is no syntax to look up.

Reading the starter’s error

for slot in opts.iter() {          // slot: &Option<i64>
    if let Some(v) = slot {        // binding mode becomes `ref` -> v: &i64
        *v += by;                  // error[E0594]: cannot assign to `*v`,
                                   //               which is behind a `&` reference

iter() yields shared references, so the default binding mode is ref, so v is a &i64, so writing through it is forbidden. The message even tells you the shape of the fix — “consider changing this to be a mutable reference” — but the change belongs on iter(), several lines up, not on the binding it points at.

Swap iter() for iter_mut() and everything else stays identical: the iterator now yields &mut Option<i64>, the binding mode becomes ref mut, v becomes &mut i64, and *v += by compiles. One method call, and the types of three things you never wrote down changed.

The explicit form, and why you rarely see it

Before match ergonomics you had to write the mode yourself:

match slot {
    &mut Some(ref mut v) => *v += by,
    &mut None => {}
}

ref and ref mut are pattern keywords meaning “bind by reference rather than by value”. They still exist and still work. You will meet them in older code and occasionally need them when you want a different mode from the default. Do not write them where the default already does the job — that is noise. (Clippy has a pedantic lint, ref_binding_to_reference, aimed at one variant of that noise. It is allow-by-default and will not fire on this problem; do not go looking for it.)

One related lint that is on by default is needless_borrowed_reference, which fires on patterns like &(ref x) where x alone would do.

And toplevel_ref_arg catches let ref x = ...; in a function body, which should be let x = &...;.

Edition 2024 tightened this, and it matters

The original ergonomics rules produced genuinely surprising types. The classic example:

let [&x, y] = &[&(), &()];    // pre-2024: x: (), y: &&()

Matching &x against a &&() stripped two layers of reference in one step. Nobody predicted that correctly.

Edition 2024 (RFC 3627) makes three previously-legal shapes into hard errors. All three still compile on edition 2021:

  1. let [x, mut y] = &[..];“cannot mutably bind by value within an implicitly-borrowing pattern”. Mixing an explicit mut binding into a pattern whose default mode is ref is now rejected.
  2. let [ref a] = &[..]; — writing ref explicitly while the default mode is already ref is rejected rather than silently doubling up.
  3. let [&b, c] = &[..]; — a reference pattern under an inherited reference mode is rejected instead of stripping two layers.

Your submissions here compile as edition 2024, so these are errors, not warnings. If you copy a pattern from a pre-2024 blog post and it does not compile, this is usually why. The rustc lint rust_2024_incompatible_pat exists to catch the same shapes when migrating an older crate.

About your loop body

The else branch is not decoration — the problem asks you to count the empty slots too, and it also keeps clippy from suggesting .flatten() (which would be the idiomatic shape if you only cared about the Somes, and which would hide the very binding you are here to understand).

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

Loading visualization…