Skip to content

Commit bb4db3d

Browse files
tall-vasetall-vasegribozavr
authored
"borrow checker invariants" section of the "leveraging the type system" chapter (#2867)
Adds materials on the "leveraging the type system/borrow checker invariants" subject. I'm still calibrating what's expected subject-and-style wise, so do spell out things where I've drifted off mark. --------- Co-authored-by: tall-vase <[email protected]> Co-authored-by: Dmitri Gribenko <[email protected]>
1 parent 03cd040 commit bb4db3d

File tree

9 files changed

+749
-0
lines changed

9 files changed

+749
-0
lines changed

src/SUMMARY.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,14 @@
455455
- [Serializer: implement Struct](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/struct.md)
456456
- [Serializer: implement Property](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/property.md)
457457
- [Serializer: Complete implementation](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/complete.md)
458+
- [Borrow checking invariants](idiomatic/leveraging-the-type-system/borrow-checker-invariants.md)
459+
- [Lifetimes and Borrows: the Abstract Rules](idiomatic/leveraging-the-type-system/borrow-checker-invariants/generalizing-ownership.md)
460+
- [Single-use values](idiomatic/leveraging-the-type-system/borrow-checker-invariants/single-use-values.md)
461+
- [Mutually Exclusive References / "Aliasing XOR Mutability"](idiomatic/leveraging-the-type-system/borrow-checker-invariants/aliasing-xor-mutability.md)
462+
- [PhantomData and Types](idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-01-types.md)
463+
- [PhantomData and Types (implementation)](idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-02-types-implemented.md)
464+
- [PhantomData: Lifetimes for External Resources](idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-03-lifetimes.md)
465+
- [PhantomData: OwnedFd & BorrowedFd](idiomatic/leveraging-the-type-system/borrow-checker-invariants/phantomdata-04-borrowedfd.md)
458466
- [Token Types](idiomatic/leveraging-the-type-system/token-types.md)
459467
- [Permission Tokens](idiomatic/leveraging-the-type-system/token-types/permission-tokens.md)
460468
- [Token Types with Data: Mutex Guards](idiomatic/leveraging-the-type-system/token-types/mutex-guard.md)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
minutes: 15
3+
---
4+
5+
# Using the Borrow checker to enforce Invariants
6+
7+
The borrow checker, while added to enforce memory ownership, can model other
8+
problems and prevent API misuse.
9+
10+
```rust,editable
11+
/// Doors can be open or closed, and you need the right key to lock or unlock
12+
/// one. Modelled with a Shared key and Owned door.
13+
pub struct DoorKey {
14+
pub key_shape: u32,
15+
}
16+
pub struct LockedDoor {
17+
lock_shape: u32,
18+
}
19+
pub struct OpenDoor {
20+
lock_shape: u32,
21+
}
22+
23+
fn open_door(key: &DoorKey, door: LockedDoor) -> Result<OpenDoor, LockedDoor> {
24+
if door.lock_shape == key.key_shape {
25+
Ok(OpenDoor { lock_shape: door.lock_shape })
26+
} else {
27+
Err(door)
28+
}
29+
}
30+
31+
fn close_door(key: &DoorKey, door: OpenDoor) -> Result<LockedDoor, OpenDoor> {
32+
if door.lock_shape == key.key_shape {
33+
Ok(LockedDoor { lock_shape: door.lock_shape })
34+
} else {
35+
Err(door)
36+
}
37+
}
38+
39+
fn main() {
40+
let key = DoorKey { key_shape: 7 };
41+
let closed_door = LockedDoor { lock_shape: 7 };
42+
let opened_door = open_door(&key, closed_door);
43+
if let Ok(opened_door) = opened_door {
44+
println!("Opened the door with key shape '{}'", key.key_shape);
45+
} else {
46+
eprintln!(
47+
"Door wasn't opened! Your key only opens locks with shape '{}'",
48+
key.key_shape
49+
);
50+
}
51+
}
52+
```
53+
54+
<details>
55+
56+
- We've seen the borrow checker prevent memory safety bugs (use-after-free, data
57+
races).
58+
59+
- We've also used types to shape and restrict APIs already using
60+
[the Typestate pattern](../leveraging-the-type-system/typestate-pattern.md).
61+
62+
- Language features are often introduced for a specific purpose.
63+
64+
Over time, users may develop ways of using a feature in ways that were not
65+
predicted when they were introduced.
66+
67+
Java 5 introduced Generics in 2004 with the
68+
[main stated purpose of enabling type-safe collections](https://jcp.org/en/jsr/detail?id=14).
69+
70+
Adoption was slow at first, but some new projects began designing their APIs
71+
around generics from the beginning.
72+
73+
Since then, users and developers of the language expanded the use of generics
74+
to other areas of type-safe API design:
75+
- Class information can be held onto via Java's `Class<T>` or Guava's
76+
`TypeToken<T>`.
77+
- The Builder pattern can be implemented using Recursive Generics.
78+
79+
We aim to do something similar here: Even though the borrow checker was
80+
introduced to prevent use-after-free and data races, we treat it as just
81+
another API design tool.
82+
83+
It can be used to model program properties that have nothing to do with
84+
preventing memory safety bugs.
85+
86+
- To use the borrow checker as a problem solving tool, we will need to "forget"
87+
that the original purpose of it is to prevent mutable aliasing in the context
88+
of preventing use-after-frees and data races.
89+
90+
We should imagine working within situations where the rules are the same but
91+
the meaning is slightly different.
92+
93+
- This example uses ownership and borrowing are used to model the state of a
94+
physical door.
95+
96+
`open_door` **consumes** a `LockedDoor` and returns a new `OpenDoor`. The old
97+
`LockedDoor` value is no longer available.
98+
99+
If the wrong key is used, the door is left locked. It is returned as an `Err`
100+
case of the `Result`.
101+
102+
It is a compile-time error to try and use a door that has already been opened.
103+
104+
- Similarly, `lock_door` consumes an `OpenDoor`, preventing closing the door
105+
twice at compile time.
106+
107+
- The rules of the borrow checker exist to prevent memory safety bugs, but the
108+
underlying logical system does not "know" what memory is.
109+
110+
All the borrow checker does is enforce a specific set of rules of how users
111+
can order operations.
112+
113+
This is just one case of piggy-backing onto the rules of the borrow checker to
114+
design APIs to be harder or impossible to misuse.
115+
116+
</details>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
minutes: 15
3+
---
4+
5+
# Mutually Exclusive References / "Aliasing XOR Mutability"
6+
7+
We can use the mutual exclusion of `&T` and `&mut T` references to prevent data
8+
from being used before it is ready.
9+
10+
```rust,editable
11+
pub struct QueryResult;
12+
pub struct DatabaseConnection {/* fields omitted */}
13+
14+
impl DatabaseConnection {
15+
pub fn new() -> Self {
16+
Self {}
17+
}
18+
pub fn results(&self) -> &[QueryResult] {
19+
&[] // fake results
20+
}
21+
}
22+
23+
pub struct Transaction<'a> {
24+
connection: &'a mut DatabaseConnection,
25+
}
26+
27+
impl<'a> Transaction<'a> {
28+
pub fn new(connection: &'a mut DatabaseConnection) -> Self {
29+
Self { connection }
30+
}
31+
pub fn query(&mut self, _query: &str) {
32+
// Send the query over, but don't wait for results.
33+
}
34+
pub fn commit(self) {
35+
// Finish executing the transaction and retrieve the results.
36+
}
37+
}
38+
39+
fn main() {
40+
let mut db = DatabaseConnection::new();
41+
42+
// The transaction `tx` mutably borrows `db`.
43+
let mut tx = Transaction::new(&mut db);
44+
tx.query("SELECT * FROM users");
45+
46+
// This won't compile because `db` is already mutably borrowed by `tx`.
47+
// let results = db.results(); // ❌🔨
48+
49+
// The borrow of `db` ends when `tx` is consumed by `commit()`.
50+
tx.commit();
51+
52+
// Now it is possible to borrow `db` again.
53+
let results = db.results();
54+
}
55+
```
56+
57+
<details>
58+
59+
- Motivation: In this database API queries are kicked off for asynchronous
60+
execution and the results are only available once the whole transaction is
61+
finished.
62+
63+
A user might think that queries are executed immediately, and try to read
64+
results before they are made available. This API misuse could make the app
65+
read incomplete or incorrect data.
66+
67+
While an obvious misunderstanding, situations such as this can happen in
68+
practice.
69+
70+
Ask: Has anyone misunderstood an API by not reading the docs for proper use?
71+
72+
Expect: Examples of early-career or in-university mistakes and
73+
misunderstandings.
74+
75+
As an API grows in size and user base, a smaller percentage of users has deep
76+
knowledge of the system the API represents.
77+
78+
- This example shows how we can use Aliasing XOR Mutability to prevent this kind
79+
of misuse.
80+
81+
- The code might read results before they are ready if the programmer assumes
82+
that the queries execute immediately rather than kicked off for asynchronous
83+
execution.
84+
85+
- The constructor for the `Transaction` type takes a mutable reference to the
86+
database connection, and stores it in the returned `Transaction` value.
87+
88+
The explicit lifetime here doesn't have to be intimidating, it just means
89+
"`Transaction` is outlived by the `DatabaseConnection` that was passed to it"
90+
in this case.
91+
92+
The reference is mutable to completely lock out the `DatabaseConnection` from
93+
other usage, such as starting further transactions or reading the results.
94+
95+
- While a `Transaction` exists, we can't touch the `DatabaseConnection` variable
96+
that was created from it.
97+
98+
Demonstrate: uncomment the `db.results()` line. Doing so will result in a
99+
compile error, as `db` is already mutably borrowed.
100+
101+
- Note: The query results not being public and placed behind a getter function
102+
lets us enforce the invariant "users can only look at query results if there
103+
is no active transactions."
104+
105+
If the query results were placed in a public struct field, this invariant
106+
could be violated.
107+
108+
</details>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
minutes: 10
3+
---
4+
5+
# Lifetimes and Borrows: the Abstract Rules
6+
7+
```rust,editable
8+
// An internal data type to have something to hold onto.
9+
pub struct Internal;
10+
// The "outer" data.
11+
pub struct Data(Internal);
12+
13+
fn shared_use(value: &Data) -> &Internal {
14+
&value.0
15+
}
16+
fn exclusive_use(value: &mut Data) -> &mut Internal {
17+
&mut value.0
18+
}
19+
fn deny_future_use(value: Data) {}
20+
21+
fn demo_exclusive() {
22+
let mut value = Data(Internal);
23+
let shared = shared_use(&value);
24+
// let exclusive = exclusive_use(&mut value); // ❌🔨
25+
let shared_again = &shared;
26+
}
27+
28+
fn demo_denied() {
29+
let value = Data(Internal);
30+
deny_future_use(value);
31+
// let shared = shared_use(&value); // ❌🔨
32+
}
33+
34+
# fn main() {}
35+
```
36+
37+
<details>
38+
39+
- This example re-frames the borrow checker rules away from references and
40+
towards semantic meaning in non-memory-safety settings.
41+
42+
Nothing is being mutated, nothing is being sent across threads.
43+
44+
- In rust's borrow checker we have access to three different ways of "taking" a
45+
value:
46+
47+
- Owned value `T`. Value is dropped when the scope ends, unless it is not
48+
returned to another scope.
49+
50+
- Shared Reference `&T`. Allows aliasing but prevents mutable access while
51+
shared references are in use.
52+
53+
- Mutable Reference `&mut T`. Only one of these is allowed to exist for a
54+
value at any one point, but can be used to create shared references.
55+
56+
- Ask: The two commented-out lines in the `demo` functions would cause
57+
compilation errors, Why?
58+
59+
`demo_exclusive`: Because the `shared` value is still aliased after the
60+
`exclusive` reference is taken.
61+
62+
`demo_denied`: Because `value` is consumed the line before the
63+
`shared_again_again` reference is taken from `&value`.
64+
65+
- Remember that every `&T` and `&mut T` has a lifetime, just one the user
66+
doesn't have to annotate or think about most of the time.
67+
68+
We rarely specify lifetimes because the Rust compiler allows us to _elide_
69+
them in most cases. See:
70+
[Lifetime Elision](../../../lifetimes/lifetime-elision.md)
71+
72+
</details>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
minutes: 5
3+
---
4+
5+
# PhantomData 1/4: De-duplicating Same Data & Semantics
6+
7+
The newtype pattern can sometimes come up against the DRY principle, how do we
8+
solve this?
9+
10+
<!-- dprint-ignore-start -->
11+
```rust,editable,compile_fail
12+
pub struct UserId(u64);
13+
impl ChatUser for UserId { /* ... */ }
14+
15+
pub struct PatronId(u64);
16+
impl ChatUser for PatronId { /* ... */ }
17+
18+
pub struct ModeratorId(u64);
19+
impl ChatUser for ModeratorId { /* ... */ }
20+
impl ChatModerator for ModeratorId { /* ... */ }
21+
22+
pub struct AdminId(u64);
23+
impl ChatUser for AdminId { /* ... */ }
24+
impl ChatModerator for AdminId { /* ... */ }
25+
impl ChatAdmin for AdminId { /* ... */ }
26+
27+
// And so on ...
28+
fn main() {}
29+
```
30+
<!-- dprint-ignore-end -->
31+
32+
<details>
33+
34+
- Problem: We want to use the newtype pattern to differentiate permissions, but
35+
we're having to implement the same traits over and over again for the same
36+
data.
37+
38+
- Ask: Assume the details of each implementation here are the same between
39+
types, what are ways we can avoid repeating ourselves?
40+
41+
Expect:
42+
- Make this an enum, not distinct data types.
43+
- Bundle the user ID with permission tokens like
44+
`struct Admin(u64, UserPermission, ModeratorPermission, AdminPermission);`
45+
- Adding a type parameter which encodes permissions.
46+
- Mentioning `PhantomData` ahead of schedule (it's in the title).
47+
48+
</details>

0 commit comments

Comments
 (0)