110-cell-interior — Cell<T>: Interior Mutability for Copy Types
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
Cell<T> to mutate a field through a shared &self referenceCell<T> works only with Copy types (no references handed out)Cell<T> with RefCell<T> (works with non-Copy, runtime borrow check)Cell<T> to lazy counters, memoization flags, and generation countersCode 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
Cell<T> makes interior mutability explicit at the type level; OCaml's ref and mutable fields are natural and pervasive.Cell bypasses the borrow checker's mutation rules; OCaml has no equivalent restriction to bypass.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);
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| Mutable cell | 'a ref | Cell<T> where T: Copy |
| Read cell | !r (dereference) | cell.get() |
| Write cell | r := value | cell.set(value) |
| Mutable record field | mutable field : t | field: Cell<T> |
| Swap and return old | let old = !r in r := v; old | cell.replace(v) |
| Function receiver | passes record by value or ref | &self (shared ref) |
Key Insights
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.
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.
&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.
RefCell<T>, Cell<T> performs no borrow-countbookkeeping at runtime. The safety guarantee comes entirely from the copy-only API at compile time.
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
Cell<Option<i32>> that computes on first access and caches the result.GenerationCounter struct with an immutable &self increment() method using Cell<u32>.Cell<String> does not work and how RefCell<String> solves it — demonstrate the runtime borrow-check panic.