-
Couldn't load subscription status.
- Fork 1.9k
"borrow checker invariants" section of the "leveraging the type system" chapter #2867
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b771e7f
3831858
a81b55a
0b4318f
728af30
a37a272
88852f2
0aa4f21
37e69fe
595ca8e
2a88748
31284d6
e7f874b
808de4f
85b70f0
44dff2a
ebf00a2
7b72587
9f49ba5
6fcc471
2d3f915
26cfe2b
d55ce18
fc3f3de
a1b3a53
d8fb34a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| --- | ||
| minutes: 15 | ||
| --- | ||
|
|
||
| # Using the Borrow checker to enforce Invariants | ||
|
|
||
| The borrow checker, while added to enforce memory ownership, can be leveraged | ||
| model other problems and prevent API misuse. | ||
|
|
||
| ```rust,editable | ||
| /// Doors can be open or closed, and you need the right key to lock or unlock | ||
| /// one. Modelled with a Shared key and Owned door. | ||
| pub struct DoorKey { | ||
| pub key_shape: u32, | ||
| } | ||
| pub struct LockedDoor { | ||
| lock_shape: u32, | ||
| } | ||
| pub struct OpenDoor { | ||
| lock_shape: u32, | ||
| } | ||
|
|
||
| fn open_door(key: &DoorKey, door: LockedDoor) -> Result<OpenDoor, LockedDoor> { | ||
| if door.lock_shape == key.key_shape { | ||
| Ok(OpenDoor { lock_shape: door.lock_shape }) | ||
| } else { | ||
| Err(door) | ||
| } | ||
| } | ||
|
|
||
| fn close_door(key: &DoorKey, door: OpenDoor) -> Result<LockedDoor, OpenDoor> { | ||
| if door.lock_shape == key.key_shape { | ||
| Ok(LockedDoor { lock_shape: door.lock_shape }) | ||
| } else { | ||
| Err(door) | ||
| } | ||
| } | ||
|
|
||
| fn main() { | ||
| let key = DoorKey { key_shape: 7 }; | ||
| let closed_door = LockedDoor { lock_shape: 7 }; | ||
| let opened_door = open_door(&key, closed_door); | ||
| if let Ok(opened_door) = opened_door { | ||
| println!("Opened the door with key shape '{}'", key.key_shape); | ||
| } else { | ||
| eprintln!( | ||
| "Door wasn't opened! Your key only opens locks with shape '{}'", | ||
| key.key_shape | ||
| ); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| - The borrow checker has been used to prevent use-after-free and multiple | ||
| mutable references up until this point, and we've used types to shape and | ||
| restrict use of APIs already using | ||
| [the Typestate pattern](../leveraging-the-type-system/typestate-pattern.md). | ||
|
|
||
| - Language features are often introduced for a specific purpose. | ||
|
|
||
| Over time, users may develop ways of using a feature in ways that were not | ||
| predicted when they were introduced. | ||
|
|
||
| In 2004, Java 5 introduced Generics with the | ||
| [main stated purpose of enabling type-safe collections](https://jcp.org/en/jsr/detail?id=14). | ||
|
|
||
| Since then, users and developers of the language expanded the use of generics | ||
| to other areas of type-safe API design. | ||
| <!-- TODO: Reference how this was adopted --> | ||
|
|
||
| What we aim to do here is similar: Even though the borrow checker was | ||
| introduced to prevent use-after-free and data races, it is just another API | ||
| design tool. It can be used to model program properties that have nothing to | ||
| do with preventing memory safety bugs. | ||
|
|
||
| - To use the borrow checker as a problem solving tool, we will need to "forget" | ||
| that the original purpose of it is to prevent mutable aliasing in the context | ||
| of preventing use-after-frees and data races. | ||
|
|
||
| We should imagine working within situations where the rules are the same but | ||
| the meaning is slightly different. | ||
|
|
||
| - This example uses ownership and borrowing are used to model the state of a | ||
| physical door. | ||
|
|
||
| `open_door` **consumes** a `LockedDoor` and returns a new `OpenDoor`. The old | ||
| `LockedDoor` value is no longer available. | ||
|
|
||
| If the wrong key is used, the door is left locked. It is returned as an `Err` | ||
| case of the `Result`. | ||
|
|
||
| It is a compile-time error to try and use a door that has already been opened. | ||
|
|
||
| - Similarly, `lock_door` consumes an `OpenDoor`, preventing closing the door | ||
| twice at compile time. | ||
|
|
||
| - The rules of the borrow checker exist to prevent memory safety bugs, but the | ||
| underlying logical system does not "know" what memory is. | ||
|
|
||
| All the borrow checker does is enforce a specific set of rules of how users | ||
| can order operations. | ||
|
|
||
| This is just one case of piggy-backing onto the rules of the borrow checker to | ||
| design APIs to be harder or impossible to misuse. | ||
|
|
||
| </details> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| --- | ||
| minutes: 15 | ||
| --- | ||
|
|
||
| # Mutually Exclusive References / "Aliasing XOR Mutability" | ||
|
|
||
| We can use the mutual exclusion of `&T` and `&mut T` references for a single | ||
| value to model some constraints. | ||
|
|
||
| ```rust,editable | ||
| pub struct QueryResult; | ||
| pub struct DatabaseConnection {/* fields omitted */} | ||
|
|
||
| impl DatabaseConnection { | ||
| pub fn new() -> Self { | ||
| Self {} | ||
| } | ||
| pub fn results(&self) -> &[QueryResult] { | ||
| &[] // fake results | ||
| } | ||
| } | ||
|
|
||
| pub struct Transaction<'a> { | ||
| connection: &'a mut DatabaseConnection, | ||
| } | ||
|
|
||
| impl<'a> Transaction<'a> { | ||
| pub fn new(connection: &'a mut DatabaseConnection) -> Self { | ||
| Self { connection } | ||
| } | ||
| pub fn query(&mut self, _query: &str) { | ||
| // Send the query over, but don't wait for results. | ||
| } | ||
| pub fn commit(self) { | ||
| // Finish executing the transaction and retrieve the results. | ||
| } | ||
| } | ||
|
|
||
| fn main() { | ||
| let mut db = DatabaseConnection::new(); | ||
|
|
||
| // The transaction `tx` mutably borrows `db`. | ||
| let mut tx = Transaction::new(&mut db); | ||
| tx.query("SELECT * FROM users"); | ||
|
|
||
| // This won't compile because `db` is already mutably borrowed. | ||
| // let results = db.results(); // ❌🔨 | ||
|
|
||
| // The borrow of `db` ends when `tx` is consumed by `commit`. | ||
| tx.commit(); | ||
|
|
||
| // Now it is possible to borrow `db` again. | ||
| let results = db.results(); | ||
| } | ||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| - Motivation: When working with a database API, a user might imagine that | ||
| transactions are being committed "as they go" and try to read results in | ||
| between queries being added to the transaction. This fundamental misuse of the | ||
| API could lead to confusion as to why nothing is happening. | ||
|
|
||
| While an obvious misunderstanding, situations such as this can happen in | ||
| practice. | ||
|
|
||
| Ask: Has anyone misunderstood an API by not reading the docs for proper use? | ||
|
|
||
| Expect: Examples of early-career or in-university mistakes and | ||
| misunderstandings. | ||
|
|
||
| As an API grows in size and user base, a smaller percentage may have "total" | ||
| knowledge of the system the API represents. | ||
|
|
||
| - This example shows how we can use Aliasing XOR Mutability prevent this kind of | ||
| misuse | ||
|
|
||
| This might happen if the user is working under the false assumption that the | ||
| queries being written to the transaction happen "immediately" rather than | ||
| being queued up and performed together. | ||
|
|
||
| - The constructor for the Transaction type takes a mutable reference to the | ||
| database connection, which it holds onto that reference. | ||
|
|
||
| The explicit lifetime here doesn't have to be intimidating, it just means | ||
| "`Transaction` is outlived by the `DatabaseConnection` that was passed to it" | ||
| in this case. | ||
|
|
||
| The `mut` keyword in the type lets us determine that there is just one of | ||
| these references present per variable of type `DatabaseConnection`. | ||
|
|
||
| - While a `Transaction` exists, we can't touch the `DatabaseConnection` variable | ||
| that was created from it. | ||
|
|
||
| Demonstrate: uncomment the `db.results()` line. | ||
|
|
||
| - This lifetime parameter for `Transaction` needs to come from somewhere, in | ||
| this case it is derived from the lifetime of the owned `DatabaseConnection` | ||
| from which an exclusive reference is being passed. | ||
|
|
||
| - As laid out in [generalizing ownership](generalizing-ownership.md) and | ||
| [the opening slide for this section](../borrow-checker-invariants.md) we can | ||
| look at the ways Mutable References and Shareable References interact to see | ||
| if they fit with the invariants we want to uphold for an API. | ||
|
|
||
| - Note: The query results not being public and placed behind a getter function | ||
| lets us enforce the invariant "users can only look at query results if they | ||
| are not also writing to a transaction." | ||
|
|
||
| If they're publicly available to the user outside of the definition module | ||
| then this invariant can be invalidated. | ||
|
|
||
| </details> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| --- | ||
| minutes: 5 | ||
| --- | ||
tall-vase marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| # Generalizing Ownership: Lifetimes Refresher | ||
|
|
||
| The logic of the borrow checker, while modelled off "memory ownership", can be | ||
| abstracted away from that use case to model other problems where we want to | ||
| prevent API misuse. | ||
|
|
||
| ```rust,editable | ||
| // An internal data type to have something to hold onto. | ||
| pub struct Internal; | ||
| // The "outer" data. | ||
| pub struct Data(Internal); | ||
|
|
||
| fn shared_use(value: &Data) -> &Internal { | ||
| &value.0 | ||
| } | ||
| fn exclusive_use(value: &mut Data) -> &mut Internal { | ||
| &mut value.0 | ||
| } | ||
| fn deny_future_use(value: Data) {} | ||
|
|
||
| fn main() { | ||
| let mut value = Data(Internal); | ||
| let shared = shared_use(&value); | ||
| // let exclusive = exclusive_use(&mut value); // ❌🔨 | ||
| let shared_again = &shared; | ||
| deny_future_use(value); | ||
| // let shared_again_again = shared_use(&value); // ❌🔨 | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code is a good demonstration of the borrow checker rules, but it doesn't connect to a real-world problem. Is it possible to find a bit more engaging example to illustrate the point? For example, make a simplified implementation of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking about it more, a large chunk of this slide can be moved to the introductory slide for this chapter and the rest can be re-purposed as a brief borrowck refresher. |
||
| ``` | ||
|
|
||
| <details> | ||
|
|
||
| - This example re-frames the borrow checker rules away from references and | ||
| towards semantic meaning in non-memory-safety settings. Nothing is being | ||
| mutated, nothing is being sent across threads. | ||
|
|
||
| - In rust's borrow checker we have access to three different ways of "taking" a | ||
| value: | ||
|
|
||
| - Owned value `T`. Value is dropped when the scope ends, unless it is not | ||
| returned to another scope. | ||
|
|
||
| - Shared Reference `&T`. Allows aliasing but prevents mutable access while | ||
| shared references are in use. | ||
|
|
||
| - Mutable Reference `&mut T`. Only one of these is allowed to exist for a | ||
| value at any one point, but can be used to create shared references. | ||
|
|
||
| - Ask: The two commented-out lines in `main` would cause compilation errors, | ||
| Why? | ||
|
|
||
| 1: Because the `shared` value is still aliased after the `exclusive` reference | ||
| is taken. | ||
|
|
||
| 2: Because `value` is consumed (AKA dropped) the line before the | ||
| `shared_again_again` reference is taken from `&value`. | ||
|
|
||
| - Remember that every `&T` and `&mut T` has a lifetime, just one the user | ||
| doesn't have to annotate or think about most of the time. We get to avoid | ||
| annotating a lot of lifetimes because the rust compiler allows a user to elide | ||
| the majority of them. See: | ||
| [Lifetime Elision](../../../lifetimes/lifetime-elision.md) | ||
|
|
||
| </details> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This slide might be a place where an analogy might help some listeners understand what we are trying to get at when we say that the borrow checker and lifetimes are just another API design tool. Here's one possible analogy. WDYT? Generics in Java were added primarily to support type-safe collections. In fact, Java 5 added generic type arguments to existing standard library collection types that were previously non-generic! So the language designers had a clear primary use case in mind. However, generics turned out to be useful in many other API designs. So it would be too narrow-minded to present Java generics as "a language feature for type-safe collections." Similarly, the lifetimes and the borrow checker were introduced in Rust for compile-time memory safety guarantees, but their applicability in API design is broader. We (the Rust community) are still discovering design patterns and trying to understand what these tools can do for API design beyond memory safety. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like a good thing to bring up, I'll pull together some references to drop in. If you've got suggestions on pieces covering this I'd be happy to hear about them, but I understand linkrot and the ephemeral nature of back-channel discussion of the time may have gotten to most of it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. JSR 14 which introduced generics lists only one goal specific to a use case, and it is "Good collections support. The core Collections APIs and similar APIs are perhaps the most important customers of genericity, so it is essential that they work well as, and with, generic classes." Furthermore, this is the #1 goal of the proposal overall. An empirical research article that I could find, Java generics adoption: how new features are introduced, championed, or ignored studies how generics were adopted in practice. It includes a data-driven argument that the most common parameterized types are collections (the only non-collection-related type in Table 1 is One source that I had high hopes for, ACM's History of programming languages journal, unfortunately does have a piece on Java. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wonderful, thank you! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This still needs some more elaboration from me, I'll be reading more on this to resolve the standing TODO. |
||
Uh oh!
There was an error while loading. Please reload this page.