Credit to pczarn in this thread Partial borrowing (for fun and profit) · Issue #1215 · rust-lang/rfcs · GitHub for the general idea and syntax.
I was looking through some suggestions regarding view types and partial borrowing and had an epyphany. This is my proposal:
Definitions
The Nill Lifetime
For my proposal to be sound and integrate well into the existing rules of borrowing, I had to include the nill lifetime - an antithesis of the 'static
lifetime. Let '!
denote the lifetime that represents no code regions at all. In other words, for<'a> 'a: '!
is a true statement. Effectively, it is meaningless - no reference &'! T
can even be used because everything outlives it.
On its own, the nill lifetime is useless, but it will come in handy later.
Lifespan
Let us define a lifespan to be a lifetime-acces pair, where 'access' is either mutable or immutable. In other words, 'a mut
is a lifespan, and so is 'static
an so on. I will denote a lifespan as "a
, analogous to the 'a
syntax.
Hence, any reference may be boiled down to a lifespan &'a mut T
is the same as &"a T
, where "a = 'a mut
.
On its own, this notion is virtually useless. It is important for my definition of composite lifespans.
Composite Lifespan
A composite lifespan is a "tuple" containing multiple (composite) lifespans, like ('a, 'b mut, 'static)
. For clarity, I will use the syntax ~a
to denote a composite lifespan named a
. Can't think of a better symbol to use, so it's a placeholder.
Note that composite lifespans may be recursive:
('a, ('b mut, 'c), 'd mut)
is a valid composite lifespan.
Fragmented References
Composite lifespans allow us to define a more general version of partial borrows and views into structs. The main challenge with implementing view types are the private fields of a (tuple) struct. I therefore propose the following syntax:
pub struct Struct<T>{
pub x: i32,
pub y: i32,
z: T
}
struct ref &("x, "y, ~z) Struct<T>
where
&~z T:
{
x: "x,
y: "y,
z: ~z
}
I the struct ref
block we tell the compiler that referencea to Struct
shall have composite lifespans consisting of two lifespans "x
and "y
and a composite lifespan ~z
that may be used with the type T
(hence, the where &~z T:
clause).
To use this syntax in function calls one would do:
impl<T> Struct<T> {
pub fn get_x<'x>(&('x mut, '!, '!) self) -> &'x mut i32 {
&mut self.x
// `&mut self.y`
//--^ value does not live long enough: '! needs to outlive 'x
}
pub fn get_y<'y>(&('!, 'y mut, '!) self) -> &'y mut i32 {
&mut self.y
}
pub fn get_z<'z>(&('!, '!, 'z mut) self) -> &'z mut T {
&mut self.z
}
}
fn main() {
let mut s: Struct = ...;
let x = s.get_x();
let y = s.get_y();
let z = s.get_z();
*x = -1;
*y = 42;
*z = ...;
}
Let's observe what happens behind the scenes:
- We create an object of type
Struct
. - The call to
Struct::get_x
takes a fragmented reference&('0 mut, '!, '!) s
which tells the borrowck "whatever insides
that is tied to the first lifetime is borrowed mutably for'0
". - The subsequent call to
Struct::get_y
uses a fragmented reference&(
!, '1 mut, '!) s. Normally, borrowing the same struct mutably twice is impossible, however, in this case, the stuff in
sthat is mutably borrowed throughout the lifetime
'1(namely,
s.x, though it could be any number of private fields!) cannot be borrowed inside the body of the function
Struct::get_y` for it has been declared to possess the nill lifetime, which does not overlap with any other lifetime ('nothing' cannot overlap with anything, per definition). - By fhe same reasoning,
Struct::get_z
may be called in conjunction with the first two methods, thus letting us borrow the private field disjointly from the public ones. Currently, such a mechanism is impossible due to the inability to deconstruct private fields.
Rules
- Any number of fragmented references are allowed to coexist simulataneously as long as the sets corresponding to each entry across all said references obey the "multiple readers, single writer" rule.
For instance, given a type with a fragmented reference
&(~a, ~b, ~c)
, a set of {&(~a_i, ~b_i, ~c_i)
} can exist if the sets {&~a_i
}, {&~b_i
}, {&~c_i
} obey the ownership rules. Because fragmented references may be recursive, so is the rule described above. The terminating condition is when we get to a set of lifespans, for which the ownership rules already exist in the language. - All non-uniform fragmented references to primitive types shall be valid yet unusable in safe Rust - to be discussed later.
- Only one type of a composite lifespan shall exist for product types (structs, tuple structs and tuples).
- A regular reference with lifespan
"0
to a type with a defined composite lifespan(~1, ~2, .., ~n)
is the same as&("0, "0, .., "0)
. - A lifespan
"0
may coerce to"1
if and only if:"0
ismut
or both aren't.- The lifetime of
"0
contains the lifetime of"1
- A composite lifespan
~0
may coerce to a composite lifespan~1
if and only if every entry in~0
can coerce to the corresponding entry in~1
. Recursion stops at lifespans. - Any lifetime may coerce to
'!
. '!
shall not outlive any lifetime that isn't'!
.- Any number of (im)mutable references with the nill lifetime may exist at the same time with any set of references that itself obeys the ownership rules.
- Only
&'_ T
may implement theCopy
trait. - If a type doesn't implement a composite lifespan rule, every field/element recieves a unique associated lifespan by default.
- Only fragmented references of the same "kind" to a primitive type shall exist at the same time. For instance, one cannot use a
&('!, '_ mut) i32
while a&('_, '!, '!) I32
to the same object already exists and its lifetime overlaps with that of the former. - Given a type with public fields, during a partial borrow/deconstruction the object is being borrowed with a fragmented reference whose lifespans which are associated to the borrowed fields are inferred, with the rest being
'!
or a recursive composite lifespan that ends with'!
.
These rules ensure that composite lifespans and fragmented references are useful, complement the concepts of ownership and references and do not break the already established rules of the language
Syntax
- Defining a fragmented reference for a struct:
Any field can be assigned any of the mentioned (composite) lifespans/lifetimes. For any generic field there must be a correspondingstruct ref &(~a, ..., "b, ..., 'c, ...) Struct<T, ...> where &~a T:, ... { field0: ~a, ..., field_m: "b, ..., field_n: 'c, ... }
where
clause. Fields may be associated with the nill lifetime, making the field unusable via a (fragmented) reference. - Defining a fragmented reference for a tuple struct:
Orstruct ref &(~a, ..., "b, ..., 'c, ...) Struct<T, ...>(~a, ..., "b, ..., 'c, ...) where &~a T:, ...;
The same considerations as above apply here too.struct ref &(~a, ..., "b, ..., 'c, ...) Struct<T, ...> where &~a T:, ... { 0: ~a, ..., m: "b, ..., n: 'c, ... }
- Defining a fragmented reference to a (tuple) struct as a function parameter:
Orfn foo<'a, ...>(_: &('a, 'a mut, '!, '! mut, (...), ...) Struct) {...}
In other words, one should use the nill lifetime and a set of generic lifetimes or the ellided lifetime, perhaps recursively composite if the type allows so.fn foo(_: &('_, '_ mut, '!, '! mut, (...)) Struct) {...}
- Defining a fragmented reference to a tuple as a function parameter:
Which is syntactic sugar forfn foo<'a, ...>(_: &('a T1, 'a mut T2, '! T3, '! mut T4, (...) T...)) {...} fn bar(_: &('_ T1, '_ mut T2, '! T3, '! mut T4, (...) T5)) {...}
fn foo<'a, ...>(_: &('a, 'a mut, '!, '! mut, (...), ...) (T1, T2, ...)) {...} fn bar(_: &('_, '_ mut, '!, '! mut, (...)) (T1, T2, T3, T4, T5)) {...}
Usage
Apart from the example that was showed previously, with the partial borrowing through function barriers, fragmented references can be used as views in an iterative context:
Suppose we have a slice of Point
s, with Point
being
pub struct Point(f32, f32);
There is currently no safe, regulated way to obtain an iterator over the first coordinate separately from the second coordinate. That is, a function set_xs
must either accept an iterator that yields the first element of each Point
or a mutable slice to the whole thing, allowing one to mutate the second element though the API clearly speaks against it.
With fragmented references one could do the following:
struct ref &("x, "y) Point("x, "y);
fn set_xs(curr: &('_ mut, '!) [Point], new: &[f32]) {
todo!()
}
Behind that todo!()
should be a new function for the slice type:
impl<~a, T> &~a [T]
where
&~a T:
{
pub fn iter_frag(self: &~a [T]) -> IterFrag<~a, T> {
// `self` is unusable as a non-uniform fragmented reference
let ptr: *mut _ = unsafe{ transmute::<_, &mut [T]>(self) } as _;
IterFrag::new(ptr)
}
}
IterFrag
is a generalised Iter
/IterMut
that yields composite references instead of regular ones, thus restricting the access to the unwanted fields of the underlying data.
Though we had to resort to unsafe
to somehow use the slice reference, the whole operation is safe because of the ownership rules regarding fragmented references and the inner implementation of IterFrag
which does not touch the fields which are not borrowed by the caller.
Partial Borrowing
The proposed concepts must not interfere with its current slternative - partial borrows. Instead, is must compliment it. When a partial borrow/deconstruction occurs, the borrowck treats the event as if a fragmented reference was aquired. For example:
pub struct Point(pub f32, pub f32, f32);
struct ref &("xy, "z) Point("xy, "xy, "z);
let mut p: Point = ...;
let &mut Point { 0: ref x, 1: ref y, .. } = &mut p;
In the last line the compiler inserts a reference of the kind &('0 mut, '0 mut, '!) p
so that methods that use &('!, '!, '_ mut) Point
may still be called.
Note that the same would be true if only one of the fields was accessed, such as let mut &mut Point { 0: ref x, .. } = &mut p
, for the lifespan associated with p.0
and p.1
is the same.