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 3837e5d
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 34 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>
80 changes: 47 additions & 33 deletions src/borrowing/interior-mutability.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,57 +4,71 @@ 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.
`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. 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 3837e5d

Please sign in to comment.