Skip to content

Commit

Permalink
go back to using RefCell
Browse files Browse the repository at this point in the history
  • Loading branch information
djmitche committed Jan 12, 2024
1 parent a473dd3 commit 0464c9e
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 32 deletions.
74 changes: 74 additions & 0 deletions orig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
minutes: 10
---

<!-- NOTES:
Introduce the concept, with an example based on Mutex showing an `&self` method doing mutation; reference Cell/RefCell without detail.
-->

# Interior Mutability

Rust provides a few safe means of modifying a value given only a shared
reference to that value. All of these replace compile-time checks with runtime
checks.

## `Cell` and `RefCell`

[`Cell`](https://doc.rust-lang.org/std/cell/struct.Cell.html) and
[`RefCell`](https://doc.rust-lang.org/std/cell/struct.RefCell.html) implement
what Rust calls _interior mutability:_ mutation of values in an immutable
context.

`Cell` is typically used for simple types, as it requires copying or moving
values. More complex interior types typically use `RefCell`, which tracks shared
and exclusive references at runtime and panics if they are misused.

```rust,editable
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug, Default)]
struct Node {
value: i64,
children: Vec<Rc<RefCell<Node>>>,
}
impl Node {
fn new(value: i64) -> Rc<RefCell<Node>> {
Rc::new(RefCell::new(Node { value, ..Node::default() }))
}
fn sum(&self) -> i64 {
self.value + self.children.iter().map(|c| c.borrow().sum()).sum::<i64>()
}
}
fn main() {
let root = Node::new(1);
root.borrow_mut().children.push(Node::new(5));
let subtree = Node::new(10);
subtree.borrow_mut().children.push(Node::new(11));
subtree.borrow_mut().children.push(Node::new(12));
root.borrow_mut().children.push(subtree);
println!("graph: {root:#?}");
println!("graph sum: {}", root.borrow().sum());
}
```

<details>

- If we were using `Cell` instead of `RefCell` in this example, we would have to
move the `Node` out of the `Rc` to push children, then move it back in. This
is safe because there's always one, un-referenced value in the cell, but it's
not ergonomic.
- To do anything with a Node, you must call a `RefCell` method, usually `borrow`
or `borrow_mut`.
- Demonstrate that reference loops can be created by adding `root` to
`subtree.children` (don't try to print it!).
- To demonstrate a runtime panic, add a `fn inc(&mut self)` that increments
`self.value` and calls the same method on its children. This will panic in the
presence of the reference loop, with
`thread 'main' panicked at 'already borrowed: BorrowMutError'`.

</details>
77 changes: 46 additions & 31 deletions src/borrowing/interior-mutability.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,72 @@ minutes: 10

# Interior Mutability

In some situations, exclusive (`mut`) access cannot be represented at compile
time. For example, data might be accessed from multiple threads. The "interior
mutability" pattern allows exclusive access behind a shared reference, using a
runtime check to enforce the borrowing rules.
In some situations, it's necessary to modify data behind a shared (read-only)
reference. For example, a shared data structure might have an internal cache,
and wish to update that cache from read-only methods.

## `Mutex`
The "interior mutability" pattern allows exclusive (mutable) access behind a
shared reference. The standard library provides several ways to do this, all
ensuring safety by performing a runtime check.

One standard type that supports interior mutability is `Mutex` (known as a lock
in some other languages). Many threads can have a shared reference to a `Mutex`,
and the lock operation performs a _runtime_ check that no other thread holds the
lock before granting access to the value inside.
## `RefCell`

```rust,editable
use std::sync::Mutex;
use std::cell::RefCell;
use std::rc::Rc;
fn handle_hit(hit_counter: &Mutex<u32>) {
let mut num_hits = hit_counter.lock().unwrap();
*num_hits += 1;
#[derive(Debug, Default)]
struct Node {
value: i64,
children: Vec<Rc<RefCell<Node>>>,
}
fn current_hit_count(hit_counter: &Mutex<u32>) -> u32 {
*hit_counter.lock().unwrap()
impl Node {
fn new(value: i64) -> Rc<RefCell<Node>> {
Rc::new(RefCell::new(Node { value, ..Node::default() }))
}
fn sum(&self) -> i64 {
self.value + self.children.iter().map(|c| c.borrow().sum()).sum::<i64>()
}
}
fn main() {
let hit_counter = Mutex::new(0);
handle_hit(&hit_counter);
println!("{} hits", current_hit_count(&hit_counter));
let root = Node::new(1);
root.borrow_mut().children.push(Node::new(5));
let subtree = Node::new(10);
subtree.borrow_mut().children.push(Node::new(11));
subtree.borrow_mut().children.push(Node::new(12));
root.borrow_mut().children.push(subtree);
println!("graph: {root:#?}");
println!("graph sum: {}", root.borrow().sum());
}
```

## `Cell` and `RefCell`
## `Cell`

`Cell` wraps a value and allows getting or setting the value, even with a shared
reference to the `Cell`. Howerver, it does not allow any references to the
value. If there are no references, then borrowing rules cannot be broken.
value. Since there are no references, borrowing rules cannot be broken.

The `RefCell` type also provides interior mutability. Its `borrow` method allows
multiple shared references to the data it contains, while its `borrow_mut`
allows a single exclusive reference. It checks the borrowing rules at runtime
and panics if they are violated.
<details>

Neither `Cell` nor `RefCell` can be shared between threads.
- `RefCell` enforces Rust's usual borrowing rules (either multiple shared
references or a single exclusive reference) with a runtime check. In this
case, all borrows are very short and never overlap, so the checks always
succeed.

<details>
- `Rc` only allows shared (read-only) access to its contents, since its purpose
is to allow (and count) many references. But we want to modify the value, so
we need interior mutability.

- While the fundamentals course doesn't cover concurrency, most students will
have seen a mutex or lock before, and understand that it checks for an
existing lock at runtime, which is the key to interior mutability.
- Demonstrate that reference loops can be created by adding `root` to
`subtree.children` (don't try to print it!).

- The cell types are more unusual, if simpler. `RefCell` is, in effect, a
`RWMutex` that panics instead of blocking.
- To demonstrate a runtime panic, add a `fn inc(&mut self)` that increments
`self.value` and calls the same method on its children. This will panic in the
presence of the reference loop, with
`thread 'main' panicked at 'already borrowed: BorrowMutError'`.

</details>
3 changes: 2 additions & 1 deletion src/concurrency/shared_state/mutex.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# `Mutex`

[`Mutex<T>`][1] ensures mutual exclusion _and_ allows mutable access to `T`
behind a read-only interface:
behind a read-only interface (another form of
[interior mutability](../../borrowing/interior-mutability)):

```rust,editable
use std::sync::Mutex;
Expand Down

0 comments on commit 0464c9e

Please sign in to comment.