Cell and RefCell for Interior Mutability
Tutorial Video
Text description (accessibility)
This video demonstrates the "Cell and RefCell for Interior Mutability" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Rust's ownership rules normally require `&mut T` for mutation — impossible when a value is shared via `Rc<T>` or multiple references. Key difference from OCaml: 1. **Compile
Tutorial
The Problem
Rust's ownership rules normally require &mut T for mutation — impossible when a value is shared via Rc<T> or multiple references. Interior mutability provides a controlled escape hatch: types that allow mutation through a shared reference (&T). Cell<T> works for Copy types by get/set semantics. RefCell<T> works for non-Copy types by moving the borrow check to runtime — it panics on violation rather than failing at compile time. These types are foundational in GUI frameworks, mock objects, memoization, and any structure requiring shared mutable access without a Mutex.
🎯 Learning Outcomes
Cell<T> enables mutation through &self for Copy types using get/setRefCell<T> enables runtime borrow checking via borrow() and borrow_mut()RefCell panics on borrow violations that &mut T would catch at compile timeRc<RefCell<T>> is the standard single-threaded shared mutable containerRc<RefCell<T>> trees, caches, test mocks, Gtk/egui widgetsCode Example
#![allow(clippy::all)]
//! Cell and RefCell for Interior Mutability
//!
//! Mutating through shared references.
use std::cell::{Cell, RefCell};
/// Cell for Copy types.
pub struct Counter {
value: Cell<i32>,
}
impl Counter {
pub fn new(value: i32) -> Self {
Counter {
value: Cell::new(value),
}
}
pub fn get(&self) -> i32 {
self.value.get()
}
pub fn set(&self, value: i32) {
self.value.set(value);
}
pub fn increment(&self) {
self.value.set(self.value.get() + 1);
}
}
/// RefCell for non-Copy types.
pub struct Cache {
data: RefCell<Vec<String>>,
}
impl Cache {
pub fn new() -> Self {
Cache {
data: RefCell::new(Vec::new()),
}
}
pub fn add(&self, item: String) {
self.data.borrow_mut().push(item);
}
pub fn get_all(&self) -> Vec<String> {
self.data.borrow().clone()
}
pub fn len(&self) -> usize {
self.data.borrow().len()
}
pub fn is_empty(&self) -> bool {
self.data.borrow().is_empty()
}
}
impl Default for Cache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counter() {
let counter = Counter::new(0);
counter.increment();
counter.increment();
assert_eq!(counter.get(), 2);
}
#[test]
fn test_cache() {
let cache = Cache::new();
cache.add("a".into());
cache.add("b".into());
assert_eq!(cache.len(), 2);
}
}Key Differences
&mut T rule is compile-time; RefCell<T> moves the same check to runtime (with panic on violation); OCaml has no borrow check — only runtime type safety.Cell<T> is zero overhead for Copy types; RefCell<T> adds a borrow counter; both are cheaper than Mutex<T> (which requires OS involvement).Cell and RefCell are !Send — single-threaded only; Mutex or RwLock are the thread-safe equivalents; OCaml's ref is accessible from any domain in OCaml 5.x (with race conditions possible).RefCell::borrow() returns a smart pointer Ref<T> that releases the borrow when dropped; OCaml ref reading is just !x — a simple dereference.OCaml Approach
OCaml's ref and mutable record fields provide interior mutability natively — no wrapper type needed:
type counter = { mutable value: int }
let increment c = c.value <- c.value + 1
let get c = c.value
Since OCaml does not track ownership, all values are freely mutable through any reference with no special wrapper.
Full Source
#![allow(clippy::all)]
//! Cell and RefCell for Interior Mutability
//!
//! Mutating through shared references.
use std::cell::{Cell, RefCell};
/// Cell for Copy types.
pub struct Counter {
value: Cell<i32>,
}
impl Counter {
pub fn new(value: i32) -> Self {
Counter {
value: Cell::new(value),
}
}
pub fn get(&self) -> i32 {
self.value.get()
}
pub fn set(&self, value: i32) {
self.value.set(value);
}
pub fn increment(&self) {
self.value.set(self.value.get() + 1);
}
}
/// RefCell for non-Copy types.
pub struct Cache {
data: RefCell<Vec<String>>,
}
impl Cache {
pub fn new() -> Self {
Cache {
data: RefCell::new(Vec::new()),
}
}
pub fn add(&self, item: String) {
self.data.borrow_mut().push(item);
}
pub fn get_all(&self) -> Vec<String> {
self.data.borrow().clone()
}
pub fn len(&self) -> usize {
self.data.borrow().len()
}
pub fn is_empty(&self) -> bool {
self.data.borrow().is_empty()
}
}
impl Default for Cache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counter() {
let counter = Counter::new(0);
counter.increment();
counter.increment();
assert_eq!(counter.get(), 2);
}
#[test]
fn test_cache() {
let cache = Cache::new();
cache.add("a".into());
cache.add("b".into());
assert_eq!(cache.len(), 2);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counter() {
let counter = Counter::new(0);
counter.increment();
counter.increment();
assert_eq!(counter.get(), 2);
}
#[test]
fn test_cache() {
let cache = Cache::new();
cache.add("a".into());
cache.add("b".into());
assert_eq!(cache.len(), 2);
}
}
Deep Comparison
OCaml vs Rust: lifetime cell refcell
See example.rs and example.ml for implementations.
Key Differences
Exercises
struct Memoized<T: Copy> { computed: Cell<Option<T>>, compute: Box<dyn Fn() -> T> } with a get() method that lazily computes and caches the value.Observable<T: Clone> struct using RefCell<Vec<Box<dyn Fn(&T)>>> for the listener list, so listeners can be added through &self.type Node<T> = Rc<RefCell<NodeInner<T>>>; struct NodeInner<T> { value: T, next: Option<Node<T>> } with append and traverse methods.