6 releases (stable)
new 1.1.0 | Jan 12, 2025 |
---|---|
1.0.2 | Nov 14, 2024 |
0.2.0 | Nov 11, 2024 |
0.1.3 | Nov 11, 2024 |
#317 in Rust patterns
119 downloads per month
55KB
273 lines
Click here to read the docs!
lib.rs
:
🔪 Partial Borrows
Zero-overhead
"partial borrows",
borrows of selected fields only, including partial self-borrows. It lets you split structs
into non-overlapping sets of mutably borrowed fields, like &<mut field1, field2>MyStruct
and
&<field2, mut field3>MyStruct
. It is similar to
slice::split_at_mut
but more flexible and tailored for structs.
🤩 Why partial borrows? Examples included!
Partial borrows offer a variety of advantages. Each of the following points includes a short in-line explanation with a link to an example code with a detailed explanation:
🪢 You can partially borrow self in methods (click to see example)
You can call a function that takes partially borrowed fields from &mut self
while holding
references to other parts of Self
, even if it contains private fields.
👓 Partial borrows make your code more readable and less error-prone (click to see example).
They allow you to drastically shorten function signatures and their usage places. They also enable you to keep the code unchanged, e.g., after adding a new field to a struct, instead of manually refactoring in potentially many places.
🚀 Partial borrows improve performance (click to see example)
Passing a single partial reference is more efficient than passing multiple separate references, resulting in better-optimized code.
📖 Other literature
In real-world applications, lack of partial borrows often affects API design, making code hard to maintain and understand. This issue was described multiple times over the years, some of the most notable discussions include:
In real-world applications, the lack of partial borrows often affects API design, making code hard to maintain and understand. This issue has been described multiple times over the years. Some of the most notable discussions include:
- Rust Internals "Notes on partial borrow".
- The Rustonomicon "Splitting Borrows".
- Niko Matsakis Blog Post "After NLL: Interprocedural conflicts".
- Afternoon Rusting "Multiple Mutable References".
- Partial borrows Rust RFC.
- HackMD "My thoughts on (and need for) partial borrows".
📖 borrow::Partial
derive macro
This crate provides the borrow::Partial
derive macro, which lets your structs be borrowed
partially.
⚠️ Some code was collapsed for brevity, click to expand.
use std::vec::Vec;
// ============
// === Data ===
// ============
type NodeId = usize;
type EdgeId = usize;
struct Node {
outputs: Vec<EdgeId>,
inputs: Vec<EdgeId>,
}
struct Edge {
from: Option<NodeId>,
to: Option<NodeId>,
}
struct Group {
nodes: Vec<NodeId>,
}
// =============
// === Graph ===
// =============
#
#
#
#
#
#
#[derive(borrow::Partial)]
#[module(crate)]
struct Graph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
pub groups: Vec<Group>,
}
The most important code that this macro generates is:
#
pub struct GraphRef<Nodes, Edges, Groups> {
pub nodes: Nodes,
pub edges: Edges,
pub groups: Groups,
}
impl Graph {
pub fn as_refs_mut(&mut self) ->
GraphRef<
&mut Vec<Node>,
&mut Vec<Edge>,
&mut Vec<Group>,
>
{
GraphRef {
nodes: &mut self.nodes,
edges: &mut self.edges,
groups: &mut self.groups
}
}
}
All partial borrows of the Graph
struct will be represented as &mut GraphRef<...>
with type
parameters instantiated to one of &T
, &mut T
, or Hidden<T>
, a marker for fields
inaccessible in the current borrow.
Please note the usage of the #[module(...)]
attribute, which specifies the path to the module
where the macro is invoked. This attribute is necessary because Rust does not allow procedural
macros to automatically detect the path of the module they are used in.
If you intend to use the generated macro from another crate, avoid using the crate::
prefix
in the #[module(...)]
attribute. Instead, refer to your current crate by its name, for
example: #[module(my_crate::data)]
and add extern crate self as my_crate;
to your lib.rs
/ main.rs
.
📖 borrow::partial
(p!
) macro
This crate provides the borrow::partial
macro, which we recommend importing under a shorter
alias p
for concise syntax. The macro allows you to parameterize borrows similarly to how you
parameterize types. Let's see how the macro expansion works:
// Given:
#
#
#
#
fn test1(graph: p!(&<nodes, mut edges> Graph)) {}
// It will expand to:
fn test2(graph: &mut p!(<nodes, mut edges> Graph)) {}
// Which will expand to:
fn test3(
graph: &mut GraphRef<
&Vec<Node>,
&mut Vec<Edge>,
Hidden<Vec<Group>>
>
) {}
The macro implements the syntax proposed in Rust Internals "Notes on partial borrow", extended with utilities for increased expressiveness:
-
Field References You can parameterize a reference by providing field names this reference should contain.
# # # # // Contains: // 1. Immutable reference to the 'nodes' field. // 2. Mutable reference to the 'edges' field. fn test(graph: p!(&<nodes, mut edges> Graph)) { /* ... */ }
-
Field Selectors You can use
*
to include all fields and!
to exclude fields. Later selectors override previous ones.# # # # // Contains: // 1. Mutable references to all, but 'edges' and 'groups' fields. // 2. Immutable reference to the 'edges' field. fn test(graph: p!(&<mut *, edges, !groups> Graph)) { /* ... */ }
-
Lifetime Annotations You can specify lifetimes for each reference. If a lifetime is not provided, it defaults to
'_
. You can override the default lifetime ('_
) by providing it as the first argument.# # # # // Contains: // 1. References with the 'b lifetime to all but the 'mesh' fields. // 2. Reference with the 'c lifetime to the 'edges' field. // // Due to explicit partial reference lifetime 'a, the inferred // lifetime dependencies are 'a:'b and 'a:'c. fn test<'a, 'b, 'c>(graph: p!(&'a <'b *, 'c edges>Graph)) { /* ... */ } // Contains: // 1. Reference with the 't lifetime to the 'nodes' field. // 2. Reference with the 't lifetime to the 'edges' field. // 3. Reference with the 'm lifetime to the 'groups' field. type PathFind<'t, 'm> = p!(<'t, nodes, edges, 'm groups> Graph);
📖 The partial_borrow
, split
, and extract_$field
methods.
The borrow::Partial
derive macro also generates the partial_borrow
, split
, and an
extraction method per struct field. These methods let you transform one partial borrow
into another:
-
partial_borrow
lets you borrow only the fields required by the target type.# # # # fn test(graph: p!(&<mut *> Graph)) { let graph2 = graph.partial_borrow::<p!(<mut nodes> Graph)>(); }
-
split
is likepartial_borrow
but also returns a borrow of the remaining fields.# # # # fn test(graph: p!(&<mut *> Graph)) { // The inferred type of `graph3` is `p!(&<mut *, !nodes> Graph)`, // which expands to `p!(&<mut edges, mut groups> Graph)` let (graph2, graph3) = graph.split::<p!(<mut nodes> Graph)>(); }
-
extract_$field
is like split, but for single field only.# # # # fn test(graph: p!(&<mut *> Graph)) { // The inferred type of `nodes` is `p!(&<mut nodes> Graph)`. // The inferred type of `graph2` is `p!(&<mut *, !nodes> Graph)`. let (nodes, graph2) = graph.extract_nodes(); }
The following example demonstrates usage of these functions. Read the comments in the code to
learn more. You can also find this example in the tests
directory.
⚠️ Some code was collapsed for brevity, click to expand.
use std::vec::Vec;
use borrow::partial as p;
use borrow::traits::*;
// ============
// === Data ===
// ============
type NodeId = usize;
type EdgeId = usize;
#[derive(Debug)]
struct Node {
outputs: Vec<EdgeId>,
inputs: Vec<EdgeId>,
}
#[derive(Debug)]
struct Edge {
from: Option<NodeId>,
to: Option<NodeId>,
}
#[derive(Debug)]
struct Group {
nodes: Vec<NodeId>,
}
#
#
#
#
#
#
// =============
// === Graph ===
// =============
#[derive(Debug, borrow::Partial)]
#[module(crate)]
struct Graph {
nodes: Vec<Node>,
edges: Vec<Edge>,
groups: Vec<Group>,
}
// =============
// === Utils ===
// =============
// Requires mutable access to the `graph.edges` field.
fn detach_node(graph: p!(&<mut edges> Graph), node: &mut Node) {
for edge_id in std::mem::take(&mut node.outputs) {
graph.edges[edge_id].from = None;
}
for edge_id in std::mem::take(&mut node.inputs) {
graph.edges[edge_id].to = None;
}
}
// Requires mutable access to all `graph` fields.
fn detach_all_nodes(graph: p!(&<mut *> Graph)) {
// Extract the `nodes` field.
// The `graph2` variable has a type of `p!(&<mut *, !nodes> Graph)`.
let (nodes, graph2) = graph.extract_nodes();
for node in nodes {
detach_node(graph2.partial_borrow(), node);
}
}
// =============
// === Tests ===
// =============
fn main() {
// node0 -----> node1 -----> node2 -----> node0
// edge0 edge1 edge2
let mut graph = Graph {
nodes: vec![
Node { outputs: vec![0], inputs: vec![2] }, // Node 0
Node { outputs: vec![1], inputs: vec![0] }, // Node 1
Node { outputs: vec![2], inputs: vec![1] }, // Node 2
],
edges: vec![
Edge { from: Some(0), to: Some(1) }, // Edge 0
Edge { from: Some(1), to: Some(2) }, // Edge 1
Edge { from: Some(2), to: Some(0) }, // Edge 2
],
groups: vec![]
};
detach_all_nodes(&mut graph.as_refs_mut());
for node in &graph.nodes {
assert!(node.outputs.is_empty());
assert!(node.inputs.is_empty());
}
for edge in &graph.edges {
assert!(edge.from.is_none());
assert!(edge.to.is_none());
}
}
Partial borrows of self in methods
The above example can be rewritten to use partial borrows of self
in methods.
⚠️ Some code was collapsed for brevity, click to expand.
use std::vec::Vec;
use borrow::partial as p;
use borrow::traits::*;
// ============
// === Data ===
// ============
type NodeId = usize;
type EdgeId = usize;
#[derive(Debug)]
struct Node {
outputs: Vec<EdgeId>,
inputs: Vec<EdgeId>,
}
#[derive(Debug)]
struct Edge {
from: Option<NodeId>,
to: Option<NodeId>,
}
#[derive(Debug)]
struct Group {
nodes: Vec<NodeId>,
}
// =============
// === Graph ===
// =============
#[derive(Debug, borrow::Partial)]
#[module(crate)]
struct Graph {
nodes: Vec<Node>,
edges: Vec<Edge>,
groups: Vec<Group>,
}
#
#
#
#
#
#
#
#
#
#
impl p!(<mut edges, mut nodes> Graph) {
fn detach_all_nodes(&mut self) {
let (nodes, self2) = self.extract_nodes();
for node in nodes {
self2.detach_node(node);
}
}
}
impl p!(<mut edges> Graph) {
fn detach_node(&mut self, node: &mut Node) {
for edge_id in std::mem::take(&mut node.outputs) {
self.edges[edge_id].from = None;
}
for edge_id in std::mem::take(&mut node.inputs) {
self.edges[edge_id].to = None;
}
}
}
⚠️ Some code was collapsed for brevity, click to expand.
#
#
#
#
#
#
#
#
#
#
// =============
// === Tests ===
// =============
fn main() {
// node0 -----> node1 -----> node2 -----> node0
// edge0 edge1 edge2
let mut graph = Graph {
nodes: vec![
Node { outputs: vec![0], inputs: vec![2] }, // Node 0
Node { outputs: vec![1], inputs: vec![0] }, // Node 1
Node { outputs: vec![2], inputs: vec![1] }, // Node 2
],
edges: vec![
Edge { from: Some(0), to: Some(1) }, // Edge 0
Edge { from: Some(1), to: Some(2) }, // Edge 1
Edge { from: Some(2), to: Some(0) }, // Edge 2
],
groups: vec![],
};
graph.as_refs_mut().partial_borrow().detach_all_nodes();
for node in &graph.nodes {
assert!(node.outputs.is_empty());
assert!(node.inputs.is_empty());
}
for edge in &graph.edges {
assert!(edge.from.is_none());
assert!(edge.to.is_none());
}
}
Please note, that you do not need to provide the partially borrowed type explicitly, it will be
inferred automatically. For example, the detach_all_nodes
method requires self to have the
edges
and nodes
fields mutably borrowed, but you can simply call it as follows:
#
#
#
fn main() {
let mut graph: Graph = Graph::default();
let mut graph_ref: p!(<mut *>Graph) = graph.as_refs_mut();
graph_ref.partial_borrow().detach_all_nodes();
}
Why identity partial borrow is disallowed?
Please note, that the partial_borrow
method does not allow you to request the same fields as
the original borrow. This is to enforce the code to be explicit and easy to understand:
- Whenever you see the call to
partial_borrow
, you can be sure that target borrow uses subset of fields from the original borrow:# use std::vec::Vec; # use borrow::partial as p; # use borrow::traits::*; # # #[derive(Default, borrow::Partial)] # #[module(crate)] # struct Graph { # nodes: Vec<usize>, # edges: Vec<usize>, # } # # impl p!(<mut nodes> Graph) { # fn detach_all_nodes(&mut self) {} # } # # fn main() {} # fn run(graph: p!(&<mut nodes, mut edges> Graph)) { // ERROR: Cannot partially borrow the same fields as the original borrow. // Instead, you should pass `graph` directly as `test(graph)`. test(graph.partial_borrow()) } fn test(graph: p!(&<mut nodes, mut edges> Graph)) { /* ... */ }
- If you refactor your code and the new version does not require all field references it used
to require, you will get compilation errors in all usage places that were assuming the full
usage. This allows you to easily review the places that either need to introduce a new
partial borrow or need to update their type signatures:
# # # # fn run(graph: p!(&<mut nodes, mut edges> Graph)) { test(graph) } // Changing this signature to `test(graph: p!(&<mut nodes> Graph))` would // cause a compilation error in the `main` function, as the required borrow // is smaller than the one provided. There are two possible solutions: // 1. Change the call site to `test(graph.partial_borrow())`. // 2. Change the `main` function signature to reflect the new requirements: // `main(graph: p!(&<mut nodes> Graph))`. fn test(graph: p!(&<mut nodes, mut edges> Graph)) { /* ... */ }
- In case you want to opt-out from this check, there is also a
partial_borrow_or_identity
method that does not perform this compile-time check. However, we recommend using it only in exceptional cases, as it may lead to confusion and harder-to-maintain code.
Dependencies
~0.7–1.1MB
~24K SLoC