A possibly more erognomic syntax for borrow

Firstly, I’m not wanting to change the actual approach for these Nor am I asking to remove the existing way

&mut is confusing to those of us who come from somewhere other than c++. I’m wondering if there might be a way to alias this functionality using clearer verbiage?

I’m all for keeping the existing way for users coming from c++ and possibly for special purpose functionality, but for newer users I’m looking for something with better affordances?

Normally I wouldn’t have said anything and just accepted the old way of doing things as an artifact carried up from assembly given it’s 3 letters preceded by an &, but I think one of the goals for RUST is to make an effort to be more ergonomic when a change in syntax improves readability and doesn’t negatively impact compiling and optimization

as an example of my confusion can I get help replicating these examples with existing syntax?

fn main (){
    let mut book1 = 1;
    let mut book2 = 10;
    let mut book3 = 100;
    let mut book4 = 1000;
    let mut book5 = 10000;

	let a = peek book1;    // look but don't touch
    let b = borrow book2;  // change, but only you have it, give it back when done
    let c = take book3;    // change, only you have it, keep it forever and burn it when done
    let d = ~share book4;  // dangerous share, d and book4 point to the same object, both can change, both are affected by changes the other makes (the ~ is there as a warning and could be any symbol we decide makes sense)
    let e = copy book5;   // e gets it's own unique copy of book5, both remain mutable

    example_peeker(show book1);    // show / peek pair,     makes it clear to both sides what's expected without needing to see the other side
	example_borrower(loan book2);  // loan / borrow pair,   makes it clear to both sides what's expected without needing to see the other side
	example_taker(give book3);     // give / take pair,     makes it clear to both sides what's expected without needing to see the other side
	example_sharer(~share book4);  // ~share / ~share pair, makes it clear to both sides what's expected without needing to see the other side
    example_copier(copy book5);    // give copier it's own duplicate of book5
}

fn example_peeker(peek book1){}      // look but don't touch
fn example_borrower(borrow book2){}  // change, but only you have it, give it back when done
fn example_taker(take book3){}       // change, only you have it, keep it forever and burn it when done
fn example_sharer(~share book4){}    // dangerous share
fn example_copier(copy book5){}      // indicate you want your own copy of book5

And if any of these don’t currently exist just comment them out and note it in your reply

NOTE: orginally posted here: https://users.rust-lang.org/t/a-possibly-more-erognomic-syntax-for-borrow/18552

I started this post in a different section because I didn’t know this was here (didn’t show up under the site that came up when searching google for ‘rust language forums’).

If someone can lock the old thread and indicate it’s moved here I’d appreciate it

You are looking at this?

fn main (){
    let book1 = 1;
    let mut book2 = 10;
    let book3 = 100;
    let mut book4 = 1000;
    let book5 = 10000;
    let book6 = Some(100000);

    let a = &book1;
    { let b = &mut book2; }
    let c = book3; enum Moved{}; #[allow(unused_variable)] let book3:Moved;
    let d = &mut book4 as *mut _;
    let e = book5;
    let mut f = book6;

    example_peeker(&book1);
    example_borrower(&mut book2);
    example_taker(c); //book3 is moved you have to use c instead
    unsafe { example_sharer(&mut book4 as *mut _); }
    example_recycler(&mut book6);
}

fn example_peeker(_:&i32){}
fn example_borrower(_:&mut i32){}
fn example_taker(_:i32){}
unsafe fn example_sharer(_:*mut i32){}
//example_copier is not possible
fn example_recycler(book: &mut Option<i32>) {
    *book = None;
}

Playground link

You can already basically do this.

type Ref<'a, T> = &'a T;
type RefMut<'a, T> = &'a mut T;

trait BorrowExt {
    fn borrow(&self) -> Ref<Self>;
    fn borrow_mut(&mut self) -> RefMut<Self>;
}

impl<T> BorrowExt for T {
    #[inline] fn borrow(self: Ref<Self>) -> Ref<Self> { self }
    #[inline] fn borrow(self: RefMut<Self>) -> RefMut<Self> { self }
}

// ...

use borrow::{ Ref, RefMut, BorrowExt };

let foo: Ref<i32> = 0.borrow();

and use it in your code, never once writing a & (bitmasking and booleans don’t exist, duh /s). It’s even kosher as a self type, because fn foo(self: &Self) is what fn foo(&self) desugars to. (Cool huh?) Of course… this will earn you weird looks, so I recommend against it.

Mind, Rust used to be far more sigiltastic: not only do you have &T and *T for borrowed and raw pointers, but you also have ~T for owning pointers (what we call Box<T>, Vec<T> (for ~[T]) and String for (for ~str) today, and @T (back when Rust had, horror of horrors, a garbage collector) for a GC’d pointer.

I think that sigils are usually a bad idea (see Perl or Ruby, which are very sigiltastic in different but equally unreadable ways), but borrowing is so utterly fundamental to any nontrivial Rust program that it deserves a sigil, just like slices (and arrays) have their brackets, lifetimes their ticks (also- saying “tick-a” out loud is fun), and macros their bangs. None of these are necessary, but I’d much rather read [T; n] over Array<T, n> (cough std::array<T, n>) and &'a T over Ref<'a, T>. They’re not just short… they’re to the point.

The ampersand being alien to non-C folks also has the nice advantage that it doesn’t have a cemented meaning to most people, so we can get away with calling it “borrow” instead of “address-of”, while still looking like and doing approximately what it does in C (except, you know, the bit about our friend tick-a).

Also reading &'a T as “and tick-a T” is fun. =p

3 Likes

isn't this a copy not a take?

It is. But you can use the following trick:

let c=book3; #[allow(unused_variable)] let book3:!;

to pretent this is a move.

It is a copy, but only because usize is a Copy type. If it’s a type that does not implement Copy, then it would carry the same meaning as your take.

Interesting, I figured since we’re just renaming / rephrasing existing functionality it wouldn’t be too hard, but that seems like it might be crateable.

Has this question not come up before?

Small nit, but this is technically UB since it amounts to transmuting an immutable borrow into a mutable borrow- getting an &mut is just one otherwise innocent-looking unsafe away!

Rust has move semantics by default; imagine that all temporaries on the stack are wrapped in an std::unique_ptr with a trivial destructor. (Unless that type is Copy... in which case a move and a copy are the same. You're still gonna memcopy either way).

1 Like

Small nit, but this is technically UB since it amounts to transmuting an immutable borrow into a mutable borrow- getting an &mut is just one otherwise innocent-looking unsafe away!

Fixed. Simply making book4 mutable.

as you were replying I realized I didn’t include an explicit copy in my op and added it

Technically, there is no “explicit copy” available in ANY language, including Rust. In Rust a “move” is just "copy, and make the old location invalid by either drop (call the destructor) or ignore".

I guess I consider the default to be an explicit copy, for example in powershell I can do this.

$i=5
$a=i
$a++
$i--
"$i $a"
4 6

The main thing I wanted to do is put in a place to show where and how copy and move are different (it’s out of the scope of this post but in theory wouldn’t implementing a move as a mutable borrow, then drop the old reference and take full ownership be more efficient than a copy and drop for any object larger than 3 pointers in size?)

In the memory model, both move and copy uses memcpy or something like that. It does not care what the content it is. The only difference is that move require to forget the old location in your brain model, that is all.

Sure, but I thought all higher level objects were handled with pointers, so you could be copying a pointer, then dropping the old pointer without actually copying all the data?

An example where you would use this is if you wanted to hand off a vector of vectors to function that would translate it to a csv and write it to a file, but you don’t care about the object after you hand it off. You could pass the object to a thread and then just let it go, without having to check and wait for it to finish so you can empty out the object, and if you had say 500,000 objects you don’t have to duplicate all the data to perform a handoff.

But it sounds like this is something that doesn’t currently exist in RUST so I’ll drop it for now as out of the scope of this post.

Yes, this is what I was talking about in my another thread.

But for concret types (no dynamic diapatch), you can see my new example_recycler.

Interesting, thanks!

I’m gonna clear up a few misconceptions in this thread about how ownership and borrowing work.

// brand new type with no implemented traits
// note that Rust does *not* define copy constructors automatically
struct Foo;

let x = Foo;
let y = x; // move the contents of x into y. at worst this is a memcpy, at best
           // best case, copy elision kicks in. x is now unreadable (and, since
           // it's not `mut`, not writeable, either)
           // note: this does *not* call any destructors. ownership has changed
           // and `x` (now `y`) is not dropped until the end of this scope
let z = x; // error! x has been moved over yonder ^

// define a brand new type with the trivial
// implementation of Rust's equivalent of a copy constructor
#[derive(Clone)]
struct Bar;

let x = Bar;
let y = x.clone(); // explicitly copy the contents of `x` into `y`. mind,
                   // a `clone` might be non-trivial, in the way that copying an
                   // `std::vector` in C++ is more than a memcpy
let z = x; // now `x` has moved, leaving `y` intact...
let a = x; // ...so now this is an error!
let b = y; // but this isn't...
let c = y; // ...but now this is!

// define a brand new type, but make it trivially copyable
#[derive(Clone, Copy)]
struct Baz;

let x = Baz;
let y = x; // `Baz` is `Copy`, so this is not a move, but a semantic copy
           // (though the compiler might still apply copy elision)
let z = x; // which is why this isn't an error- `x` hasn't moved.

// you can totally allocate huge things on the stack- a lot of Rust 
// code is aggresive about not allocating behind the scenes,
// and applies RVO when it can to stop you from making huge 
// copies across call boundaries, just like C++ does
let big_ol_array = [0u64; 1024]; // 64 * 1024 = 65K, as much memory
                                 // as your NES can address!

To get a better picture of how Rust’s move semantics work beyond my sleep-deprived rambling, I suggest checking out the first few chapters of The Book.

6 Likes

Specifically how is it confusing? It's probably one of the earliest things you will learn from any introductory Rust guide. If you don't currently know what it precisely does, then you'll need to learn about it anyway; no amount of syntax will help that. You can't make even a keyword-based syntax descriptive enough to convey all the exact semantics, unless, highly unrealistically, the keyword is an 1500-word essay explaining all the details of borrowing.

This also applies to the "I'm not coming from C++" argument. If you are coming from a non-systems language and you are willing to learn a systems language (which is great), then you'll be exposed to and need to learn more new concepts anyway.

Another reason I oppose this is that, perhaps counter-intuitively, it also creates confusion, because it makes two completely different syntactic forms do the same thing. We already have enough of this phenomenon in the language; we shouldn't add more if it. Things that look different should behave differently, and things that look similar should behave similarly.

6 Likes

Much thanks, that mostly makes sense though I don’t quite get how #[derive(Clone)] enables clone for x…

oh, unless this is implied? let x = Bar::new();

:thinking: