|
2 | 2 | minutes: 15 |
3 | 3 | --- |
4 | 4 |
|
5 | | -# Mutually Exclusive References, or "Aliasing XOR Mutability" |
| 5 | +# Mutually Exclusive References / "Aliasing XOR Mutability" |
6 | 6 |
|
7 | 7 | We can use the mutual exclusion of `&T` and `&mut T` references for a single |
8 | 8 | value to model some constraints. |
9 | 9 |
|
10 | | -```rust,editable,compile_fail |
11 | | -pub struct Transaction(/* specifics omitted */); |
12 | | -pub struct QueryResult(String); |
13 | | -
|
14 | | -pub struct DatabaseConnection { |
15 | | - transaction: Transaction, |
16 | | - query_results: Vec<QueryResult>, |
17 | | -} |
| 10 | +```rust,editable |
| 11 | +pub struct QueryResult; |
| 12 | +pub struct DatabaseConnection {/* fields omitted */} |
18 | 13 |
|
19 | 14 | impl DatabaseConnection { |
20 | 15 | pub fn new() -> Self { |
21 | | - Self { |
22 | | - transaction: Transaction(/* again, specifics omitted */), |
23 | | - query_results: vec![], |
24 | | - } |
25 | | - } |
26 | | - pub fn get_transaction(&mut self) -> &mut Transaction { |
27 | | - &mut self.transaction |
| 16 | + Self {} |
28 | 17 | } |
29 | 18 | pub fn results(&self) -> &[QueryResult] { |
30 | | - &self.query_results |
31 | | - } |
32 | | - pub fn commit(&mut self) { |
33 | | - // Work omitted, including sending/clearing the transaction |
34 | | - println!("Transaction committed!") |
| 19 | + &[] // fake results |
35 | 20 | } |
36 | 21 | } |
37 | 22 |
|
38 | | -pub fn do_something_with_transaction(transaction: &mut Transaction) {} |
| 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 | +} |
39 | 38 |
|
40 | 39 | fn main() { |
41 | 40 | let mut db = DatabaseConnection::new(); |
42 | | - let mut transaction = db.get_transaction(); |
43 | | - do_something_with_transaction(transaction); |
44 | | - let assumed_the_transactions_happened_immediately = db.results(); // ❌🔨 |
45 | | - do_something_with_transaction(transaction); |
46 | | - // Works, as the lifetime of "transaction" as a reference ended above. |
47 | | - let assumed_the_transactions_happened_immediately_again = db.results(); |
48 | | - db.commit(); |
| 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. |
| 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(); |
49 | 54 | } |
50 | 55 | ``` |
51 | 56 |
|
52 | 57 | <details> |
53 | 58 |
|
54 | | -- Aliasing XOR Mutability means "we can have multiple immutable references, a |
55 | | - single mutable reference, but not both." |
| 59 | +- Motivation: When working with a database API, a user might imagine that |
| 60 | + transactions are being committed "as they go" and try to read results in |
| 61 | + between queries being added to the transaction. This fundamental misuse of the |
| 62 | + API could lead to confusion as to why nothing is happening. |
| 63 | + |
| 64 | + While an obvious misunderstanding, situations such as this can happen in |
| 65 | + practice. |
56 | 66 |
|
57 | | -- This example shows how we can use the mutual exclusion of these kinds of |
58 | | - references to dissuade a user from reading query results while using a |
59 | | - transaction API. |
| 67 | + Ask: Has anyone misunderstood an API by not reading the docs for proper use? |
| 68 | + |
| 69 | + Expect: Examples of early-career or in-university mistakes and |
| 70 | + misunderstandings. |
| 71 | + |
| 72 | + As an API grows in size and user base, a smaller percentage may have "total" |
| 73 | + knowledge of the system the API represents. |
| 74 | + |
| 75 | +- This example shows how we can use Aliasing XOR Mutability prevent this kind of |
| 76 | + misuse |
60 | 77 |
|
61 | 78 | This might happen if the user is working under the false assumption that the |
62 | 79 | queries being written to the transaction happen "immediately" rather than |
63 | 80 | being queued up and performed together. |
64 | 81 |
|
65 | | -- By borrowing one field of a struct via a method that returns a mutable / |
66 | | - exclusive reference we prevent access to the other fields of that struct under |
67 | | - a shared / non-exclusive reference until the lifetime of that borrow ends. |
68 | | - |
69 | | -- The `transaction` field must be borrowed via a method, as the compiler can |
70 | | - reason about borrowing different fields in mutable/shared ways simultaneously |
71 | | - if that borrowing is done manually. |
| 82 | +- The constructor for the Transaction type takes a mutable reference to the |
| 83 | + database connection, which it holds onto that reference. |
72 | 84 |
|
73 | | - Demonstrate: |
| 85 | + The explicit lifetime here doesn't have to be intimidating, it just means |
| 86 | + "`Transaction` is outlived by the `DatabaseConnection` that was passed to it" |
| 87 | + in this case. |
74 | 88 |
|
75 | | - - Change the instances of `db.get_transaction()` and `db.results()` to manual |
76 | | - borrows (`&mut db.transaction` and `&db.query_results` respectively) to show |
77 | | - the difference in what the borrow checker allows. |
| 89 | + The `mut` keyword in the type lets us determine that there is just one of |
| 90 | + these references present per variable of type `DatabaseConnection`. |
78 | 91 |
|
79 | | - - Put the non-`main` part of this example in a module to reiterate that this |
80 | | - manual access is not possible across module boundaries. |
| 92 | +- While a `Transaction` exists, we can't touch the `DatabaseConnection` variable |
| 93 | + that was created from it. |
81 | 94 |
|
82 | | -- As laid out in [generalizing ownership](generalizing-ownership.md) we can look |
83 | | - at the ways Mutable References and Shareable References interact to see if |
84 | | - they fit with the invariants we want to uphold for an API. |
| 95 | + Demonstrate: uncomment the `db.results()` line. |
85 | 96 |
|
86 | | -- In this case, having the query results not public and placed behind a getter |
87 | | - function, we can enforce the invariant "users of this API are not looking at |
88 | | - the query results at the same time as they are writing to a transaction." |
| 97 | +- This lifetime parameter for `Transaction` needs to come from somewhere, in |
| 98 | + this case it is derived from the lifetime of the owned `DatabaseConnection` |
| 99 | + from which an exclusive reference is being passed. |
89 | 100 |
|
90 | | -- The "don't look at query results while building a transaction" invariant can |
91 | | - still be circumvented, how so? |
| 101 | +- As laid out in [generalizing ownership](generalizing-ownership.md) and |
| 102 | + [the opening slide for this section](../borrow-checker-invariants.md) we can |
| 103 | + look at the ways Mutable References and Shareable References interact to see |
| 104 | + if they fit with the invariants we want to uphold for an API. |
92 | 105 |
|
93 | | - - The user could access the transaction solely through `db.get_transaction()`, |
94 | | - leaving the lifetime too temporary to prevent access to `db.results()`. |
| 106 | +- Note: The query results not being public and placed behind a getter function |
| 107 | + lets us enforce the invariant "users can only look at query results if they |
| 108 | + are not also writing to a transaction." |
95 | 109 |
|
96 | | - - How could we avoid this by working in other concepts from "Leveraging the |
97 | | - Type System"? |
| 110 | + If they're publicly available to the user outside of the definition module |
| 111 | + then this invariant can be invalidated. |
98 | 112 |
|
99 | 113 | </details> |
0 commit comments