Skip to content
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

Open
kylewlacy opened this issue Jul 18, 2015 · 54 comments
Open

Partial borrowing (for fun and profit) #1215

kylewlacy opened this issue Jul 18, 2015 · 54 comments
Labels
A-borrowck Borrow checker related proposals & ideas A-lifetimes Lifetime related proposals. A-syntax Syntax related proposals & ideas A-typesystem Type system related proposals & ideas T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@kylewlacy
Copy link

Consider the following struct and impl for 2D coordinates:

struct Point {
    x: f64,
    y: f64
}

impl Point {
    pub fn x_mut(&mut self) -> &mut f64 {
        &mut self.x
    }

    pub fn y_mut(&mut self) -> &mut f64 {
        &mut self.y
    }
}

fn main() {
    let mut point = Point { x: 1.0, y: 2.0 };
    // ...
}

Even in this simple example, using point.x and point.y gives us more flexibility than using point.mut_x() and point.mut_y(). Compare following examples that double the components of a point:

{
    // Legal:
    point.x *= 2.0;
    point.y *= 2.0;
}
{
    // Legal:
    let x_mut = &mut point.x;
    let y_mut = &mut point.y;
    *x_mut *= 2.0;
    *y_mut *= 2.0;
}
{
    // Legal:
    let ref mut x_ref = point.x;
    let ref mut y_ref = point.y;
    *x_ref *= 2.0;
    *y_ref *= 2.0;
}
{
    // Legal:
    *point.x_mut() *= 2.0;
    *point.y_mut() *= 2.0;
}
{
    // Illegal:
    let x_mut = point.x_mut();
    let y_mut = point.y_mut();
    //          ^~~~~
    // error: cannot borrow `point` as mutable more than once at a time
    *x_mut *= 2.0;
    *y_mut *= 2.0;
}

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 of self (written as fn 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 above impl block for a possible syntax to express partial borrows using a where clause:

impl Point {
    pub fn x_mut<'a>(&mut self)
        -> &'a f64
        where 'a: &mut self.x
    {
        &mut self.x
    }

    pub fn y_mut<'a>(&mut self)
        -> &'a f64
        where 'a: &mut self.y
    {
        &mut self.y
    }
}

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:

struct Vec2(f64, f64) // Represents a vector with 2 coordinates
impl Vec2 {
    // General vector operations
    // ...
}

// Represents a coordinate on a 2D plane.
// Note that this change to `Point` would require no change to the above
// example that uses `Point::x_mut` and `Point::y_mut` (the details of
// the structure are effectively hidden from the client; construction could
// be moved into `Point::new` as well)
struct Point(Vec2);
impl Point {
    pub fn x_mut<'a>(&mut self)
        -> &'a mut f64
        where 'a: &mut self.0.0
    {
        &mut self.0.0
    }

    pub fn y_mut<'a>(&mut self)
        -> &'a mut f64
        where 'a: &mut self.0.1
    {
        &mut self.0.1
    }

}

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.

@ftxqxd
Copy link
Contributor

ftxqxd commented Jul 18, 2015

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 Self, &Self, &mut Self, or Box<Self> parameter to be a method that can be called with the x.y() notation (a feature that has been proposed in the past as part of UFCS).

@kylewlacy
Copy link
Author

@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 &mut and returns a &mut).

@oli-obk
Copy link
Contributor

oli-obk commented Jul 24, 2015

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 Self::{x, y}.

Also this needs a lint that denies specifying all fields.

@glaebhoerl
Copy link
Contributor

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 fn x_y_mut(&mut self) -> (&mut f64, &mut f64).

@pczarn
Copy link

pczarn commented Aug 21, 2015

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 '(a, _) and '(_, b) of a struct are possible if no field has both 'a and 'b assigned in the struct declaration. Here, _ is a free lifetime variable.
Example:
(I think the syntax Self::x is incompatible with lowercase associated types.)

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)>) {}

@ticki
Copy link
Contributor

ticki commented Oct 18, 2015

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 ref does not enable annotation of lifetimes. Introducing lifetimes for ref breaks the consistency.

@glaebhoerl
Copy link
Contributor

@ticki What does your use case look like? Does the fn x_y_mut(&mut self) -> (&mut f64, &mut f64) pattern not work?

pnkfelix added a commit to pnkfelix/pgy that referenced this issue Oct 18, 2015
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
@aidanhs
Copy link
Member

aidanhs commented Nov 4, 2015

Just a vote for wanting this in some form.
It'd be nice to be able to get immutable references to the keys in a hashmap while also being able to get mutable references to the values later.

@eddyb
Copy link
Member

eddyb commented Mar 16, 2016

pub fn point_get_x(&Point { ref x, .. }: &Point) -> &f64 {

I've mentioned, e.g. pub fn point_get_x(p: &Point { x }) -> &f64 { &p.x } before, on reddit.

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, ..default syntax and variant types.

@nrc nrc added the T-lang Relevant to the language team, which will review and decide on the RFC. label Aug 25, 2016
@crumblingstatue
Copy link

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

@burdges
Copy link

burdges commented May 9, 2017

I think this interacts well with #1546.

Also, there is a cute color for shed door if you permit multiple self arguments, like :

trait Foo {
    position: usize,
    slice: &[u8],
    fn tweak(&mut self.position, &'a self.slice) -> &'a [u8];
}

It does not play so nicely with non-self arguments, but maybe it could be short hand for the green roof suggested by @oli-obk. It works with some sort of destructing function call syntax too :

fn foo((ref x,ref 'a mut y) : (usize,[u8])) -> &'a [u8] { .. }

@oberien
Copy link

oberien commented May 13, 2017

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.

@crumblingstatue
Copy link

crumblingstatue commented Sep 30, 2017

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(&region(mut data, length) self) -> &mut [T] {
        do_something_with(self.ptr, self.len)
    }
    // Only needs to access the `length` region.
    fn len(&region(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.

@burdges
Copy link

burdges commented Oct 1, 2017

That sounds like the disjointness rules in #1546

@Rantanen
Copy link

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 data/len of Vec<T>

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 Display would need to borrowck fields that are not used for display.

// 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 );

@wbogocki
Copy link

wbogocki commented Oct 4, 2018

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.

@Centril Centril added A-syntax Syntax related proposals & ideas A-typesystem Type system related proposals & ideas A-lifetimes Lifetime related proposals. A-borrowck Borrow checker related proposals & ideas labels Feb 12, 2019
@estebank
Copy link
Contributor

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
    }
}

@burdges
Copy link

burdges commented Dec 17, 2019

I doubt Point could/should destrtucture in type position like that, but you could destrtucture in argument position like:

pub fn x_swap_y<T>(
    Point { ref mut x, .. }: Point<T>, 
    Point { ref mut y, .. }: Point<T>
) {
    mem::swap(x,y);
}

impl Point<f64> {
    pub fn x_mut(Point { ref mut x, .. } = &mut self) -> &mut f64 { x }
    pub fn y_mut(Point { ref mut y, ..} = &mut self) -> &mut f64 { y }
}

In particular, these methods have no self argument because the calling convention destrtuctures it for them.

One should decide that a series of destructuring borrows works this way too though.

@bjorn3
Copy link
Member

bjorn3 commented Dec 17, 2019

Maybe fn a (&mut self { ref mut x }){}?

@mathstuf
Copy link

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.

@estebank
Copy link
Contributor

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.

It would have to be explicit for exactly that reason.

this allows better handling of cases like Vec and slices.

I would like to see the borrow checker eventually supporting let mut (x, y) = (&mut v[..2], &mut v[2..]); on its own, but it feels orthogonal to this feature.

@tuxzz
Copy link

tuxzz commented Jun 6, 2020

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.

It would have to be explicit for exactly that reason.

this allows better handling of cases like Vec and slices.

I would like to see the borrow checker eventually supporting let mut (x, y) = (&mut v[..2], &mut v[2..]); on its own, but it feels orthogonal to this feature.

I think they are different features because for slice it requires borrowing checker to tracking the value of const fn parameters, which is more 'dynamic' and difficult to implementation.
And unfortunately, seems like we have no const fn version of slice currently.
For partial borrowing of slice, we have slice::split_at, which is usable for most cases.

For partial borrowing of struct members, I think it's really useful because we usually want getters and setters.

@benkay86
Copy link

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.
        }
    }
}

@burdges
Copy link

burdges commented Sep 13, 2020

Anyone doing object oriented design could usually employ interior mutability like self: &RefCell<Self> because (a) they could afford the minuscule performance penalty and (b) they'd do some more careful non-object-oriented design if race conditions, etc. looked risky. Ignoring that argument..

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 Borrow abstract over immutable borrows using Rc and Arc, so maybe we should consider a BorrowMut-like trait that abstracts over RefCell and BorrowMut:

pub trait RefCellLike<T> {
    ...
}

impl<T> RefCellLike<T> for RefCell<T> { .. }
impl<T,B> RefCellLike<T> for B where B: Borrow<T>+BorrowMut<T> { .. }  

You'd then abstract over this within methods using self: impl RefCellLike<Self>.

@aspin
Copy link

aspin commented Sep 13, 2020

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 get_tile_mut, the computing the position on the screen they need to be at afterward. The problem is that WorldMap#tile_to_position can no longer be used since I've already mutably borrowed WorldMap when updating the tiles, even though self.width and self.height will be the only properties used in tile_to_position.

@burdges
Copy link

burdges commented Sep 14, 2020

You could reverse your call order there, so no need for tiles: RefCell<Vec<Tile>> or tiles: Vec<Cell<Tile>>. It's trickier if both methods require &mut self of course.

@djdisodo
Copy link

@TOETOE55
Copy link

TOETOE55 commented Sep 29, 2021

It seems that "row polymorphism" can play with this sense?
"row poly" is a type system allows to write programs that are polymorphic on record field.
https://github.com/purescript/documentation/blob/master/language/Types.md#Row-Polymorphism

@ijackson
Copy link

There are some crates that let you do something like this: partial_borrow partial_ref and borrow_as. For people wanting something like this, it might be a good idea to experiment with them and see whether these approach are worthwhile.

Full disclosure: I'm the author of partial_borrow.

@jonhoo
Copy link
Contributor

jonhoo commented Nov 12, 2021

@nikomatsakis's blog post on "view types" is definitely relevant here: https://smallcultfollowing.com/babysteps//blog/2021/11/05/view-types/

@LunarLambda
Copy link

LunarLambda commented Apr 20, 2022

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

@KevinThierauf
Copy link

KevinThierauf commented Oct 5, 2022

Hi all,
I'm not an expert on rust (so forgive me if I make any mistakes!) but as I've been using rust more and more, I've found this issue to be one of my biggest frustrations. As far as I understand, the biggest blocks with any proposed solution are as follows:

  • Any ability to partially borrow fields of a structure exposes information about that struct (violating the "black box" philosophy)
  • Some of the proposed syntax doesn't seem to be intuitive, and would be a break from standard syntax. Additionally, some of the syntax is a little verbose! (in my eyes).
  • Partial lifetimes should fit in with other rust constructs (such as traits)
  • No clear path for inferred partial lifetimes -- this could have major effects on code readability, especially when the lifetime should be intuitive
  • Partial borrowing could increase the volatility of what should be a stable interface (breaking changes may be needed as the internal structure of an API changes)
    These are the main issues I saw while skimming over this thread -- let me know if I've missed anything!

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:

struct Planet {
   // who knows!
}

impl Planet {
   // declare some associated lifetimes
   type 'creature;
   type 'building;

   pub fn addCreature(&'creature mut self, name: &str, loc: Location) { ... }
   pub fn addBuilding(&'building mut self, structure: StructureType, loc: Location) { ...}
   // .. so on

   // but 'creature and 'building lifetimes are completely normal lifetimes, so you can use them the in combination (or however!)
   // 'a is a lifetime that borrows both 'creature and 'building
   pub fn enterBuilding<'a: 'creature + 'building>(&'a mut self, creature: CreatureId, building: BuildingId) { ... }
   // 'a is a lifetime that borrows both 'b (for fireball) and 'creature
   pub fn explodeCreature<'b, 'a: 'b + 'creature>(&'a mut self, fireball: &'b mut Fireball) { ... }
}

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.

// lets define "Planet" as before, but with some fields
struct Planet {
   creatures: Vec<Creature>,
   buildings: Vec<Building>,
}

impl Planet {
   // we'll declare the same lifetimes as before
   type 'creature;
   type 'building;

   pub fn addCreature(&'creature mut self, name: &str, loc: Location) {
      // because "creatures" vec is accessed in a method borrowed for "creature" lifetime, "creatures" vec is now locked to "creature" lifetime. Any access of the "creatures" field is a borrow of 'creature
      self.creatures.push(Creature(name, loc));
   }

   pub fn addBuilding(&'building mut self, structure: StructureType, loc: Location) {
      // same as before. "buildings" vec is now locked to "building" lifetime; any borrow of "buildings" object borrows "building" lifetime
      self.buildings.push(Building(structure, loc));
   }

   pub fn badFunction(&'building mut self) {
      self.creatures.push(Creature::default()); // compiler error! self.creatures cannot be borrowed under "building" lifetime, since it was already borrowed under "creature" lifetimes
   }

   pub fn goodFunction<'a: 'creature + 'building>(&'a mut self) {
      self.creatures.push(Creature::default()); // valid -- 'creature lifetime is borrowed
      self.buildings.push(Building::default()); // valid -- 'building lifetime is (also) borrowed
   }
}

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).
If a user does not explicitly borrow any individual associated lifetime, they are borrowing 'self -- that is, every associated lifetime.

There is one partial (get it? hahaha) problem I can see with inferring lifetimes here. Consider the following:

struct Structure {
   a: u32,
   b: u32,
   c: u32,
}

impl Structure {
   type 'x;
   type 'z;
   type 'common;

   fn first<'a: 'common + 'z>(&'a mut self) {
      self.a + self.b; // lock "a" and "b" to either "common" or "z"
   }

   fn second<'b: 'common + 'x>(&'b mut self) {
      self.b + self.c; // lock "b" and "c" to either "common" or "x"
   }
}

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).
But on the other hand, this should only be a problem if a user exclusively combines associated lifetimes (and does not lock any field to a specific lifetime). And, ultimately, this doesn't seem like a largely practical thing to do (though again -- it is possible to deduce, as needed, pretty sure this is just introductory level linear algebra).
There is an additional edge case, where a user may want to design an API where two associated lifetimes are always required for a specific field. In this case, it may make sense to allow users to define an associated lifetime as follows:

impl Structure {
   type 'a;
   type 'b
   type 'both: 'a + 'b; // combination of both 'a and 'b
}

(Though this ^^ is a last minute thought).

I think this syntax solves most of the mentioned issues. For example (in the mentioned order):

  • Associated lifetimes have no direct relations to the fields; users of the API borrow specific lifetimes in the object, with no forward facing relationships between fields and lifetimes. APIs could even define a lifetime that isn't directly associated with any field as a way to "block" invalid API combinations at compile time (e.g. in the "OpenGL" example gist linked by @LunarLambda, users would be unable to create multiple binds to the same target at the same time. Instead, users would have to bind to a target, then drop the borrow before rebinding another buffer to the same target).
  • In my eyes, this is very easy to grasp, and ergonomic, too. It's easy to read and understand, adds no new syntax (or punctuation), with the new usage similar enough to existing lifetimes to be understandable.
  • Partial lifetimes would be, in most ways, completely normal lifetimes. This would enable them to become first-class citizens, as they would simply opt into existing syntax. Hopefully this would also make compiler implementation a low-complexity change. This would also enable them to be used in traits, or impl blocks, or wherever.
  • Partial lifetimes are inferred! It should be straightforward to use, easy to understand, and possible to write clear, concise, and understandable compiler errors.
  • Since partial borrows aren't explicitly tied to structure fields, a well designed API should not need breaking changes when there are internal changes. This is assuming a well designed API with clear boundaries between abstract components, but IMO this is the responsibility of an API designer. Additionally, the current workaround (using multiple objects that are related/passed around) faces the same issue, so this not a regression.

Phew. Hopefully I explained myself well (and I'm not missing anything!). In short:

  • Allow structures/traits to declare "associated lifetimes", which are a public part of the implementation
  • An associated lifetime divides the lifetime of 'self into several smaller lifetimes. 'self is the superset of all of it's associated lifetimes, and so borrowing 'self is also a borrow of all associated lifetimes
  • A structure without any associated lifetimes is unchanged
  • Associated lifetimes are inferred. Each structure field is locked into one associated lifetime. The associated lifetime for each field is inferred based on which lifetime a field is accessed under. If a field is accessed under 'b, the field must always be accessed under b. In this sense, the 'self lifetime could be defined as the combination of all associated lifetimes of a structure.
  • I'm running out of things to write!

I'd love any feedback/criticism/tips anyone can provide. Thanks!

@SkiFire13
Copy link

@KevinThierauf wouldn't this require deep changes to how lifetimes and references work? AFAIK the act of borrowing is intrinsic in references, that is &'something mut self will borrow Self no matter what 'something is, while 'something can only change how much the borrow can last. Your proposal feels like mixing the responsabilities.

@KevinThierauf
Copy link

KevinThierauf commented Oct 5, 2022

@KevinThierauf wouldn't this require deep changes to how lifetimes and references work? AFAIK the act of borrowing is intrinsic in references, that is &'something mut self will borrow Self no matter what 'something is, while 'something can only change how much the borrow can last. Your proposal feels like mixing the responsabilities.

Conceptually, I don't think so. Essentially, instead of borrowing self, the borrow checker would borrow self.creatures or self.buildings. Lifetimes themselves aren't becoming anything new, or anything different -- there's just more lifetimes -- lifetimes which are under the umbrella of (and associated with) self. Instead of borrowing all of self, you borrow one of it's lifetimes. Kind of a hierarchy of lifetimes, which I think fits neatly into the hierarchical nature of OOP code (with composition).
Implementation wise, though, this may be a whole different story. I'm not familiar with compiler internals, so someone more experienced than I will have to say!

@SkiFire13
Copy link

Instead of borrowing all of self, you borrow one of it's lifetimes

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 self. Unless you change how the default lifetimes work, by making fn foo<'a>(&'a mut self) not able to access any field, which seems counterintuitive to me.

@benkay86
Copy link

benkay86 commented Oct 5, 2022

@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.

@KevinThierauf
Copy link

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.

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.
So, if we have the associated lifetime defined as:
type 'creatures;
Then instead of borrowing all of self, we can borrow only the part of self under the umbrella of 'creatures.

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 self. Unless you change how the default lifetimes work, by making fn foo<'a>(&'a mut self) not able to access any field, which seems counterintuitive to me.

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 self.
In the case of fn foo<'a>(&'a mut self), this would normally be no different than fn foo(&mut self), which would be to borrow all of self, that is, all partial lifetimes (if they exist) which is the same as borrowing all of self normally. So foo would have full access to all fields, but callers would only be able to use foo so long as they are not partially borrowing self.

@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.

Thank you! I think the underlying concept is more or less the same, but there are a couple of notable differences.
I really liked @pczarn's "composite lifetimes" idea, but I found the syntax to be confusing (and something beginners especially would really trip up on). I also wanted to shift the lifetime parameters away from being a generic parameter to an associated type, since because I don't think they are as much a part of the type declaration as they are a part of the type interface. But the principle idea of creating a lifetime that applies to specific fields is pretty much the same!
I think the concept of associated lifetimes is very similar to @crumblingstatue's proposed "borrow regions", with the changes largely being syntax based. I really liked how the regions worked nicely with traits and how they abstracted away from any fields, and the ability to have clear and understandable compiler errors is a big plus. I think the syntax is much clearer, but it does still feel like the programmer is forced to explicitly define what could cleanly be inferred by the compiler. I do a lot of iterative prototyping when designing interfaces, and so I'd love for the compiler to infer as much as it reasonably can (without sacrificing readability)!
One notable advantage of the borrowing regions that the associated lifetimes concept does not provide is the ability for a function to partially borrow multiple regions with different mutability in the same function. I'm not sure how significant of a issue this is, but I do feel like it's worth (at the very least!) making a note of.

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.

@SkiFire13
Copy link

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 self.

I don't think the two "restraining" are the same:

  • bounding the length of the lifetime to a sufficiently long one is something that restricts the caller, and so gives more power to the method body because it can do more assumptions on the lifetime of self. In other words, the caller can only do the same or less, while the method body can only do the same or more.

  • bounding to "partial borrows lifetimes" instead is something that restricts the method body, and so gives more power to the caller because it can call the method even if it doesn't have full access to self. In other words, the caller can only do the same or more, while the method body can only do the same or less.

So even though both actions "restrict" someone, they kinda have the opposite effects. Thus I would be against using the same syntax for both.

@KevinThierauf
Copy link

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 self parameter directly?
I.e. instead of:
fn foo(&'creatures self) { ... }
We might express as something more like:
fn foo(&Self::Creatures)
This might be a more correct way of expressing the concept, since the idea behind partial borrowing is to borrow only a specific part of the structure, instead of the structure as a whole. In this way, creatures is more of a partial type than it is a partial lifetime, so I think this makes sense.
(The syntax here is more of an example than anything concrete, I'm open to alternate proposals).
Is this more of what you had in mind?

@nielsle
Copy link

nielsle commented Dec 13, 2022

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])
    }
}

@crumblingstatue
Copy link

crumblingstatue commented Dec 13, 2022

@nielsle Deconstructing doesn't allow any borrowing pattern that isn't possible otherwise.
foo(&mut self.bs[i], &mut self.cs[j]) works just as well.
The problem is that borrows can't be made partial across function boundaries, not that they can't be made partial within a single function.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-borrowck Borrow checker related proposals & ideas A-lifetimes Lifetime related proposals. A-syntax Syntax related proposals & ideas A-typesystem Type system related proposals & ideas T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

No branches or pull requests