-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Partial borrowing (for fun and profit) #1215
Comments
An alternate syntax (adds a new keyword (that could easily just be contextual), but in my opinion makes more sense and is clearer): impl Point {
pub fn x_mut(&mut self borrowing x) -> &mut f64 {
&mut self.x
}
}
// With a non-self parameter:
pub fn point_get_x(x: &Point borrowing x) -> &f64 {
&x.x
} Edit: another syntax that doesn’t require any syntactic additions to the language: impl Point {
pub fn x_mut(&mut Point { ref mut x, .. }: &mut Self) -> &mut f64 {
x
}
}
pub fn point_get_x(&Point { ref x, .. }: &Point) -> &f64 {
x
} Although in addition to requiring extra borrowck logic, it would require considering any ‘static’ method that has a |
@P1start I think either of those syntaxes would work fine as well! But, there would need to be a way to annotate explicit lifetimes for either syntax as well; something like this: pub fn x_mut<'a>(&mut self borrowing 'a x) -> &'a mut f64 { ... } or this: pub fn x_mut<'a>(&mut Point { 'a ref mut x, .. }: &mut Self) -> &mut f64 { ... } ...which would be necessary where lifetime elision would be ambiguous (such as a bare function that takes multiple |
I want a green roof on the bikeshed: impl Point {
pub fn x_mut<'a>(self: &'a mut Self::x) -> &'a mut f64 {
self.x
}
} for borrowing multiple fields you'd use Also this needs a lint that denies specifying all fields. |
I don't like the idea of a type signature requiring knowledge of private fields to understand, and if the field is public then of course there's not much point. The use case of borrowing multiple fields at the same time can be addressed at a slight cost to ergonomics by just having a single method return both, for example |
Yes, exposing names of private fields doesn't sit well. There's another way. Composite lifetimes allow separate borrows of individual parts of a type. Two mutable borrows with composite lifetimes struct Point<ref '(a, b)> where ref x: 'a, ref y: 'b {
x: f64,
y: f64
}
impl Point {
pub fn x_mut<'a>(&'(a,) mut self) -> &'a mut f64 {
&mut self.x
}
pub fn y_mut<'b>(&'(_, b) mut self) -> &'b mut f64 {
&mut self.y
}
}
// Typical lifetime parameters work.
struct PointRef<'a> {
borrowed: &'a Point
}
// type signature.
fn draw<'a>(point: PointRef<'a>) {}
// equivalent type signature, for some 'c, 'd.
fn draw<'c, 'd>(point: PointRef<'(c, d)>) {} |
I really, really like this proposal. I had some cases where I could choose to either rewrite the same piece of code a lot of times (because it's borrowing self) or clone the values before mutation (which is costy, especially if LLVM can't optimize it away). However, the syntaxes proposed here seems noisy and non-obvious. The syntax should be something that's easy to spot and write. I like the idea of @P1start's second proposal for a syntax because it seems consistent with the rust syntax. However, I find it hard to include lifetimes in that proposal, as |
@ticki What does your use case look like? Does the |
This was to enable me to have the backend own the grammar, which was the easiest option for me to force the grammar of the backend to match the grammar we use during codegen. I would like to exploe alternative options here; perhaps this RFC issue would be an avenue to explore: rust-lang/rfcs#1215
Just a vote for wanting this in some form. |
I've mentioned, e.g. What makes this attractive to me is that it's refinement to include only specific fields, which would interact not only with borrowing, but many other language features, such as partial moves, |
Here is some motivation fuel from a contributor to the sdl2 crate: https://cobrand.github.io/rust/sdl2/2017/05/07/the-balance-between-soundness-cost-useability.html |
I think this interacts well with #1546. Also, there is a cute color for shed door if you permit multiple self arguments, like :
It does not play so nicely with non-
|
I really would love to see this as well. But for partial self borrowing to happen I guess someone™ needs to write up an RFC, summarizing everything in this issue. |
Since we probably don't want to expose the names of private fields in a public API, there should be a way to declare abstract "borrow regions" for your type as part of the public API. You can then privately declare which fields belong to which region. Multiple fields can belong to one region. Here is an (arbitrary) example: // Note that this is just pseudo-syntax, I'm not proposing any particular syntax.
// This example uses a new keyword called `region`.
struct Vec<T> {
/// Data belonging to the `Vec`.
pub region data;
/// Length of the `Vec`.
pub region length;
#[region(data)] ptr: *mut T,
#[region(length)] len: i32,
}
impl<T> Vec<T> {
// Needs mutable access to `data`, but doesn't mutate `length`.
fn as_mut_slice(®ion(mut data, length) self) -> &mut [T] {
do_something_with(self.ptr, self.len)
}
// Only needs to access the `length` region.
fn len(®ion(length) self) -> usize {
self.len
}
// This one borrows everything, so no need for special region annotation.
// This means it borrows all regions.
fn retain<F>(&mut self, f: F) where F: FnMut(&T) -> bool {
// ...
}
} This would allow disjoint borrows, while still not exposing private fields in the public API or error messages. fn main() {
let mut v = vec![1, 2, 3];
// This is fine because the borrow regions for `as_mut_slice` and `len` don't conflict.
for num in v.as_mut_slice() {
println!("{} out of {}", num, v.len());
}
// Not valid:
v.retain(|num| num == v.len());
// Error: Cannot borrow region `length` of `v` as immutable, because it is also borrowed as mutable.
// v.retain(|&num| num == v.len());
// - ^^^^^^ - - mutable borrow of `length` ends here
// | | |
// | | borrow occurs due to use of `v` in closure
// | immutable borrow of `length` occurs here
// mutable borrow of `length` occurs here
} Oh, and this would also work nicely for traits. The trait could declare the regions it has, and the implementing type then can map that to its own private fields. |
That sounds like the disjointness rules in #1546 |
The fields in traits will resolve some of these issues. However they can't be used in case the struct/trait involves multi-field invariants, which are maintained by controlling their modification. An example of this would be While the region syntax might be a bit of an overkill for this scenario, I personally like it a lot. Here is another example with it that includes traits as well. I encountered the need in a scenario where the user impling their own type and they were holding immutable borrow for one field while wanting to call a method on self, which would have modified other disjoint fields. struct Sphere {
// Field belonging to two regions would allow callers to acquire two mutable
// references to the field by using two different mutable regions.
// Since we don't want this, we don't need to support defining fields in multiple
// regions. This allows us to use scopes instead of attributes here.
pub region position { x: f64, y: f64, z: f64 }
pub region size { radius: f64 }
pub region physics { weight : f64 }
}
impl Foo {
// Borrows everything.
fn copy_from(&mut self, other: &Sphere) { ... }
// Mutably borrows position.
fn move(&mut self borrows { &mut position }, x: f64, y: f64, z: f64) { ... }
// Immutable borrow of size.
fn get_size(&self borrows { &size }) -> f64 { ... }
// Bikeshedding: If we are using 'regions', any syntax that relies on field
// destructuring probably won't work.
} When it comes to traits, these could be implemented using specific regions. There's no reason borrowing for // Subtracting a point only alters the sphere position.
pub trait SubAssign<Point3D> for Sphere using { mut position } {
// 'self' is valid only on fields under 'position' here.
fn sub_assign(&mut self, rhs: Point3D) {
self.x -= rhs.x;
self.y -= rhs.y;
self.z -= rhs.z;
}
} Also traits could possibly describe their own regions. trait Render {
region geometry;
region texture;
...
}
// Render is implemented on Sphere using position and size.
// Borrowing 'physics' region is still valid while the sphere is being rendered.
impl Render for Sphere using { position, size } {
// Rendered geometry inludes both position and size.
region geometry = { position, size }
// Sphere doesn't contain texture data. It'll use static values
// for any method that would need textures.
region texture = {}
...
}
// Acquiring reference to the trait will automatically borrow the used regions.
let r : &Render = &sphere;
// OK:
// increase_weight mut borrows only 'physics' region, which isn't used by 'Render' impl:
increase_weight( &mut sphere );
// FAIL:
// grow mut borrows physics and size. This fails, as &Render is holding borrow to size region.
grow( &mut sphere ); |
I think regions are not a good idea. They do not convey anything about the reasons for being laid out in a particular way and they are at best tangent to describing which parts of the struct need to be borrowable separately as opposed to which parts lay close in whatever mental model. Overall I think the information about which part of a struct to borrow should be specified or inferred at the point at which it is borrowed. |
We now have arbitrary self types in traits. I think whatever syntax we use should approximate or mimic it. Personally, I lean towards the following: impl Point {
pub fn x_mut(self: Point { ref mut x, .. }) -> &mut f64 {
&mut self.x
}
pub fn y_mut(self: Point { ref mut y, ..}) -> &mut f64 {
&mut self.y
}
} |
I doubt
In particular, these methods have no One should decide that a series of destructuring borrows works this way too though. |
Maybe |
Would this be implicit or something developers can declare explicitly? Because if it is implicit, what fields are used in internal functions is now part of the API. This could affect combining two fields into a new enum or debugging implementations which perform extra sanity checks with different feature flags. |
It would have to be explicit for exactly that reason.
I would like to see the borrow checker eventually supporting |
I think they are different features because for For partial borrowing of struct members, I think it's really useful because we usually want getters and setters. |
As the discussion here has shown, any general syntax for partial borrowing will tie a function's signature very closely to its "private" implementation, either through explicit declarations about which disjoint structure fields are borrowed or through implicit analysis of the same performed automatically by the borrow checker. Either case has the undesired consequence of increasing the potential surface area for API-breaking changes. On the other hand, Rust really needs this feature to be an object-oriented programming language. At this point a lot of developers feel that that being object-oriented is actually undesirable (i.e. "objects tend to be bags of mutable state"), but @nikomatsakis does a great job laying out how unergonomic the current situation is in this blog post. It feels like we've been spinning our wheels on the general problem of partial borrowing for a few years now with no general solution in sight, but it may be much easier to solve partial borrows in the narrower but very common use case of interprocedural conflicts between private methods. What do the discussants here think about @crlf0710's proposal to opt-in to non-local borrow checking, in particular @kornelski's suggestion of limiting non-local borrow checking to private methods so as to avoid changing the public signatures of functions? Full example on Rust Playground. mod my_mod {
pub struct MyStruct {
somethings: Vec<SomeType>,
// ...
}
impl MyStruct {
pub fn do_something(&mut self)
{
for something in &self.somethings {
// Can't borrow all of self as &mut while &self.somethings is borrowed immutably.
#[inline_private_borrowchk]
self.do_something_else();
}
}
// Function is private, no effect on public API.
fn do_something_else(&mut self) {
// Do whatever we want to other members of MyStruct,
// but borrow checker will make sure we don't mutate self.somethings.
}
}
} |
Anyone doing object oriented design could usually employ interior mutability like Yes, I agree opt-in less-local borrowck sounds interesting, but maybe heavy development and rustc runtime costs, so not necessarily worth resources anytime soon. In the shorter term, we have
You'd then abstract over this within methods using |
Posted a question in the Rust Discord and was suggested to copy my problem here as an example of how the lack of split borrows can be unergonomic: I have a struct like this: struct WorldMap {
tiles: Vec<Tile>,
width: usize,
height: usize,
}
impl WorldMap {
pub fn get_tile_mut(&mut self, x: usize, y: usize) -> Option<&mut Tile> {
self.tiles.get_mut(y * self.width + x)
}
pub fn tile_to_position(&self, x: usize, y: usize) -> (f32, f32) {
// basically compute diff from x, y to center of tile map, then
// multiply by tile length to determine a position on the screen
}
} I have a function in which I'm checking and updating all the tiles that need updating via |
You could reverse your call order there, so no need for |
It seems that "row polymorphism" can play with this sense? |
There are some crates that let you do something like this: Full disclosure: I'm the author of |
@nikomatsakis's blog post on "view types" is definitely relevant here: https://smallcultfollowing.com/babysteps//blog/2021/11/05/view-types/ |
What is the state of partial borrowing in Rust in 2022? Are there newer or related RFCs that would allow some of these cases to work? Right now, in a library I'm prototyping, I'm stuck with exposing plain struct fields to get proper lifetime checking and partial borrows. // This is a singleton type representing a piece of hardware.
// (https://docs.rust-embedded.org/book/peripherals/singletons.html)
//
// Depending on the Mode, the display allows using one or more layers,
// which themselves are singletons, since they're bound to specific memory addresses.
//
// The layers must not be kept past a mode change, since their
// behaviour and access changes depending on the mode.
//
// However, we want partial borrows because the DisplayControl has
// functions that can be used independently of the layers.
pub struct Display<M: Mode> {
pub control: DisplayControlRegister<M>,
pub layers: M::DisplayLayers,
}
impl<M: Mode> Display<M> {
// Consumes self, so all references to layers must be cleaned up first
pub fn change_mode<N>(self) -> Display<N> {
/* ... */
}
} Which is fine enough, but not particularly ergonomic, since any structural change is now a breaking API change: use video::{DisplayControl, Mode0, Mode3};
fn main() {
let mut display = hw::init_display::<Mode3>();
// Alternatively, let Mode3Layers { ref mut bg2, ref mut obj } = display.layers;
// works as a kind of syntax sugar for this
let bg2 = &mut display.layers.bg2;
// This still works, the borrow checker gets it
display.control.write(
DisplayControl::new().enable_bg2()
);
bg2.write_pixel(120, 80, 0x001F);
bg2.write_pixel(136, 80, 0x03E0);
bg2.write_pixel(120, 96, 0x7C00);
// Can no longer use bg2 past this point
let mut display = display.change_mode::<Mode0>();
let bg0 = &mut display.layers.bg0;
// and so on...
} My use case feels quite similar to the gist mentioned in the issue |
Hi all,
I've been brainstorming for a solution, and I think I have a really simple solution. In short, adding the ability to create "associated lifetimes". For example:
The next question becomes how the lifetimes are used for an API designer (what makes them work). The idea here is pretty simple: any singular field borrowed by any code must always borrow the same associated lifetime.
If a user has internal access to a structure (such as accessing a structure with public fields) they are still going to have to borrow the partial lifetimes as mentioned above. This is leaking of the internals, but is only possible when the fields are public (or in other words, it only applies when the internals are already being used). There is one partial (get it? hahaha) problem I can see with inferring lifetimes here. Consider the following:
In this case, it's not immediately clear which fields are managed under which lifetime. It's still very much possible to deduce ('common is shared between both functions accessing b, and must therefore be responsible for managing b; "a" can belong to either "z" or "common") but it might be somewhat less intuitive, and more challenging to implement (+ debug).
(Though this ^^ is a last minute thought). I think this syntax solves most of the mentioned issues. For example (in the mentioned order):
Phew. Hopefully I explained myself well (and I'm not missing anything!). In short:
I'd love any feedback/criticism/tips anyone can provide. Thanks! |
@KevinThierauf wouldn't this require deep changes to how lifetimes and references work? AFAIK the act of borrowing is intrinsic in references, that is |
Conceptually, I don't think so. Essentially, instead of borrowing |
In my eyes you don't "borrow a lifetime", you "borrow for a lifetime", that is the lifetime only affect the point until you can borrow something. Also, this doesn't play well with lifetime bounds. Normally you add a bound to restrain the caller to a sufficiently long lifetime, however in your example the lifetime bounds are restraining the implementation of the function because they're no longer able to access the whole |
@KevinThierauf, thank you for sharing your idea. It's clear that you've given this problem a lot of thought. How would your "associated lifetimes" be substantially more ergonomic than @pczarn's "composite lifetimes" or @crumblingstatue's "borrow regions"? As an aside, since we're discussing this again, note that you can achieve some of the benefits of partial borrows on stable Rust using projection references to specify which members of a struct can be mutated disjointly. |
I agree -- sorry if my terminology isn't quite right! A more accurate statement on my behalf might be that you borrow a part of self for a lifetime, with the specific fields borrowed specified by the associated lifetime.
I think you are still restraining the caller to a sufficiently long lifetime (the lifetime of 'creatures, or 'buildings), but also restraining access to specific fields to prevent accessing the whole of
Thank you! I think the underlying concept is more or less the same, but there are a couple of notable differences. I really liked the other contributions on this thread (and I definitely incorporated many of the other ideas provided!) but I found the syntax proposed to be largely confusing, so that was one of my main changes. I think the associated lifetime syntax is more intuitive, but maybe I'm just getting inside my own head! Additionally, I really want any partial borrow implementation to be inferred, since I think that is a major source of ergonomics. |
I don't think the two "restraining" are the same:
So even though both actions "restrict" someone, they kinda have the opposite effects. Thus I would be against using the same syntax for both. |
Yeah I think you're right. If an associated lifetime is not be the proper way to express this, what about specifying the partial borrow as a part of the |
For future reference. One can solve many problems by deconstructing self. Here is an example. struct B;
struct C;
fn foo(b: &mut B, c: &mut C) {}
struct System {
bs: Vec<B>,
cs: Vec<C>
}
impl System {
fn apply_foo(&mut self, i: usize, j: usize) {
let System {bs, cs} = self;
foo(&mut bs[i], &mut cs[j])
}
} |
@nielsle Deconstructing doesn't allow any borrowing pattern that isn't possible otherwise. |
Consider the following struct and impl for 2D coordinates:
Even in this simple example, using
point.x
andpoint.y
gives us more flexibility than usingpoint.mut_x()
andpoint.mut_y()
. Compare following examples that double the components of a point:The lifetime elision rules make it pretty clear why this happens:
x_mut()
returns a mutable borrow that has to live at least as long as the mutable borrow ofself
(written asfn x_mut<'a>(&'a self) -> &'a f64
).In order to 'fix' the above example, there needs to be a way to say that a borrow only has to live as long as the mutable borrow of
self.x
. Here's a modification to the aboveimpl
block for a possible syntax to express partial borrows using awhere
clause:While the above examples aren't particularly encouraging, allowing for this type of change could be very powerful for abstracting away implementation details. Here's a slightly better use case that builds off of the same idea of representing a
Point
:For a more involved example of an API that requires mutable borrows, see this gist that describes a possible zero-cost OpenGL wrapper.
There has been some discussion about partial borrows in the past, but it hasn't really evolved into anything yet, and it seems like the idea hasn't been rejected either.
The text was updated successfully, but these errors were encountered: