1
- # Compose structs together for better borrowing
2
-
3
- TODO - this is not a very snappy name
1
+ # Struct decomposition for independent borrowing
4
2
5
3
## Description
6
4
@@ -20,71 +18,93 @@ Here is a contrived example of where the borrow checker foils us in our plan to
20
18
use a struct:
21
19
22
20
``` rust
23
- struct A {
24
- f1 : u32 ,
25
- f2 : u32 ,
26
- f3 : u32 ,
21
+ struct Database {
22
+ connection_string : String ,
23
+ timeout : u32 ,
24
+ pool_size : u32 ,
27
25
}
28
26
29
- fn foo (a : & mut A ) -> & u32 { & a . f2 }
30
- fn bar (a : & mut A ) -> u32 { a . f1 + a . f3 }
27
+ fn print_database (database : & Database ) {
28
+ println! (" Connection string: {}" , database . connection_string);
29
+ println! (" Timeout: {}" , database . timeout);
30
+ println! (" Pool size: {}" , database . pool_size);
31
+ }
31
32
32
- fn baz (a : & mut A ) {
33
- // The later usage of x causes a to be borrowed for the rest of the function.
34
- let x = foo (a );
35
- // Borrow checker error:
36
- // let y = bar(a); // ~ ERROR: cannot borrow `*a` as mutable more than once
37
- // at a time
38
- println! (" {}" , x );
33
+ fn main () {
34
+ let mut db = Database {
35
+ connection_string : " initial string" . to_string (),
36
+ timeout : 30 ,
37
+ pool_size : 100 ,
38
+ };
39
+
40
+ let connection_string = & mut db . connection_string;
41
+ print_database (& db ); // Immutable borrow of `db` happens here
42
+ // *connection_string = "new string".to_string(); // Mutable borrow is used
43
+ // here
39
44
}
40
45
```
41
46
42
- We can apply this design pattern and refactor ` A ` into two smaller structs, thus
43
- solving the borrow checking issue:
47
+ We can apply this design pattern and refactor ` Database ` into three smaller
48
+ structs, thus solving the borrow checking issue:
44
49
45
50
``` rust
46
- // A is now composed of two structs - B and C.
47
- struct A {
48
- b : B ,
49
- c : C ,
50
- }
51
- struct B {
52
- f2 : u32 ,
53
- }
54
- struct C {
55
- f1 : u32 ,
56
- f3 : u32 ,
51
+ // Database is now composed of three structs - ConnectionString, Timeout and PoolSize.
52
+ // Let's decompose it into smaller structs
53
+ #[derive(Debug , Clone )]
54
+ struct ConnectionString (String );
55
+
56
+ #[derive(Debug , Clone , Copy )]
57
+ struct Timeout (u32 );
58
+
59
+ #[derive(Debug , Clone , Copy )]
60
+ struct PoolSize (u32 );
61
+
62
+ // We then compose these smaller structs back into `Database`
63
+ struct Database {
64
+ connection_string : ConnectionString ,
65
+ timeout : Timeout ,
66
+ pool_size : PoolSize ,
57
67
}
58
68
59
- // These functions take a B or C, rather than A.
60
- fn foo (b : & mut B ) -> & u32 { & b . f2 }
61
- fn bar (c : & mut C ) -> u32 { c . f1 + c . f3 }
69
+ // print_database can then take ConnectionString, Timeout and Poolsize struct instead
70
+ fn print_database (connection_str : ConnectionString ,
71
+ timeout : Timeout ,
72
+ pool_size : PoolSize ) {
73
+ println! (" Connection string: {:?}" , connection_str );
74
+ println! (" Timeout: {:?}" , timeout );
75
+ println! (" Pool size: {:?}" , pool_size );
76
+ }
62
77
63
- fn baz (a : & mut A ) {
64
- let x = foo (& mut a . b);
65
- // Now it's OK!
66
- let y = bar (& mut a . c);
67
- println! (" {}" , x );
78
+ fn main () {
79
+ // Initialize the Database with the three structs
80
+ let mut db = Database {
81
+ connection_string : ConnectionString (" localhost" . to_string ()),
82
+ timeout : Timeout (30 ),
83
+ pool_size : PoolSize (100 ),
84
+ };
85
+
86
+ let connection_string = & mut db . connection_string;
87
+ print_database (connection_string . clone (), db . timeout, db . pool_size);
88
+ * connection_string = ConnectionString (" new string" . to_string ());
68
89
}
69
90
```
70
91
71
92
## Motivation
72
93
73
- TODO Why and where you should use the pattern
94
+ This pattern is most useful, when you have a struct that ended up with a lot of
95
+ fields that you want to borrow independently. Thus having a more flexible
96
+ behaviour in the end.
74
97
75
98
## Advantages
76
99
77
- Lets you work around limitations in the borrow checker.
78
-
79
- Often produces a better design.
100
+ Decomposition of structs lets you work around limitations in the borrow checker.
101
+ And it often produces a better design.
80
102
81
103
## Disadvantages
82
104
83
- Leads to more verbose code.
84
-
85
- Sometimes, the smaller structs are not good abstractions, and so we end up with
86
- a worse design. That is probably a 'code smell', indicating that the program
87
- should be refactored in some way.
105
+ It can lead to more verbose code. And sometimes, the smaller structs are not
106
+ good abstractions, and so we end up with a worse design. That is probably a
107
+ 'code smell', indicating that the program should be refactored in some way.
88
108
89
109
## Discussion
90
110
0 commit comments