R
RUSTY YELLOW PAGES

Temporary mutabilityConcept


Explanation

Temporary mutability is the idiom of making a binding mut only for the short window where it's actually being built or assembled, then removing mutability the moment that work is done — either by shadowing it with a new, immutable let of the same name, or by scoping the mutable work inside a block expression whose final value becomes an immutable binding outside it. Either way, the rest of the function only ever sees the finished, immutable value; the fact that construction needed mut at all is invisible past that point.

The motivation is the same one behind immutability by default generally: a binding that stays mut for its entire lifetime tells every reader "this could change anywhere below this line," even if, in practice, all the mutation happens in the first five lines and the value is read-only for the following fifty. Shrinking the mut window to exactly where mutation happens keeps that signal accurate — a mut that appears in scope is a mut a reader has to actually worry about.

The shadowing form (let mut v = ...; /* build v */ let v = v;) is the lighter-weight option when the mutable phase is just a few statements in the same function; the block-expression form (let v = { let mut tmp = ...; /* build tmp */ tmp };) is preferable when the construction logic is long enough to want its own visual scope, since it makes the boundary between "building" and "built" a real syntactic block rather than a single re-declaration easy to miss while skimming.

Neither form changes what the compiler generates — the rebinding is purely a hint to the reader (and to lints like Clippy's unused_mut, which would otherwise flag a mut binding that's never mutated again after the rebinding point). The value itself doesn't move or get copied by the shadowing; only the binding's mutability annotation changes.

Basic usage example

let mut greeting = String::new(); // <- mut needed only while building the value
greeting.push_str("hello, ");
greeting.push_str("world");
let greeting = greeting; // <- shadows the binding as immutable; nothing below this line can mutate it

println!("{greeting}");

Best practices & deeper information

Scenario

Creating a new object

Building a Config requires several mutating steps, so the construction work happens inside a block expression, and only the finished, read-only value escapes it.

struct Config {
    entries: Vec<(String, String)>,
}

let config = { // <- block expression scopes the mutable phase
    let mut entries = Vec::new();
    entries.push(("host".to_string(), "localhost".to_string()));
    entries.push(("port".to_string(), "8080".to_string()));
    Config { entries } // last expression: becomes the outer, immutable `config`
};

// config is never `mut` from here on; nothing further in this scope can mutate it
println!("{} entries", config.entries.len());

Why this way: scoping the mut phase to the block keeps the mutation window visually contained, so a reader skimming past the block already knows config is finished and read-only — the Rust Design Patterns book documents this exact block-expression shape as the idiomatic way to build a value that needs temporary mutation.

Scenario

Working with collections

A list of sensor readings needs sorting before it's used for lookups, but nothing after that point should be able to reorder it — shadowing the binding immutable right after sorting makes that guarantee visible in the code itself.

let mut readings = vec![19.5, 22.1, 18.0, 25.3];
readings.sort_by(|a, b| a.partial_cmp(b).unwrap()); // <- the only mutation this Vec ever needs
let readings = readings; // <- shadowed immutable: sorted order is now guaranteed not to change

println!("lowest: {}", readings[0]);

Why this way: once a collection is sorted for a purpose like binary search or displaying a ranked list, further mutation would silently invalidate that invariant — rebinding immutable turns "don't mutate this again" from a comment into something the compiler enforces for the rest of the scope.

Explanation

Embedded support: Full

Register-configuration values are a natural home for this idiom in embedded code: building up the bitfields that eventually get written to a peripheral's control register — clock source, prescaler, mode bits — almost always takes several steps, but once the value is handed to a HAL init function it should never change again for the rest of the peripheral's lifetime. Assembling it in a temporarily-mut local and then freezing it before the call makes that boundary explicit: the config can't be accidentally mutated between "built" and "handed to hardware," a distinction worth having when the next line is a real register write that takes effect on physical silicon the moment it runs. Like in hosted Rust, this costs nothing at runtime — it's purely a binding annotation the compiler erases — so it applies equally well to a config built on the stack with no allocator in sight.

Basic usage example

struct UartConfig {
    baud_rate: u32,
    stop_bits: u8,
}

fn uart_init(config: UartConfig) {
    let _ = (config.baud_rate, config.stop_bits); // stand-in for a real register write
}

let mut config = UartConfig { baud_rate: 9_600, stop_bits: 1 }; // <- mut needed only while building the value
config.baud_rate = 115_200; // adjust before freezing
let config = config; // <- shadows the binding as immutable; nothing below this line can mutate it

uart_init(config);

Best practices & deeper information

Scenario

Creating a new object

A GPIO peripheral's configuration is assembled from several independent choices (mode, pull, speed) inside a block expression, and only the finished, read-only value is passed to the HAL's init call.

struct GpioConfig {
    output: bool,
    pull_up: bool,
    high_speed: bool,
}

let config = { // <- block expression scopes the mutable phase
    let mut cfg = GpioConfig { output: false, pull_up: false, high_speed: false };
    cfg.output = true;
    cfg.high_speed = true;
    cfg // last expression: becomes the outer, immutable `config`
};

// config is never `mut` from here on; the HAL init call receives a value that can't change underneath it
gpio_init(config);

fn gpio_init(config: GpioConfig) {
    let _ = (config.output, config.pull_up, config.high_speed);
}

Why this way: scoping the mut phase to the block keeps the mutation window visually contained right up to the point the value is handed to hardware — the Rust Design Patterns book documents this block-expression shape as the idiomatic way to build a value that needs temporary mutation, and it reads especially clearly right before a call that programs physical registers.

Scenario

Bit manipulation and flags

A peripheral's control-register value is built up bit by bit from several independent flags before being written out; once assembled, it should be treated as the fixed value that actually got written, not a live bitmask still open to change.

const ENABLE_BIT: u32 = 1 << 0;
const CLOCK_SRC_BIT: u32 = 1 << 4;

let mut ctrl_reg: u32 = 0;
ctrl_reg |= ENABLE_BIT;
ctrl_reg |= CLOCK_SRC_BIT;
let ctrl_reg = ctrl_reg; // <- shadowed immutable: this is now the exact value being written to hardware

// write_control_register(ctrl_reg); // stand-in for an actual volatile register write

Why this way: once a register value is finalized and written, further in-place mutation of the same binding would misleadingly suggest the live hardware state could still change through it — rebinding immutable turns "this is what's on the wire now" from a comment into something the compiler enforces for the rest of the scope.