ExamplesBy LevelBy TopicLearning Paths
550 Advanced

Cell and RefCell for Interior Mutability

Functional Programming

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

  • • How Cell<T> enables mutation through &self for Copy types using get/set
  • • How RefCell<T> enables runtime borrow checking via borrow() and borrow_mut()
  • • Why RefCell panics on borrow violations that &mut T would catch at compile time
  • • How Rc<RefCell<T>> is the standard single-threaded shared mutable container
  • • Where interior mutability is used: Rc<RefCell<T>> trees, caches, test mocks, Gtk/egui widgets
  • Code 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

  • Compile-time vs runtime: Rust's &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.
  • Performance: Cell<T> is zero overhead for Copy types; RefCell<T> adds a borrow counter; both are cheaper than Mutex<T> (which requires OS involvement).
  • Thread safety: 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).
  • API style: 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

  • OCaml uses garbage collection
  • Rust uses ownership and borrowing
  • Both support the core concept
  • Exercises

  • Memoize with Cell: Implement struct Memoized<T: Copy> { computed: Cell<Option<T>>, compute: Box<dyn Fn() -> T> } with a get() method that lazily computes and caches the value.
  • Observer with RefCell: Build an Observable<T: Clone> struct using RefCell<Vec<Box<dyn Fn(&T)>>> for the listener list, so listeners can be added through &self.
  • Rc<RefCell<T>> tree: Implement a simple linked list using type Node<T> = Rc<RefCell<NodeInner<T>>>; struct NodeInner<T> { value: T, next: Option<Node<T>> } with append and traverse methods.
  • Open Source Repos