02 / 20 · Day 1
Day 1 · Concept 02

Variables, types, mutability

Immutable by default. let mut opts in. Shadowing lets you reuse a name with a new type — encouraged, not avoided. Static types with strong inference everywhere. Conversions are explicit: the compiler never silently truncates.


1 · The intuition

In Rust, variables are immutable by default. That's not a style choice — it's a compile-time invariant the borrow checker leans on. To mutate, you write let mut. The opposite of Go's "everything is mutable, except const". Once you stop fighting it, it forces you to think about where mutation lives.

Three keywords. let binds a name. let mut binds a mutable name. const declares a compile-time constant. There's also static for module-level data with a fixed address — rare in day-to-day Rust.

2 · Try it

rust src/main.rs
fn main() {
    // Immutable — the default
    let name = "Rust";

    // Mutable
    let mut count = 0;
    count += 1;
    count += 1;

    // Type annotation when you want it
    let age: u8 = 30;

    // Shadowing — reuse the name, possibly with a new type
    let value = "5";
    let value: i32 = value.parse().unwrap();
    let value = value + 10;

    println!("{} count={} age={} value={}", name, count, age, value);
}

3 · The numeric types

SignedUnsignedUse for
i8 / i16 / i32 / i64 / i128u8 / u16 / u32 / u64 / u128Specific widths — protocols, bit-packing.
isizeusizePointer-sized — array indices, lengths.
f32 / f64Floats. f64 is the default.
booltrue / false. 1 byte.
charA single Unicode code point, 4 bytes.

Default integer is i32. Default float is f64. Use usize for anything index-shaped — Rust will complain when you mix usize with smaller integers.

4 · Explicit conversions with as

rust src/main.rs
fn main() {
    let x: i32 = 42;
    let y: u8 = x as u8;     // explicit, may truncate
    let z: f64 = x as f64;

    println!("x={} y={} z={}", x, y, z);

    // Won't compile:
    // let bad: u8 = x;  // mismatched types
}

Unlike C, Rust never auto-converts. Even i32 to i64 needs an as. The verbosity is intentional — every potential loss of information is visible in the source.

5 · Shadowing — the underrated feature

rust src/main.rs · shadowing
fn main() {
    let spaces = "   ";              // a &str
    let spaces = spaces.len();       // now a usize — same name, different type!

    println!("{}", spaces);          // 3
}

In most languages re-binding with a new type would be a workaround. In Rust it's the idiom — you avoid intermediate names like spaces_count and the compiler still type-checks every line.

Shadowing vs let mut. Shadowing creates a new binding (different memory, different type allowed). let mut reassigns the same binding (same type, same memory). Pick shadowing when the type changes; pick mut when the value changes but the type stays.

6 · Constants & statics

rust src/main.rs · constants
// const — inlined at compile time, type required, must be a const-expr
const MAX_USERS: u32 = 100_000;
const PI: f64 = 3.14159;

// static — lives at a fixed address for the program's lifetime
static GREETING: &str = "Hello";

// static mut — possible, but unsafe (needs unsafe blocks to touch)
static mut COUNTER: u32 = 0;

Shows in numeric literals are visual grouping: 1_000_000 is one million. The compiler ignores them.

7 · Common mistakes

  • Trying to mutate without mut. let x = 0; x = 1; won't compile. Add mut.
  • Forgetting the explicit cast. Mixing numeric types needs as every time. No implicit promotion.
  • Confusing const and let. const must be a compile-time expression, type-annotated, written in SCREAMING_SNAKE. let is runtime binding.
  • Integer overflow. In debug builds, overflow panics. In release builds, it wraps. Use checked_add / saturating_add / wrapping_add when you care.
  • Comparing different numeric types. let a: i32 = 5; let b: usize = 5; a == b — doesn't compile. Convert one side.

8 · Exercises (~10 min)

  1. Fahrenheit-to-Celsius. Write a function that takes f64 and returns f64. Use it in main.
  2. The shadow trick. Read a string from std::env::args(), shadow it to an i32 via parse, then shadow again with n * 2. Three lines, three types, one name.
  3. Overflow on purpose. Set let x: u8 = 255; let y = x + 1;. Run debug. Run cargo run --release. Note the difference.
  4. The bool size. Use std::mem::size_of::<bool>(). Report. Now char — guess before running.

9 · When it clicks

  • You write let by reflex; mut only where you mutate.
  • You stop being surprised that i32 and i64 are different types.
  • Shadowing for type-conversion feels normal, not weird.
  • You can predict integer-overflow behaviour for the build mode you're in.
Found this useful?