ExamplesBy LevelBy TopicLearning Paths
110 Intermediate

110-cell-interior — Cell<T>: Interior Mutability for Copy Types

Functional Programming

Tutorial

The Problem

Rust's borrow checker prevents mutation through shared references (&T). But sometimes you need to update a field in a struct that is shared by multiple callers — a memoized cache, a call counter, or a lazy-initialized field — without needing &mut self. Interior mutability provides a controlled escape hatch.

Cell<T> is the simplest interior mutability primitive: it allows mutation through a shared reference by enforcing a rule that prevents multiple mutable accesses simultaneously (via the get/set API that never hands out references to the interior).

🎯 Learning Outcomes

  • • Understand what interior mutability is and why it is needed
  • • Use Cell<T> to mutate a field through a shared &self reference
  • • Know that Cell<T> works only with Copy types (no references handed out)
  • • Contrast Cell<T> with RefCell<T> (works with non-Copy, runtime borrow check)
  • • Apply Cell<T> to lazy counters, memoization flags, and generation counters
  • Code Example

    use std::cell::Cell;
    
    // Immutable binding, mutable interior
    let counter = Cell::new(0u32);
    counter.set(counter.get() + 1);
    counter.set(counter.get() + 1);
    assert_eq!(counter.get(), 2);
    
    // Struct with selectively mutable field
    struct Config { name: String, call_count: Cell<u32> }
    fn use_config(c: &Config) {           // shared ref — no &mut needed
        c.call_count.set(c.call_count.get() + 1);
    }

    Key Differences

  • Explicitness: Rust's Cell<T> makes interior mutability explicit at the type level; OCaml's ref and mutable fields are natural and pervasive.
  • Borrow-check bypass: Cell bypasses the borrow checker's mutation rules; OCaml has no equivalent restriction to bypass.
  • Thread safety: Cell<T> is !Sync (not thread-safe); OCaml's ref is also not thread-safe but the GC protects from use-after-free.
  • **Copy restriction**: Cell<T> requires T: Copy because get returns by value (never handing out &T); RefCell<T> removes this restriction with runtime cost.
  • OCaml Approach

    OCaml's ref is the direct equivalent: a mutable cell that can be updated through any binding:

    let counter = ref 0   (* mutable ref — no 'mut' annotation needed *)
    counter := !counter + 1
    counter := !counter + 1
    Printf.printf "%d
    " !counter  (* 2 *)
    

    OCaml record fields can be declared mutable:

    type config = { name: string; mutable call_count: int }
    let process cfg = cfg.call_count <- cfg.call_count + 1
    

    There is no distinction between Cell and RefCell in OCaml — all mutable state is accessible through any binding.

    Full Source

    #![allow(clippy::all)]
    // Example 110: Cell<T> — Interior Mutability for Copy Types
    //
    // Cell<T> allows mutation through a shared reference (&T).
    // It works only with Copy types and avoids runtime borrow-check overhead
    // by never handing out references to the interior — values are only moved
    // in and out with `set` / `get`.
    
    use std::cell::Cell;
    
    // ── Approach 1: Simple counter ────────────────────────────────────────────────
    //
    // Mirrors OCaml `let counter = ref 0`.  Here the binding is immutable (`let`),
    // yet `Cell::set` can still update the interior value.  The key insight:
    // `Cell` wraps the mutation, so the *binding* never needs `mut`.
    
    pub fn counter_demo() -> u32 {
        let counter = Cell::new(0u32);
        counter.set(counter.get() + 1);
        counter.set(counter.get() + 1);
        counter.get()
    }
    
    // ── Approach 2: Mutable field inside an otherwise-immutable struct ────────────
    //
    // `Config` can be shared via `&Config` (multiple callers, no `mut` required),
    // yet `call_count` tracks how many times it has been used.
    // This is the classic "shared-but-selectively-mutable" pattern in Rust.
    
    pub struct Config {
        pub name: String,
        pub call_count: Cell<u32>,
    }
    
    impl Config {
        pub fn new(name: &str) -> Self {
            Config {
                name: name.to_string(),
                call_count: Cell::new(0),
            }
        }
    
        // Takes `&self` (shared reference) yet increments the counter.
        // Without Cell we would need `&mut self`, preventing sharing.
        pub fn use_it(&self) {
            self.call_count.set(self.call_count.get() + 1);
        }
    
        pub fn count(&self) -> u32 {
            self.call_count.get()
        }
    }
    
    // ── Approach 3: Lazy / cached computation ────────────────────────────────────
    //
    // Mirrors the OCaml pattern of storing `None` initially and replacing with
    // `Some(computed)` on first access.  `Cell<Option<T>>` works here because
    // `Option<T>` is `Copy` when `T: Copy`.
    
    pub struct CachedSquare {
        input: i32,
        cache: Cell<Option<i32>>,
    }
    
    impl CachedSquare {
        pub fn new(input: i32) -> Self {
            CachedSquare {
                input,
                cache: Cell::new(None),
            }
        }
    
        // Expensive computation (simulated).  Result is stored on first call.
        pub fn get(&self) -> i32 {
            match self.cache.get() {
                Some(v) => v,
                None => {
                    let v = self.input * self.input;
                    self.cache.set(Some(v));
                    v
                }
            }
        }
    }
    
    // ── Approach 4: Cell as a flag (bool) ─────────────────────────────────────────
    //
    // `bool` is Copy, so `Cell<bool>` is a lightweight, non-atomic toggle.
    // Useful for visited flags in traversal, or once-only guards, without
    // the overhead of a Mutex.
    
    pub fn toggle_demo() -> (bool, bool) {
        let flag = Cell::new(false);
        let before = flag.get();
        flag.set(!flag.get());
        let after = flag.get();
        (before, after)
    }
    
    // ─────────────────────────────────────────────────────────────────────────────
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_counter_increments_via_shared_ref() {
            // Cell::set works through &Cell<T> — no `mut` binding needed.
            let c = Cell::new(0u32);
            let r = &c; // shared reference
            r.set(r.get() + 10);
            r.set(r.get() + 5);
            assert_eq!(c.get(), 15);
        }
    
        #[test]
        fn test_counter_demo_returns_two() {
            assert_eq!(counter_demo(), 2);
        }
    
        #[test]
        fn test_config_call_count_through_shared_ref() {
            let cfg = Config::new("test");
            let r1 = &cfg;
            let r2 = &cfg; // two shared refs at the same time — allowed!
            r1.use_it();
            r2.use_it();
            r1.use_it();
            assert_eq!(cfg.count(), 3);
        }
    
        #[test]
        fn test_config_starts_at_zero() {
            let cfg = Config::new("fresh");
            assert_eq!(cfg.count(), 0);
        }
    
        #[test]
        fn test_cached_square_computed_once() {
            let cs = CachedSquare::new(7);
            // First call computes, subsequent calls return cached value.
            assert_eq!(cs.get(), 49);
            assert_eq!(cs.get(), 49); // from cache
                                      // Confirm the cache cell is now Some.
            assert_eq!(cs.cache.get(), Some(49));
        }
    
        #[test]
        fn test_cached_square_negative_input() {
            let cs = CachedSquare::new(-4);
            assert_eq!(cs.get(), 16);
        }
    
        #[test]
        fn test_toggle_demo() {
            let (before, after) = toggle_demo();
            assert!(!before);
            assert!(after);
        }
    
        #[test]
        fn test_cell_replace() {
            // Cell::replace returns the old value — handy for swap patterns.
            let c = Cell::new(42i32);
            let old = c.replace(99);
            assert_eq!(old, 42);
            assert_eq!(c.get(), 99);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_counter_increments_via_shared_ref() {
            // Cell::set works through &Cell<T> — no `mut` binding needed.
            let c = Cell::new(0u32);
            let r = &c; // shared reference
            r.set(r.get() + 10);
            r.set(r.get() + 5);
            assert_eq!(c.get(), 15);
        }
    
        #[test]
        fn test_counter_demo_returns_two() {
            assert_eq!(counter_demo(), 2);
        }
    
        #[test]
        fn test_config_call_count_through_shared_ref() {
            let cfg = Config::new("test");
            let r1 = &cfg;
            let r2 = &cfg; // two shared refs at the same time — allowed!
            r1.use_it();
            r2.use_it();
            r1.use_it();
            assert_eq!(cfg.count(), 3);
        }
    
        #[test]
        fn test_config_starts_at_zero() {
            let cfg = Config::new("fresh");
            assert_eq!(cfg.count(), 0);
        }
    
        #[test]
        fn test_cached_square_computed_once() {
            let cs = CachedSquare::new(7);
            // First call computes, subsequent calls return cached value.
            assert_eq!(cs.get(), 49);
            assert_eq!(cs.get(), 49); // from cache
                                      // Confirm the cache cell is now Some.
            assert_eq!(cs.cache.get(), Some(49));
        }
    
        #[test]
        fn test_cached_square_negative_input() {
            let cs = CachedSquare::new(-4);
            assert_eq!(cs.get(), 16);
        }
    
        #[test]
        fn test_toggle_demo() {
            let (before, after) = toggle_demo();
            assert!(!before);
            assert!(after);
        }
    
        #[test]
        fn test_cell_replace() {
            // Cell::replace returns the old value — handy for swap patterns.
            let c = Cell::new(42i32);
            let old = c.replace(99);
            assert_eq!(old, 42);
            assert_eq!(c.get(), 99);
        }
    }

    Deep Comparison

    OCaml vs Rust: Cell<T> — Interior Mutability for Copy Types

    Side-by-Side Code

    OCaml

    (* OCaml ref — a mutable cell, always heap-allocated *)
    let counter = ref 0
    let () =
      counter := !counter + 1;
      counter := !counter + 1;
      assert (!counter = 2)
    
    (* Mutable field in a record *)
    type config = { name : string; mutable call_count : int }
    let use_config c =
      c.call_count <- c.call_count + 1
    

    Rust (idiomatic — Cell<T>)

    use std::cell::Cell;
    
    // Immutable binding, mutable interior
    let counter = Cell::new(0u32);
    counter.set(counter.get() + 1);
    counter.set(counter.get() + 1);
    assert_eq!(counter.get(), 2);
    
    // Struct with selectively mutable field
    struct Config { name: String, call_count: Cell<u32> }
    fn use_config(c: &Config) {           // shared ref — no &mut needed
        c.call_count.set(c.call_count.get() + 1);
    }
    

    Rust (functional / cached)

    use std::cell::Cell;
    
    struct CachedSquare { input: i32, cache: Cell<Option<i32>> }
    
    impl CachedSquare {
        fn get(&self) -> i32 {
            match self.cache.get() {
                Some(v) => v,
                None => {
                    let v = self.input * self.input;
                    self.cache.set(Some(v));
                    v
                }
            }
        }
    }
    

    Type Signatures

    ConceptOCamlRust
    Mutable cell'a refCell<T> where T: Copy
    Read cell!r (dereference)cell.get()
    Write cellr := valuecell.set(value)
    Mutable record fieldmutable field : tfield: Cell<T>
    Swap and return oldlet old = !r in r := v; oldcell.replace(v)
    Function receiverpasses record by value or ref&self (shared ref)

    Key Insights

  • **No mut binding required.** In OCaml every ref is implicitly mutable;
  • in Rust a Cell<T> binding can be immutable (let c = Cell::new(0)) yet still accept set calls. The mutability lives inside the type, not on the binding.

  • Copy-only constraint. OCaml ref works for any type. Cell<T> requires
  • T: Copy because it can only move values in/out — it never hands out a reference to the interior, which is exactly how it sidesteps the borrow checker's aliasing rules.

  • Shared-reference mutation. Rust normally forbids mutating through &T.
  • Cell is the explicit exception for single-threaded code: cell.set(v) compiles on &Cell<T>. The OCaml equivalent is a mutable record field or a ref value stored in a record — mutation through any alias is always allowed.

  • No runtime cost. Unlike RefCell<T>, Cell<T> performs no borrow-count
  • bookkeeping at runtime. The safety guarantee comes entirely from the copy-only API at compile time.

  • **Not Sync.** Cell<T> is !Sync, so it cannot be shared across threads.
  • For multi-threaded use, reach for Mutex<T> or AtomicT; for single-threaded non-Copy types use RefCell<T>. OCaml's GIL (in the classic runtime) means refs are also not truly thread-safe without explicit coordination.

    When to Use Each Style

    **Use Cell<T> when:** you have a Copy field (counters, flags, cached numeric results) that must be mutable through a shared reference in single-threaded code and you want zero runtime overhead.

    **Use RefCell<T> when:** the inner type is not Copy (e.g. String, Vec) and you are still in a single-threaded context.

    **Use OCaml ref / mutable when:** you are in OCaml — every value can be wrapped in a ref without type-system restrictions; there is no Copy/non-Copy distinction to worry about.

    Exercises

  • Implement a lazy-initialized field using Cell<Option<i32>> that computes on first access and caches the result.
  • Create a GenerationCounter struct with an immutable &self increment() method using Cell<u32>.
  • Show why Cell<String> does not work and how RefCell<String> solves it — demonstrate the runtime borrow-check panic.
  • Open Source Repos