Composing structs

pull/2/head
Nick Cameron 9 years ago
parent fd47bf3ba8
commit e340602f3f

@ -42,6 +42,7 @@ language.
* TODO composition of structs to please the borrow checker
* TODO `Error` traits and `Result` forwarding
* TODO graphs
* [Compose structs together for better borrowing](patterns/compose-structs.md)

@ -0,0 +1,101 @@
# Compose structs together for better borrowing
TODO - this is not a very snappy name
## Description
Sometimes a large struct will cause issues with the borrow checker - although
fields can be borrowed independently, sometimes the whole struct ends up being
used at once, preventing other uses. A solution might be to decompose the struct
into several smaller structs. Then compose these together into the original
struct. Then each struct can be borrowed separately and have more flexible
behaviour.
This will often lead to a better design in other ways: applying this design
pattern often reveals smaller units of functionality.
## Example
Here is a contrived example of where the borrow checker foils us in our plan to
use a struct:
```rust
struct A {
f1: u32,
f2: u32,
f3: u32,
}
fn foo(a: &mut A) -> &u32 { &a.f2 }
fn bar(a: &mut A) -> u32 { a.f1 + a.f3 }
fn main(a: &mut A) {
// x causes a to be borrowed for the rest of the function.
let x = foo(a);
// Borrow check error
let y = bar(a); //~ ERROR: cannot borrow `*a` as mutable more than once at a time
}
```
We can apply this design pattern and refactor `A` into two smaller structs, thus
solving the borrow checking issue:
```
// A is now composed of two structs - B and C.
struct A {
b: B,
c: C,
}
struct B {
f2: u32,
}
struct C {
f1: u32,
f3: u32,
}
// These functions take a B or C, rather than A.
fn foo(b: &mut B) -> &u32 { &b.f2 }
fn bar(c: &mut C) -> u32 { c.f1 + c.f3 }
fn main(a: &mut A) {
let x = foo(&mut a.b);
// Now it's OK!
let y = bar(&mut a.c);
}
```
## Motivation
Why and where you should use the pattern
## Advantages
Lets you work around limitations in the borrow checker.
Often produces a better design.
## Disadvantages
Leads to more verbose code.
Sometimes, the smaller structs are not good abstractions, and so we end up with
a worse design. That is probably a 'code smell', indicating that the program
should be refactored in some way.
## Discussion
This pattern is not required in languages that don't have a borrow checker, so
in that sense is unique to Rust. However, making smaller units of functionality
often leads to cleaner code: a widely acknowledged principle of software
engineering, independent of the language.
This pattern relies on Rust's borrow checker to be able to borrow fields
independently of each other. In the example, the borrow checker knows that `a.b`
and `a.c` are distinct and can be borrowed independently, it does not try to
borrow all of `a`, which would make this pattern useless.
Loading…
Cancel
Save