ExamplesBy LevelBy TopicLearning Paths
111 Advanced

RefCell<T> — Runtime Borrow Checking

OwnershipBorrowingInterior Mutability

Tutorial Video

Text description (accessibility)

This video demonstrates the "RefCell<T> — Runtime Borrow Checking" functional Rust example. Difficulty level: Advanced. Key concepts covered: Ownership, Borrowing, Interior Mutability. Enable mutation through a shared (`&self`) reference by deferring Rust's borrow rules from compile time to runtime, using `RefCell<T>` to enforce "one writer XOR many readers" dynamically. Key difference from OCaml: 1. **Borrow enforcement:** OCaml enforces nothing; Rust enforces "one writer XOR multiple readers" — just at runtime instead of compile time with `RefCell`.

Tutorial

The Problem

Enable mutation through a shared (&self) reference by deferring Rust's borrow rules from compile time to runtime, using RefCell<T> to enforce "one writer XOR many readers" dynamically.

🎯 Learning Outcomes

  • • Understand when the borrow checker can't prove safety but the programmer can
  • • Use borrow() / borrow_mut() to obtain guarded references at runtime
  • • Design structs with &self mutation (no &mut self needed) via RefCell
  • • Use try_borrow() / try_borrow_mut() for fallible, panic-free access
  • 🦀 The Rust Way

    RefCell<T> wraps a value and hands out Ref<T> (shared) or RefMut<T> (exclusive) guard objects. The counts are tracked at runtime; any attempt to hold a mutable borrow alongside any other borrow causes an immediate panic. Sequential borrows — where each guard is dropped before the next is acquired — are always safe.

    Code Example

    use std::cell::RefCell;
    
    pub fn collect_items() -> Vec<String> {
        let items: RefCell<Vec<String>> = RefCell::new(Vec::new());
        items.borrow_mut().push("first".to_string());
        items.borrow_mut().push("second".to_string());
        items.borrow_mut().push("third".to_string());
        let borrowed = items.borrow();
        borrowed.clone()
    }

    Key Differences

  • Borrow enforcement: OCaml enforces nothing; Rust enforces "one writer XOR multiple readers" — just at runtime instead of compile time with RefCell.
  • Receiver type: OCaml mutable field methods implicitly allow mutation; Rust requires &mut self unless RefCell provides interior mutability, enabling &self methods.
  • Failure mode: OCaml allows races silently; Rust panics immediately on double-mutable-borrow, making bugs loud and reproducible.
  • Fallible API: try_borrow() / try_borrow_mut() let library code handle contention gracefully rather than panicking.
  • OCaml Approach

    OCaml has no borrow rules — a ref value or mutable record field can be read and written freely at any time. The programmer bears full responsibility for correctness. This makes code concise but removes the compile-time safety net that Rust provides.

    Full Source

    #![allow(clippy::all)]
    // Example 111: RefCell<T> — Runtime Borrow Checking
    //
    // RefCell<T> enforces Rust's borrowing rules at runtime instead of compile time,
    // enabling interior mutability for non-Copy types.
    //
    // Rule: either multiple Ref<T> OR one RefMut<T> — never both.
    // Violation = panic at runtime (same rule as borrow checker, just deferred).
    
    use std::cell::RefCell;
    
    // ---------------------------------------------------------------------------
    // Approach 1: Interior mutability — mutate through a shared reference
    //
    // The `items` binding is immutable, but the Vec inside can be mutated.
    // Useful when you need to share a collector across callbacks or closures
    // without making the binding itself `mut`.
    // ---------------------------------------------------------------------------
    pub fn collect_items() -> Vec<String> {
        // Immutable binding — the RefCell *is* the mutability
        let items: RefCell<Vec<String>> = RefCell::new(Vec::new());
    
        // Each borrow_mut() returns a RefMut guard; it releases when dropped
        items.borrow_mut().push("first".to_string());
        items.borrow_mut().push("second".to_string());
        items.borrow_mut().push("third".to_string());
    
        // borrow() returns a shared Ref guard; bind before returning to avoid lifetime issue
        let borrowed = items.borrow();
        borrowed.clone()
    }
    
    // ---------------------------------------------------------------------------
    // Approach 2: Shared mutable stack
    //
    // Stack<T> stores its data in a RefCell so push/pop can take &self
    // instead of &mut self — multiple owners can share one Stack via Rc<Stack<T>>.
    // ---------------------------------------------------------------------------
    pub struct Stack<T> {
        data: RefCell<Vec<T>>,
    }
    
    impl<T> Stack<T> {
        pub fn new() -> Self {
            Stack {
                data: RefCell::new(Vec::new()),
            }
        }
    
        pub fn push(&self, value: T) {
            self.data.borrow_mut().push(value);
        }
    
        pub fn pop(&self) -> Option<T> {
            self.data.borrow_mut().pop()
        }
    
        pub fn peek(&self) -> Option<T>
        where
            T: Clone,
        {
            self.data.borrow().last().cloned()
        }
    
        pub fn len(&self) -> usize {
            self.data.borrow().len()
        }
    
        pub fn is_empty(&self) -> bool {
            self.data.borrow().is_empty()
        }
    }
    
    impl<T> Default for Stack<T> {
        fn default() -> Self {
            Self::new()
        }
    }
    
    // ---------------------------------------------------------------------------
    // Approach 3: Shared observer — multiple immutable handles, one mutable log
    //
    // Demonstrates why RefCell is indispensable: an observer that records events
    // while appearing immutable to the subjects it watches.
    // ---------------------------------------------------------------------------
    pub struct EventLog {
        events: RefCell<Vec<String>>,
    }
    
    impl EventLog {
        pub fn new() -> Self {
            EventLog {
                events: RefCell::new(Vec::new()),
            }
        }
    
        // Takes &self — caller sees an immutable observer
        pub fn record(&self, event: &str) {
            self.events.borrow_mut().push(event.to_string());
        }
    
        pub fn entries(&self) -> Vec<String> {
            self.events.borrow().clone()
        }
    
        pub fn count(&self) -> usize {
            self.events.borrow().len()
        }
    }
    
    impl Default for EventLog {
        fn default() -> Self {
            Self::new()
        }
    }
    
    // ---------------------------------------------------------------------------
    // Helper: demonstrate try_borrow to avoid panics
    // ---------------------------------------------------------------------------
    pub fn try_borrow_example() -> Result<usize, String> {
        let cell: RefCell<Vec<i32>> = RefCell::new(vec![1, 2, 3]);
    
        // Exclusive borrow held across this scope
        let _writer = cell.borrow_mut();
    
        // try_borrow returns Err rather than panicking
        // Bind to local to avoid borrow-outlive-local issue
        let result = match cell.try_borrow() {
            Ok(r) => Ok(r.len()),
            Err(e) => Err(format!("borrow failed: {e}")),
        };
        result
    }
    
    // ---------------------------------------------------------------------------
    // Tests
    // ---------------------------------------------------------------------------
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_collect_items_order() {
            let items = collect_items();
            assert_eq!(items, vec!["first", "second", "third"]);
        }
    
        #[test]
        fn test_collect_items_length() {
            let items = collect_items();
            assert_eq!(items.len(), 3);
        }
    
        #[test]
        fn test_stack_push_pop() {
            let s: Stack<i32> = Stack::new();
            assert!(s.is_empty());
    
            s.push(1);
            s.push(2);
            s.push(3);
    
            assert_eq!(s.len(), 3);
            assert_eq!(s.pop(), Some(3));
            assert_eq!(s.pop(), Some(2));
            assert_eq!(s.len(), 1);
        }
    
        #[test]
        fn test_stack_peek_does_not_remove() {
            let s: Stack<&str> = Stack::new();
            s.push("hello");
            s.push("world");
    
            assert_eq!(s.peek(), Some("world"));
            assert_eq!(s.len(), 2);
        }
    
        #[test]
        fn test_stack_empty_pop() {
            let s: Stack<u8> = Stack::new();
            assert_eq!(s.pop(), None);
            assert_eq!(s.peek(), None);
        }
    
        #[test]
        fn test_event_log_records_in_order() {
            let log = EventLog::new();
            log.record("connect");
            log.record("query");
            log.record("disconnect");
    
            assert_eq!(log.count(), 3);
            assert_eq!(log.entries(), vec!["connect", "query", "disconnect"]);
        }
    
        #[test]
        fn test_event_log_immutable_receiver() {
            let log = EventLog::new();
            let log_ref: &EventLog = &log;
            log_ref.record("event-a");
            log_ref.record("event-b");
            assert_eq!(log.count(), 2);
        }
    
        #[test]
        fn test_try_borrow_returns_err_when_mutably_borrowed() {
            let result = try_borrow_example();
            assert!(result.is_err());
            let msg = result.unwrap_err();
            assert!(msg.contains("borrow failed"));
        }
    
        #[test]
        fn test_multiple_shared_borrows_allowed() {
            let cell: RefCell<Vec<i32>> = RefCell::new(vec![10, 20, 30]);
            let r1 = cell.borrow();
            let r2 = cell.borrow();
            assert_eq!(r1.len(), r2.len());
            assert_eq!(*r1, *r2);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_collect_items_order() {
            let items = collect_items();
            assert_eq!(items, vec!["first", "second", "third"]);
        }
    
        #[test]
        fn test_collect_items_length() {
            let items = collect_items();
            assert_eq!(items.len(), 3);
        }
    
        #[test]
        fn test_stack_push_pop() {
            let s: Stack<i32> = Stack::new();
            assert!(s.is_empty());
    
            s.push(1);
            s.push(2);
            s.push(3);
    
            assert_eq!(s.len(), 3);
            assert_eq!(s.pop(), Some(3));
            assert_eq!(s.pop(), Some(2));
            assert_eq!(s.len(), 1);
        }
    
        #[test]
        fn test_stack_peek_does_not_remove() {
            let s: Stack<&str> = Stack::new();
            s.push("hello");
            s.push("world");
    
            assert_eq!(s.peek(), Some("world"));
            assert_eq!(s.len(), 2);
        }
    
        #[test]
        fn test_stack_empty_pop() {
            let s: Stack<u8> = Stack::new();
            assert_eq!(s.pop(), None);
            assert_eq!(s.peek(), None);
        }
    
        #[test]
        fn test_event_log_records_in_order() {
            let log = EventLog::new();
            log.record("connect");
            log.record("query");
            log.record("disconnect");
    
            assert_eq!(log.count(), 3);
            assert_eq!(log.entries(), vec!["connect", "query", "disconnect"]);
        }
    
        #[test]
        fn test_event_log_immutable_receiver() {
            let log = EventLog::new();
            let log_ref: &EventLog = &log;
            log_ref.record("event-a");
            log_ref.record("event-b");
            assert_eq!(log.count(), 2);
        }
    
        #[test]
        fn test_try_borrow_returns_err_when_mutably_borrowed() {
            let result = try_borrow_example();
            assert!(result.is_err());
            let msg = result.unwrap_err();
            assert!(msg.contains("borrow failed"));
        }
    
        #[test]
        fn test_multiple_shared_borrows_allowed() {
            let cell: RefCell<Vec<i32>> = RefCell::new(vec![10, 20, 30]);
            let r1 = cell.borrow();
            let r2 = cell.borrow();
            assert_eq!(r1.len(), r2.len());
            assert_eq!(*r1, *r2);
        }
    }

    Deep Comparison

    OCaml vs Rust: RefCell<T> — Runtime Borrow Checking

    Side-by-Side Code

    OCaml

    (* OCaml: mutable reference inside an immutable binding *)
    let collect_items () =
      let items = ref [] in
      items := "first" :: !items;
      items := "second" :: !items;
      items := "third" :: !items;
      List.rev !items
    
    (* Shared mutable stack with mutable record field *)
    type 'a stack = { mutable data : 'a list }
    let push s x = s.data <- x :: s.data
    let pop s = match s.data with
      | [] -> None
      | x :: rest -> s.data <- rest; Some x
    

    Rust (idiomatic — RefCell interior mutability)

    use std::cell::RefCell;
    
    pub fn collect_items() -> Vec<String> {
        let items: RefCell<Vec<String>> = RefCell::new(Vec::new());
        items.borrow_mut().push("first".to_string());
        items.borrow_mut().push("second".to_string());
        items.borrow_mut().push("third".to_string());
        let borrowed = items.borrow();
        borrowed.clone()
    }
    

    Rust (functional — interior-mutable Stack via &self)

    pub struct Stack<T> {
        data: RefCell<Vec<T>>,
    }
    
    impl<T> Stack<T> {
        pub fn push(&self, value: T) {          // &self, not &mut self
            self.data.borrow_mut().push(value);
        }
        pub fn pop(&self) -> Option<T> {
            self.data.borrow_mut().pop()
        }
    }
    

    Type Signatures

    ConceptOCamlRust
    Mutable bindinglet x = ref valuelet x = RefCell::new(value)
    Read access!x (deref ref)x.borrow()Ref<T>
    Write accessx := new_valx.borrow_mut()RefMut<T>
    Mutable fieldmutable field : 'afield: RefCell<T>
    Borrow violation— (no rule)Runtime panic
    Safe fallible borrowtry_borrow()Result<Ref<T>, BorrowError>

    Key Insights

  • OCaml has no borrow rules — any ref value can be read and written freely at any point; mutation safety is the programmer's responsibility alone.
  • RefCell defers the borrow checker to runtime — Rust's "one writer XOR many readers" rule is enforced when borrow() / borrow_mut() is called, not at compile time. Violation panics rather than failing to compile.
  • **Interior mutability unlocks &self mutation** — Stack::push(&self) can mutate internal state without requiring &mut self, enabling shared ownership via Rc<Stack<T>> without Rc<RefCell<Stack<T>>>.
  • **try_borrow avoids panics** — when the borrow outcome is uncertain (e.g., in library code), try_borrow() returns Result instead of panicking, mirroring OCaml's implicit "it just works" but with explicit error handling.
  • **RAII guards — Ref<T> and RefMut<T>** — borrows are tracked via guard objects that decrement the borrow count when dropped, so sequential borrow_mut() calls in separate statements never overlap.
  • When to Use Each Style

    **Use RefCell::borrow_mut() directly** when mutating through a non-shared local value and you want clarity that each statement releases its borrow before the next starts.

    **Use RefCell inside a struct** when you need &self methods that still mutate state — most commonly for mock/spy objects in tests, observers/loggers, and data structures shared via Rc.

    Exercises

  • Build a simple event bus using Rc<RefCell<Vec<Box<dyn Fn(&Event)>>>> that allows subscribers to register closures and the bus to broadcast events to all of them.
  • Implement a mutable graph using HashMap<NodeId, RefCell<Node>> where edges can be added at runtime, and write a BFS traversal that borrows each node only when needed.
  • Deliberately trigger a RefCell borrow panic by holding a mutable borrow and attempting a second mutable borrow in the same scope; then refactor the code to avoid the panic using split borrows or restructuring.
  • Open Source Repos