Expand description
§🔪 Partial Borrows
Zero-cost
“partial borrows”
— the ability to borrow only selected fields of a struct, including partial self-borrows.
This allows splitting a struct into disjoint, mutably borrowed field sets, such as
&<mut field1, field2>MyStruct and &<field2, mut field3>MyStruct. It is conceptually
similar to slice::split_at_mut,
but tailored for structs and more flexible.
§🤩 Why Partial Borrows? With Examples!
Partial borrows provide several advantages. Each point below includes a brief explanation and a link to a detailed example:
§🪢 You can partially borrow self in methods (click to see example)
Allows invoking functions that take only specific fields of &mut self while simultaneously
accessing other fields of self, even if some are private.
§👓 Improves code readability and reduces errors (click to see example)
Enables significantly shorter function signatures and usage. It also allows you to leave the code unchanged after adding new fields to a struct — no need for extensive refactoring.
§🚀 Boosts performance (click to see example)
Passing a single partial reference is more efficient than passing multiple individual references, resulting in better-optimized code.
§📖 Further Reading
The lack of partial borrows often complicates API design in real-world applications, leading to code that is harder to maintain and understand. The topic has been discussed extensively:
- 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”.
§📖 TL;DR Example
A simple example is worth more than a thousand words. The code below demonstrates the basics of partial borrows. While simple, it serves as a good starting point for the more advanced examples discussed later.
use std::vec::Vec;
use borrow::partial as p;
use borrow::traits::*;
// =============
// === Graph ===
// =============
type NodeId = usize;
type EdgeId = usize;
#[derive(Debug, borrow::Partial)]
#[module(crate)]
struct Graph {
nodes: Vec<Node>,
edges: Vec<Edge>,
groups: Vec<Group>,
}
/// A node in a graph.
#[derive(Debug)]
struct Node {
outputs: Vec<EdgeId>,
inputs: Vec<EdgeId>,
}
/// An edge between two nodes.
#[derive(Debug)]
struct Edge {
from: Option<NodeId>,
to: Option<NodeId>,
}
/// A group (cluster) of nodes.
#[derive(Debug)]
struct Group {
nodes: Vec<NodeId>,
}
// =============
// === Utils ===
// =============
// Requires mutable access to the `graph.edges` field.
fn detach_node(mut 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 the `graph` fields.
fn detach_all_nodes(mut graph: p!(&<mut *> Graph)) {
// Borrow the `nodes` field. The `graph2` variable has a type of
// `p!(&<mut edges, mut groups> Graph)`.
let (nodes, mut graph2) = graph.borrow_nodes_mut();
for node in nodes {
detach_node(p!(&mut graph2), 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![]
};
// Borrow the fields required by the `detach_all_nodes` function.
detach_all_nodes(p!(&mut graph));
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());
}
}§📖 borrow::Partial derive macro
This crate provides the borrow::Partial derive macro, which enables partial borrows for your
structs. Let’s revisit the Graph struct:
#[derive(borrow::Partial)]
#[module(crate)]
struct Graph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
pub groups: Vec<Group>,
}All partial borrows of this struct are represented as &mut GraphRef<Graph, ...> with type
parameters instantiated to &T, &mut T, or Hidden (a marker indicating an inaccessible
field). Here’s a simplified version of what GraphRef looks like:
pub struct GraphRef<__Self__, __Tracking__, Nodes, Edges, Groups> {
pub nodes: Nodes,
pub edges: Edges,
pub groups: Groups,
marker: std::marker::PhantomData<(__Self__, __Tracking__)>,
}
impl Graph {
pub fn as_refs_mut(&mut self) ->
GraphRef<
Self,
borrow::True,
&mut Vec<Node>,
&mut Vec<Edge>,
&mut Vec<Group>,
>
{
GraphRef {
nodes: &mut self.nodes,
edges: &mut self.edges,
groups: &mut self.groups,
marker: std::marker::PhantomData,
}
}
}As you see, there are two special phantom parameters: __Self__ and __Tracking__. The former
is always instantiated with the original struct type (in this case, Graph) and is used to
track type parameters when working with generic or polymorphic structs. This is especially
useful when defining traits for partially borrowed types.
The latter parameter, __Tracking__, controls whether the system should emit diagnostics
related to unused borrowed fields.
In reality, the GraphRef struct is slightly more complex to support runtime diagnostics for
unused borrows. These diagnostics introduce a small performance overhead, but only in debug
builds. When compiled in release mode, the structure is optimized to match the simplified
version, and the overhead is completely eliminated. Let’s look at what the actual GraphRef
structure looks like:
pub struct GraphRef<__Self__, __Tracking__, Nodes, Edges, Groups>
where __Tracking__: borrow::Bool {
pub nodes: borrow::Field<__Tracking__, Nodes>,
pub edges: borrow::Field<__Tracking__, Edges>,
pub groups: borrow::Field<__Tracking__, Groups>,
marker: std::marker::PhantomData<__Self__>,
// In release mode this is optimized away.
usage_tracker: borrow::UsageTracker,
}
impl Graph {
pub fn as_refs_mut(&mut self) ->
GraphRef<
Self,
borrow::True,
&mut Vec<Node>,
&mut Vec<Edge>,
&mut Vec<Group>,
>
{
let usage_tracker = borrow::UsageTracker::new();
GraphRef {
// In release mode this is the same as `&mut self.nodes`.
nodes: borrow::Field::new(
"nodes",
Some(borrow::Usage::Mut),
&mut self.nodes,
usage_tracker.clone(),
),
// In release mode this is the same as `&mut self.edges`.
edges: borrow::Field::new(
"edges",
Some(borrow::Usage::Mut),
&mut self.edges,
usage_tracker.clone(),
),
// In release mode this is the same as `&mut self.groups`.
groups: borrow::Field::new(
"groups",
Some(borrow::Usage::Mut),
&mut self.groups,
usage_tracker.clone(),
),
marker: std::marker::PhantomData,
usage_tracker,
}
}
}Note: both the borrow::UsageTracker and borrow::Field wrappers are fully optimized out in
release builds, ensuring zero runtime overhead. They exist solely to provide enhanced
diagnostics about unused field borrows, as explained later in this documentation.
§📖 borrow::partial (p!) macro
This crate provides the borrow::partial macro, which we recommend importing under a shorter
alias, p, for convenience. The macro can be used both at the type level to specify the type of
partial borrow, and at the value level to create a partial borrow instance. Let’s see how the
macro expands. Given the code:
fn test(graph: p!(&<mut *> Graph)) {
test2(p!(&mut graph));
}
fn test2(graph: p!(&<nodes, mut edges> Graph)) {
// ...
}It will expand to the following:
fn test(graph:
&mut GraphRef<
Graph,
borrow::True,
&mut Vec<Node>,
&mut Vec<Edge>,
&mut Vec<Group>
>
) {
test2(&mut graph.partial_borrow())
}
fn test2(graph:
&mut GraphRef<
Graph,
borrow::True,
&Vec<Node>,
&mut Vec<Edge>,
Hidden
>
) {
// ...
}
More formally, the macro implements syntax as proposed in the Rust Internals: Notes on Partial Borrow, and extends it with utilities for increased flexibility:
-
Field References
You can specify which fields to borrow by naming them explicitly.// 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
Use*to include all fields. Later selectors override earlier ones.// Contains: // 1. Mutable references to all, but the 'edges' field. // 2. Immutable reference to the 'edges' field. fn test(graph: p!(&<mut *, edges> Graph)) { /* ... */ } -
Lifetime Annotations
You can attach lifetimes to each reference. If not specified,'_is used by default. You can override the default by attaching lifetimes after the&.// Contains: // 1. References with the 'a lifetime to all but the 'mesh' fields. // 2. Reference with the 'b lifetime to the 'edges' field. fn test<'a, 'b>(graph: p!(&<'a *, 'b 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); -
Owned Borrows
You can omit the&to create an owned partial borrow. For example:p!(&<mut *> Graph)expands to&mut GraphRef<...>.p!(<mut *> Graph)expands toGraphRef<...>.
This is especially useful when defining methods or implementing traits for partial borrows, as traits can’t be implemented for reference types directly in many cases.
/// Methods defined on a partially borrowed struct. impl p!(<mut edges, mut nodes> Graph) { fn detach_all_nodes(&mut self) { // ... } }
§📖 The partial_borrow, split, and borrow_$field methods.
Partially borrowed structs expose a set of methods that allow transforming one partial borrow
into another. The p! macro can also be used as shorthand for the partial_borrow method.
-
fn partial_borrow<'s, Target>(&'s mut self) -> Target where Self: Partial<'s, Target>
Allows borrowing only the fields specified by the target type. You don’t need to callas_refs_mutexplicitly,partial_borrowhandles it internally.fn main() { let mut graph = Graph::default(); // Creating a partial borrow (recommended way): test(p!(&mut graph)); // Which expands to: test(&mut graph.partial_borrow()); // Which expands to: test(&mut graph.as_refs_mut().partial_borrow()); } fn test(mut graph: p!(&<mut *> Graph)) { // Creating a partial borrow of the current borrow (recommended way): test2(p!(&mut graph)); // The above is the same as the following: test2(&mut graph.partial_borrow()); // Which is the same as the most explicit version: let graph2 = &mut graph.partial_borrow::<p!(<mut nodes> Graph)>(); test2(graph2); } fn test2(graph: p!(&<mut nodes> Graph)) { // ... } -
fn split<'s, Target>(&'s mut self) -> (Target, Self::Rest) where Self: Partial<'s, Target>
Similar topartial_borrow, but also returns a borrow of the remaining fields.fn test(mut graph: p!(&<mut *> Graph)) { // The inferred type of `graph3` is `p!(&<mut edges, mut groups> Graph)`. let (graph2, graph3) = graph.split::<p!(<mut nodes> Graph)>(); } -
borrow_$fieldandborrow_$field_mutare like split, but for single field only.fn test(mut graph: p!(&<mut *> Graph)) { // Type of `nodes` is `&mut Vec<Node>`. // Type of `graph2` is `p!(&<mut edges, mut groups> Graph)`. let (nodes, graph2) = graph.borrow_nodes_mut(); // ... // Type of `edges` is `&Vec<Edge>`. // Type of `graph2` is `p!(&<mut nodes, edges, mut groups> Graph)`. let (edges, graph3) = graph.borrow_edges(); }
The following example demonstrates how to use these functions in practice. Refer to comments
in the source for additional context. This example is also available 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(mut 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(mut graph: p!(&<mut *> Graph)) {
// Borrow the `nodes` field.
// The `graph2` has a type of `p!(&<mut edges, mut groups> Graph)`.
let (nodes, mut graph2) = graph.borrow_nodes_mut();
for node in nodes {
detach_node(p!(&mut graph2), 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(p!(&mut graph));
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 example above can be rewritten to use partial borrows directly on self in method
implementations.
⚠️ 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, mut self2) = self.borrow_nodes_mut();
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![],
};
p!(&mut graph).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();
p!(&mut graph).detach_all_nodes();
}§Unused borrows tracking
This crate makes it easy to keep track of which fields are actually used, which is helpful when refactoring or trying to minimize the set of required borrows.
Unlike standard Rust or Clippy lints, diagnostics for unused borrows are performed at runtime, and they incur overhead in debug builds. The diagnostics can be disabled or optimized away entirely using the following mechanisms:
- Enabled by default in debug builds.
- Disabled in release builds.
- Can be turned off explicitly with the
no_usage_trackingfeature. - Can be forced on in release with the
usage_trackingfeature.
Consider the following code:
struct Node;
struct Edge;
struct Group;
#[derive(borrow::Partial, Default)]
#[module(crate)]
struct Graph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
pub groups: Vec<Group>,
}
fn main() {
let mut graph = Graph::default();
pass1(p!(&mut graph));
}
fn pass1(mut graph: p!(&<mut *> Graph)) {
pass2(p!(&mut graph));
}
fn pass2(mut graph: p!(&<mut nodes, edges> Graph)) {
let _ = &*graph.nodes;
}When running it, you’ll see the following output in stderr:
Warning [lib/src/lib.rs:19]:
Borrowed but not used: edges.
Borrowed as mut but used as ref: nodes.
To fix the issue, use: &<nodes>.
Warning [lib/src/lib.rs:15]:
Borrowed but not used: groups.
To fix the issue, use: &<mut edges, mut nodes>.After fixing, it becomes:
struct Node;
struct Edge;
struct Group;
#[derive(borrow::Partial, Default)]
#[module(crate)]
struct Graph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
pub groups: Vec<Group>,
}
fn main() {
let mut graph = Graph::default();
pass1(p!(&mut graph));
}
fn pass1(mut graph: p!(&<mut edges, nodes> Graph)) {
// Simulate mut usage of edges.
let _ = &mut *graph.edges;
pass2(p!(&mut graph));
}
fn pass2(mut graph: p!(&<nodes> Graph)) {
// Simulate ref usage of nodes.
let _ = &*graph.nodes;
}§Special Case 1: Trait Interface
When passing a partial borrow into a trait method you consider an interface, you might not want
the diagnostics to complain about unused fields. You can prefix the & with _ to silence tracking:
struct Node;
struct Edge;
struct Group;
#[derive(borrow::Partial, Default)]
#[module(crate)]
struct Graph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
pub groups: Vec<Group>,
}
trait RenderPass {
fn run_pass(graph: p!(_&<mut *> Graph));
}
struct MyRenderPass;
impl RenderPass for MyRenderPass {
fn run_pass(graph: p!(_&<mut *> Graph)) {
// Simulate mut usage of edges.
let _ = &mut *graph.edges;
}
}
fn main() {
let mut graph = Graph::default();
// No warnings here.
MyRenderPass::run_pass(p!(&mut graph));
}§Special Case 2: Conditional Use
If your function uses a borrow only under certain conditions, you can silence the warnings by manually marking all fields as used:
struct Node;
struct Edge;
struct Group;
#[derive(borrow::Partial, Default)]
#[module(crate)]
struct Graph {
pub nodes: Vec<Node>,
pub edges: Vec<Edge>,
pub groups: Vec<Group>,
}
fn main() {
let mut graph = Graph::default();
pass1(true, p!(&mut graph));
pass1(false, p!(&mut graph));
}
fn pass1(run2: bool, mut graph: p!(&<mut edges, nodes> Graph)) {
// Simulate mut usage of edges.
let _ = &mut *graph.edges;
if run2 {
pass2(p!(&mut graph));
} else {
// Disable field usage tracking for this condition.
graph.mark_all_fields_as_used();
}
}
fn pass2(mut graph: p!(&<nodes> Graph)) {
// Simulate ref usage of nodes.
let _ = &*graph.nodes;
}If the struct isn’t used at all, Clippy will still warn you about the unused variable, but partial borrow diagnostics will be suppressed.