177 comments

[ 3.1 ms ] story [ 254 ms ] thread
A little mental model I used to explain ownership and borrowing to someone who is new to Rust. I hope others find it helpful!
It's helpful.

It's especially helpful to people who are both new to Rust AND come at Rust from language ecosystems with a garbage collector.

People are new to Rust but have come to it from a C/C++ have had to worry about ownership all the time, so they might not find anything here a revelation except this: "Oh wow, the Rust compiler takes care of all of this for me! No inadvertent memory leaks?! Yaay!"

Just want to say that I appreciated this comment when I read it earlier in the day and when reading through the thread again just now.

> It's especially helpful to people who are both new to Rust AND come at Rust from language ecosystems with a garbage collector.

This is exactly the audience I was writing for so I'm very happy that you thought so.

Perhaps add a part on the syntax, i.e: passing a reference to a function, then passing that along to some other function, declaring that function as accepting a mutable reference to a vector, versus having a mutable parameter that is an (unmutable) reference to that vector, versus declaring that parameter is a reference to a that mutable reference; same for closures, what does a &&parameter even means, when to use & in the declaration and when to *deref at the point you use it, iterating by default "for x in Vec" versus iterating &Vec, versus using .iter() and .into_iter() etc. etc.

Those subtle distinctions were the overwhelming force leading me to ampersand driven development in the early days, i.e I had the "pointer" mental model and tried to shoehorn that into Rust somehow and get that pointer to work like it does in C.

I don't think I saw a simple write-up with clear examples of what to do and what not aimed for beginners, seasoned rusters just act like slick professionals that wouldn't make such nonsensical silly mistakes and beginners are to somehow emulate their genius.

Just one small content addition:

Both `&` and `&mut` can be moved to another thread (through only `&` can be used by multiple threads). This holds even if the livetime is not `'static`, but requires the usage of a scoped threads API which use a trick to assure that the sub-thread(s) can not outlive the parent thread from which the `&`/`&mut` comes.

Originally such an API was in rust/std but it was removed as it was found to not be sound in the way it was implemented, but sound versions of this API do exist(1) since a long time as libraries and there have been efforts to move it back into std (but I don't know the state of it).

(1): For example crossbeam::scope : https://docs.rs/crossbeam/latest/crossbeam/fn.scope.html

std::thread::scope was reintroduced in 1.63
And all it really does is force a thread.join when "scope" goes out of scope. Maybe just me but seems only marginally useful.
That's exactly the same as the crossbeam implementation. It's quite useful especially for relatively simple parallelism tasks.
In Rust if you have a &Thing right here, you can't give that to some new thread unless you can promise the thread has a shorter lifespan than the &Thing does - otherwise the thread could access it after it ceases to exist, and you've introduced Undefined Behaviour.

Without scoped threads this means all references you want to give to a thread must be allowed to live forever, the lifetime 'static, since your threads might live forever (the mechanism to kill them could be discarded unused somehow). Since scoped threads always cease to exist when they go out of scope, we can give them references that live any time longer than that scope.

The important part is it safely allows you to pass references or other objects with lifetimes to those threads. (Or at least lets the compiler know so you can avoid having to write unsafe things yourself.)

This happened to be useful for something I was doing at work where the wrapper for a native C library expressed the required lifetimes of the library constructs using lifetimes. (Eg. You must set up Foo before Bar, and Foo must outlive Bar. It was OK to pass Bar and use it from multiple threads, but it had a lifetime attached. Using scoped threads we can satisfy the compiler's lifetime checker, since it knows even though we gave Bar to many threads, none of them outlive Foo.)

Doing this using a destructor, or worse in straight-line code, turns out to be unsound. So it's more than marginally useful for those familiar with Chesterton's fence.
I didn't say it was useless, only that its use is highly limited to a very specific case, namely "do a bunch of things at the same time and block until completion".
If the scope is the lifetime of the program, then it hardly makes a difference. Note that rust programs exit upon return from main anyways.
This is the point of lifetimes. Everything has a lifetime whether or not it is annotated. Sometimes the compiler doesn't know what the lifetime of an object is, particularly in relation to other objects.

If you can scope something to main() then that is a 'static lifetime and there is no need for a scoped thread for it.

It's a building block allowing more threading abstractions to be build on top of it without using unsafe code.

Most importantly it works with references like `&`,`&mut` or `Cow` etc.

E.g. in the example below it use used to ad-hoc implemet a typical simple parallel fold pattern where a collection of data points is chunked and split across num_cpu threads where each chunk is processed in parallel followed by combining the results.

https://play.rust-lang.org/?version=stable&mode=debug&editio...

Now in practice for many use cases you want to use more convenient/optimized libraries like rayon for this kinda of things instead of ad-hoc implementing them by hand.

But having the necessary primitives to implement it ad-hoc yourself without unsafe code is grate anyway.

Good article but whats the reason for all the syntetic meaningless images?
Honestly, I thought they'd help illustrate the ideas. But maybe not! Thanks for the feedback :)
There are too many for the amount of text. So you have two paragraphs, and then an image the size of five paragraphs. Then two more paragraphs, and another 5-paragraph image.

They are also somewhat inconsistent as to where they appear.

You can remove all but one or two, and the article won't be any worse :)

Done :)
I'm not sure what you think you've fixed, but the images are still way too large from my perspective. Some of your images don't even fit on a single screen at once.
I for one liked the illustrations, just think they were a bit large compared to the text. Maybe fewer would be good, but I liked the Rust-crab theme.

The balloon one is a bit busy and I didn't find it to have much explanatory value in that context.

I removed the last two. Thanks for the feedback :)

Update: also made them smaller.

This is an interesting phenomenon, how some people focus on the design of the page rather than the content. We see it quite regularly on HN related to blog post such as this. One wonders if the sibling commentors here have ever heard of "bike shedding"?

I personally appreciate the content, so thanks for that!

https://en.wikipedia.org/wiki/Law_of_triviality

That's probably a good indication that the design of the page was distracting.
If the design of the page is so bad that it makes the content hard to parse, it seems worth mentioning.

Yes, bike shedding. People love to bring that up as a way to deflect criticism they disagree with.

Disagree is too strong, more like disregard and find generally trivial and distracting.
Perhaps the set of complainers is largely contained within the set of ad-blocker users.
IMO the images should be blue, and the number of them should be divisible by three. One of them should be of an otter.

Edit: Aside from that, as a Rust neophyte I did like the content.

I am sorry, but they made me quit the article without reading the content. However, I really liked all illustrations :)
I'm curious, can you explain how an image of some kind of crab plushy toy that takes up THE ENTIRE SCREEN ON A DESKTOP helps illustrate anything about immutable references? It is fully 2x larger than the previous text and I have to scroll multiple times to get to the remainder of the article.

Is it that you used a child sharing their toy as a way to explain the concept, and you needed to remind us what a toy was?

Are these images generated by Dall-E or something similar?
Midjourney
Would you mind if I suggest Taft Test [0] instead? The pictures take about the same amount of space that the text does, and they serve as... moodsetters or whatever it's called, so I think replacing them with pictures of the America's greatest (by volume) president would work just as fine.

[0] https://tafttest.com/

I don't understand, why is "&" not the default and passing ownership something you must explicitly request ?

It seems the latter is more common than the former, and that would avoid to have to add "&" everywhere.

Ooh that's a fun question. I think it comes from reusing the C/C++ notation for references (https://stackoverflow.com/a/2094715/2242866). But I have no idea where the original idea for using those characters came from.

There's one other thing in Rust that would be kind of difficult if passing a reference were the default, rather than the owned value. Rust has implicit returns so the last thing at the end of a block or function is returned. If you needed to explicitly ask for ownership, you'd need to do that at the end of every function. Also, Rust will yell at you if you try to have a function return a reference to a value that's going to be dropped.

> I think it comes from reusing the C/C++ notation for references (https://stackoverflow.com/a/2094715/2242866). But I have no idea where the original idea for using those characters came from.

In C++, I think that the use of '&' came from the use of '&' in C as meaning "the address of". A reference and an address to a value are similar concepts.

I think it's, um, borrowed from C++'s pass by value / reference syntax. To be honest I'm still a bit muddled about how &x in Rust relates to &x in C++.
& is about the same as const & in C++, just swapping the default for immutability.
One reason is that "&" is nestable, but ownership is just ownership.

Currently the types are:

    owned T            T
    ref of T          &T
    ref of ref of T  &&T
You're suggesting that instead you skip the "&" and write some sigil for owned types, say "@":

    owned T           @T
    ref of T           T
    ref of ref of T   ???
How do you write the type "ref of ref of T"?

On its own this isn't a super compelling argument, because "&&T" is an unusual type that one doesn't normally use. But it hints at the fact that "&" is a modifier. The stronger but less pithy version of this argument is that if you have a type parameter "T" it's essential that it can be instantiated as "T=&U" for some "U".

EDIT: come to think of it, you don't often write "&&T", but it is commonly used. If you `println!("{}", x)` where "x" is a reference, the "println!" macro also takes a reference and you get a "&&" type.

> come to think of it, you don't often write "&&T", but it is commonly used. If you `println!("{}", x)` where "x" is a reference, the "println!" macro also takes a reference and you get a "&&" type.

Yup, i see them all the time in iterators too. Eg `foos.iter().filter(|t| ..` `t` is `&&Foo` in that example, iirc. Not that you usually see it though, due to closures obscuring types.

You would use an explict "&", just that &T is implicit by default.

I have to assume rust creators knew what they were doing since they are obviously way more experienced at this than me, but I still don't get it.

I suspect you're not imagining all the places this would touch the language. For example:

- Does "i32" mean an integer or a reference to an integer? You almost always want it to be an integer, so maybe we carve out an exception for small primitive types, that they don't get an implicit "&".

- Likewise some library-defined types like "Rc<T>". "Rc<T>" is the way you store a ref-counted T. So you usually want "Rc<T>" instead of "&Rc<T>". Is there an exception for this type, too, or do you always have to write "@Rc<T>"? It's defined in the standard library, so if it gets an exception than there has to be a language mechanism for "types that should not have an implicit &".

- The ".next()" method on iterators has the type signature "next(&mut self) -> Option<T>", where "T" is the type of the iterator's elements. Under your proposal, you'd write "next(&mut self) -> Option<@T>". If "T=&str", then you're returning an "Option<@&str>". Which has to be equal to "Option<&str>" or else everything breaks, but that might not be super intuitive to programmers.

That last one's the most important, though I don't know how to explain it clearly. If you have a type parameter "T", then "T" does not mean owned T. It means "T", whatever "T" is. If "T" is a reference, then it's a reference. And if you have a type parameter "T" then "&T" means a reference to "T" whatever "T" is. If "T" is itself a reference, then "&T" is a reference to a reference.

Gosh, thanks for this explanation. This will help as my eyes protest the assault of '&'s.
It's simpler to buy any true value option than pick from all the rental properties in the rust belt.
Oh, so it's for performance reason: the default choice is not the easiest, the the cheapest, and they force you to be explicit about paying the price of a reference?
Because Rust is pass by value.

  f(&s);
is practically equal to

  let r = s.borrow();
  f(r);
If I ask why is the sky blue, telling me because blue is the color or the sky doesn't help.
Rust 0.x used to have this the other way around with an explicit `move` keyword.

It was tedious and eventually dropped. For example, it's more natural to move in assignment. `= move` is weirdly boilerplatey, `=` borrowing is unhelpful, and `=` moving without the move keyword is inconsistent.

Also the moment something is borrowed starts a lifetime scope, which is useful to know. It'd be harder to notice loan scopes of loans were implicit.

Another step in getting proficient with ampersand (or rather immutable refs), is pulling stuff from enums and structs, in an immutable way. I.e. Option<SomeStruct>, or Result<SomeStruct, ErrorKind>. Using as_ref and friends.

There was a great post the other day about the "Register of rust", and getting to know all the different variants of these features to be proficient and being able to build whatever you want.

The same goes for async features as well. Async especially requires that you understand lambdas, borrow checking and so on. As it can be quite tricky to send a value to a tokio::task::spawn!(...) if you don't have some mastery over the borrow checker.

When you get to taking lambda in a func, that should be async then you begin to get into the weeds of rust, and where some of the nasty compilers errors show up, some of the unhelpful variants, where you are expected to know the intricacies of how lifetimes work, and how rust handles generics vs impl vs enums etc.

In the beginning I just recommend using a good old Arc<Mutex<...>> if you need to pass stuff to functions, you can still shoot yourself in the foot, but you can take the optimizations piecemeal.

I’m not sure I like the word ‘register’ in that if you are singing a song you might choose to switch registers from time to time to hit all the notes whereas a song is going to have certain music features consistent throughout the song.

In programming you might use different ‘registers’ in different parts of a program, but for all of the lower teachings about ‘encapsulation’, ‘information hiding’, and such programs as a whole have to have certain global invariants respected if they are going to work correctly…. Particularly when it comes to memory management!

Garbage collection is a great facilitator for encapsulation because it means you can just allocate and not think at all about deallocation. A strength of C is that each program can implement a custom optimal memory handling strategy but that’s a roadblock to reuse with libraries because any library has to adapt to the memory allocation strategy of the program as a whole.

Superficially it looks as if objects, inheritance, and polymorphism are the key features for code reuse in Java but I’d make the case that the garbage collector is more fundamental - you certainly lose some performance but neither C nor Rust has managed the feat that LISP and Java has of encapsulating memory management in a way that allows arbitrary code to work in the same runtime together.

(1) I read that article, (2) I think the sociolinguistic term is derivative of the musical term or the other way around and it is the same in that you can change registers in the same document.
I know this is a personal issue, but I find the use of "registers" in that article to be very confusing, because to me, a "register" is a slot in the CPU that holds a value to be operated on, or is the result of an operation.

That's so deeply ingrained into my mind that I really struggle to interpret it differently.

How about using Register Renaming to have a different set for different domains. Business registers, scientific registers, lolcat, etc? :)
This is a great post demonstrating just how bad the ergonomics of Rust are (I know I know, that's not the point of Rust). But for people who care about language usability you can point to this post and say "they were not thinking about the user journey during the design process, how would you have designed it differently?"

Take the first two sections as a perfect example. You decide you want to introduce a single-character control symbol `&` (which is questionable by itself), how do you apply it? In one case you decide that this control symbol indicates that a value is immutable and in another it's part of the declaration of a value as explicitly mutable. You've now introduced a semantic contradiction that a learner has to overcome.

Now obviously Rust works, and that's its point, and people love that enough to overcome all the ergonomic challenges the language puts up. What I can't wait for is Pythonic Rust where we have a language that treats humans as first class citiziens but offers the guarantees that Rust does. The only question in my mind for if this will ever happen is if the fully literate programming modality enabled by LLMs will make it a moot point.

The essential complexity of borrowing in Rust is more fundamental than the accidental complexity of the syntax that’s required to express borrowing. You could make something more verbose and a little more self-documenting and people will still struggle to write programs for which borrowing isn’t a good fit.
Came to say the same thing. Rust requires explicit annotation of usage semantics. This information somehow has to get conveyed to the compiler and runtime. So the 'ergonomic' issues are fundmantal.

I think from a PLT perspective, the interesting question is 'can the problems Rust is supposed to solve be resolved in a simpler (but radically different) conceptual model'? We started off originally with a spatial conceptual model -- stack, heap, scope -- that mapped lifecycle semantics to spaces and transitions between spaces. Concurrent programming stressed out that model and we're here with explicit mapping of semantics by the programmer. I've been wondering lately whether a new spatial approach can yet again simplify matters.

> I've been wondering lately whether a new spatial approach can yet again simplify matters.

I've been closely following the development of Vale since I first saw it here. Though their approach is slightly higher-level than Rust and requires (some) runtime safety checks (though to be fair, so does GC).

https://vale.dev/

https://verdagon.dev/blog/zero-cost-memory-safety-regions-ov...

I think it would be tough to change the spatial model in a language as low-level as Rust, because that spatial model is just reflecting how your CPU actually works under the hood. If you try to hide that away, the programmer is going to end up losing some control.

(comment deleted)
For most applications, garbage collection.

For a while I was thinking of the question of "Why didn't production rules (a.k.a. expert systems, business rules engines) catch on as a general approach to programming?" Systems like that can do some remarkable things, similar to Excel and logic programming in that you can write programs that aren't concerned with the exact sequence of events that things have to be done in. This is not only convenient for the non-professional programmer, it makes some "chicken-and-egg" problems in conventional programming languages trivial, back in the 1980s people would have seen this to answer to parallel programming, today we might see it as an answer to the async choreography hell of front end programming.

The thing is that sometimes the order of events matters and when you look at rules engines they never settled on a standardized way to control execution. There are numerous things that work, an advanced rules engine implements several, but none of them are 100% satisfactory for everyone.

One answer to these problems I see is "rules and schemes" where there are two programs that exist side-by-side, one that defines what is to be done and the other that fills in the details of how exactly it is done.

I can see something like that for the general programming language case. (Write a "style sheet" that explains how memory management is done) Also sometimes I fantasize that the way to achieve what Rust is trying to achieve is some combination of

   (1) A really good macro assembler
   (2) A theorem prover
   (3) An ergonomic parser generator that makes writing rich DSLs easy and completely mainstream.
(1) is a good project (e.g. gas is by and for people who hate assembly language), (3) is a good project (I can rant about how bad parser generators hold developers back), the (1)-(2) punch is appealing with the the caveat that portability and optimization are problems in that case, but those could be addressed by developing architecture-specialized macro packs and

   (4) A general-purpose superoptimizer
which hooks into (2) which is itself a project the world needs.
> I've been wondering lately whether a new spatial approach can yet again simplify matters.

This is indeed the case! Rust's borrow checking is the first step towards "regions", a more general and more flexible model. TL;DR: We convey to the compiler that a certain set of objects are conceptually in the same region, and then we can turn immutable the entire region at once.

Pony and Forty2 [0] first came up with them, and Vale is building them into the language as first-class citizens and blending them with borrow-checker like semantics [1] to eliminate memory safety overhead.

It helps us do patterns that more traditional borrow checking has trouble with: observers, dependency references, delegates, callbacks, some forms of RAII, and intrusive data structures. This is mostly because regions get a lot of the borrow checker's benefits while still allowing mutable aliasing.

[0] http://forty2.is/

[1] https://verdagon.dev/blog/zero-cost-memory-safety-regions-pa...

Very cool. I will take a look. This * was a (just) sketch I did years ago starting from a CSP base (Go). Explicit spaces (or "regions") and defined transition semantics (which compiler and runtime can enforce and guarantee). Single ownership spaces also make distributed GC so much simpler.

*https://github.com/alphazero/csp-cso-chipmunk-

I'm not sure the complexity of the borrowing rules are always essential. Sometimes it is, but often it adds an accidental layer of complexity that doesn't exist in the real world.

For example, I once saw someone bring in an entire message-routing framework (Yew) because they couldn't get a simple observer to work within the borrow checker.

This is why I'm glad that Rust added Rc/RefCell; its design acknowledges that sometimes, the borrow checker gets in the way more than it helps, and it adds a more flexible alternative so we don't shoot ourselves in the foot by using the borrow checker in places where it doesn't make as much sense.

> This is a great post demonstrating just how bad the ergonomics of Rust are (I know I know, that's not the point of Rust).

A chainsaw is meant to saw wood. But it should still have a good usability.

Err, sorry. I meant to say hammer.

> But for people who care about language usability you can point to this post and say "they were not thinking about the user journey during the design process, how would you have designed it differently?"

People have been discussing how to make Rust usable on the rust-lang issue trackers and RFC repositories all the time that I’ve observed from the peanut gallery. Before the stable 1.0 release as well.

It’s not for a lack of trying.

That said, I'm looking forward to seeing what the first generation of languages influenced by Rust will look like. A first iteration on an idea is always going to make mistakes that a second iteration can do away with.
I'm particularly excited for this too. We're starting to see a lot on the horizon:

* Austral is using linear types for zero-cost memory safety [0] and then allows for borrowing on top of that. Val does something similar called mutable value semantics, which is pretty interesting [3].

* Vale brings borrow checking up to the regions level [1] which gives borrow checking benefits while still allowing mutable aliasing, which makes it much more flexible and ergonomic.

* Cone uses borrow checking on top of any user-specified allocation strategy, such as single ownership, RC, GC, etc. [2]

[0] https://borretti.me/article/introducing-austral

[1] https://verdagon.dev/blog/zero-cost-memory-safety-regions-pa...

[2] https://cone.jondgoodwin.com/

[3] https://github.com/val-lang/val

Note for others: This person (based on nick and domain name) is the “lead for the Vale Programming Language” but didn’t feel like disclosing that fact in this comment.
And anyone that has learned (or just plain gotten used to) Rust in the last 3 years would be in for a shock if they tried to use Rust 1.0. Things have gotten better since. And I'd bet that it will continue to do so, at least for a while.
Pythonic Rust you’re speaking of sounds an awful lot like Swift.
Swift uses ARC everywhere from what I recall. Sure it's ergonomic. But why not just go for full GC then?
RC is a GC algorithm, so it is already there.
No it is not. The whole point of GC is to collect cycles and not think about ownership, while Swift and Rust require you to think about ownership.

In Rust you do so via borrow checker or explicit Rc/Weak wrappers. In Swift you do that via additional weak annotations (strong being the implicit default).

Grandparent is correct. Reference counting is a garbage collection mechanism. In same way a bubble sort is a sort.

It doesn't care about ownership either. Apart from some Swift specific optimizations iiuc.

Full GC I mentioned is more of mark and sweep variety.

Alright, malloc/free is also garbage collection then.
Ref counting is considered GC (despite the colloquial definition excluding it) because RC handles the same problem domain as full GC, even with drawbacks around cycles. malloc/free, on the other hand, are just ways to allocate and release memory. They're too low level to be garbage collection.

See this excellent comment: https://news.ycombinator.com/item?id=32277088

Not true. Swift uses value semantics for structs and enums, arc for classes. The arc optimizer afaik is able to remove atomics when it can determine code is not used for multi threading too.
That sounds a lot like C# which uses GC everywhere except for structs.

Having optional GC but integrated GC does lead to a schism in API libraries (see C#, D, etc.). From my experience it's hard to go no-GC in C# because a lot of libraries depend on GC.

Yeah, same for enums in Swift. Although with classes in Swift it's not quite like a runtime GC. Static code is inserted to retain and release. Afaik the arc optimizer uses an ownership model to optimize placement of retain/releases.

I have been out of C# programming for over a decade now, but in Swift most guidance actually discourage use of classes, while a lot of types in the stdlib use a "Copy on Write" pattern where a struct contains a class that acts as a backing store. This gives the type value semantics but guarantees assignments will be cheap, only when the backing data is modified and the reference count is not unique is the data copied.

When people tell me that Rust is nice to write it feels like gaslighting. I want a smooth path from not having code to having running code and Rust doesn't offer this at all.
I think it depends on what alternate paths the person is thinking of. I throw most GC languages out. So now my path is mostly (not all) Rust, C, C++, etc.

Rust is great in that regard, for me at least. The stdlib is awesome, crates are awesome, powerful features like Iterators are awesome, and being confident in the code i produce is awesome. Which isn't a knock at C, but C just isn't for me.

I'm not sure how to interpret the "smooth path" thing in a way that applies to most languages but not rust.
When you find the path you need smoothest is the path from compiling/running code to _correctly working_ code, I think that's where Rust shines. Yes, writing code that will compiler is harder; and yes, you _will_ learn to more effortlessly write code that compiles.
That's subjective. I find it mostly natural (until you get into type bound fuckery) but I can't for love of God get good at Prolog or Lisp.
Good point. I'm finding Rust unpleasant (but expect that once I've crested the learning curve, this will change), but I find Lisp very intuitive and pleasant.

Probably has something to do with how we conceptualize things.

Depending on what language you're coming from, I think that's totally fair.

A part of Rust that I very much appreciate is that once you write Rust code and get it to compile, there's a pretty good chance that it's going to do what you expect. As someone who is new to Rust, it's definitely no small feat to get the code to compile in the first place. But I think that's one of the main things people tend to love once they get over the initial hump of grokking all the different ways of passing variables around.

I wonder if this is hurts testing culture with Rust. People think the compiler is magical enough they doing out a lot of energy into writing good tests still
You still need functional tests in rust.
IMO more than made up for by providing the tooling out of the box. It's easier to write your basic tests in Rust than it is in many languages because the Rust ecosystem already knows how to do unit testing and doc tests as supplied.

If you write comments with triple slashes, they're (Markdown format) documentation in Rust. If you use that Markdown to write an example of how to use something in your documentation, Rust's out of box tooling will assume that it should check your documented code works, by default. So you go from "I wrote an example in the source code of how to call my foo() function" to "I have basic HTML document for foo() and a test proving foo() basically works" in zero steps.

    /// Appends an element to the back of a collection.
    ///
    /// # Examples
    ///
    /// ```
    /// let mut vec = vec![1, 2];
    /// vec.push(3);
    /// assert_eq!(vec, [1, 2, 3]);
    /// ```
> But I think that's one of the main things people tend to love once they get over the initial hump of grokking all the different ways of passing variables around.

Agreed. I think the GP comment really underestimates how smooth Rust feels to Rust devs who have gotten over that hump. I've programmed in many languages and i confidently do not feel i am slower in Rust than other languages. Maybe not faster, but not slower in development speed.

But what i do get, for development speed, is the ability to solve large problems with many, many tools. I found this quite annoying in Go. In Rust, it's great. Complex problems are still complex, and Rust doesn't inherently simplify them - but it gives me the tools to come at problems in many ways. Which if you need it is invaluable.

For me at least, Rust smooths the path from not having code to having code which is correct. Out of Rust, C#, Python, Go, Java, and C, which I've spent non-trivial time writing, Rust comes easy first for the likelihood that my program actually does what I intended once it runs. Python is last, while C gets special commendation for being almost as unlikely as Python but with the extra fun of more Undefined Behaviour so when it doesn't work it's far more difficult to track down why not.

Edited: However, I don't see why my parent got down-voted.

I'm with you. Getting something working is a lot quicker when the "cargo check; fix errors; cargo check" loop catches the vast majority of your bugs than building a container image, uploading it to staging, and finding the magical combination of inputs that triggers that code path.
> tell me that Rust is nice to write it feels like gaslighting

There's a weird ideology or cultural attitude prevalent in Rust circles which does, indeed, involve endless gaslighting of anyone having difficulties with the language (at any level). It involves, amongst other things, insisting that the language is essentially so easy to write that any difficulties anyone has are not, in fact, difficulties, and the person claiming to experience them is not, in fact, doing so.

It's a form of group-identity narcissism I'm sure anyone will recognise as it's not unique to Rust (and seems to be something tech circles are particularly prone to, for, I suspect, demographic reasons).

My guess is this will diminish with Rust because of its fast growth in user numbers. It already seems more prevalant in some forums than others (stay away, for example, from the official Rust user forums, which are noxious).

You‘re correct.

What feels nice about Rust is that its typing system is tightly coupled with solutions that are impossible to use incorrectly.

My Mutex wraps my value. There is no way to access my value without correctly locking it first. I can‘t not handle all of my enum variants when matching. To get to the value of my result, I am forced to unwrap it, thereby handling a possible error.

While arriving at a compiling solution can be annoying, it feels nice to be able to trust.

People who say that are either delusional or speaking of the context of needing low level control. So it can be a lot nicer than writing C/C++/Fortran. It has lots of nice aspects, amazing package manager, amazing doc system, compiler errors. But fundamentally there is a whole new sort of thing to worry about compared to GC languages.
I have a lot of criticisms of Rust's usability, but I think you're being pretty much just unreasonable here. One of the first things a Rust novice will see when picking up the language is:

    let a = 1; // a is immutable
    let mut a = 1; // a is mutable
Then a bit further in they will learn about references, and that there are two kinds of references:

    a: &i8 // a is a immutable reference
    a: &mut i8 // a is a mutable reference
I believe it is pretty clear that "mut" is what signals mutability, while "&" signals that something is a reference. There is next to no reason to associate "&" intrinsically with mutability, since you will have seen "mut" in a non-reference context and you will have seen "&" in a non-mutable context.
The OP is entirely about how `&` is misunderstood and misused, and how just adding `&` to appease the compiler is a common learner behavior. The author then explains `&var` and `&mut var` not in terms of their nature as references but in their aspects of mutability. Your view is the way understanding of the language should proceed, but I argue that it is not the reality in fact.
Fair enough. I understood your phrasing more as a criticism of the language's design, and that is where I disagree, but I agree that the article could be structured better.
> how just adding `&` to appease the compiler is a common learner behavior.

If learner is unfamiliar with the very idea of references, then his/her problem is not an ampersand, but how to grasp the distinction between reference and value.

Rust makes it hard, because it takes its own view on how variable relates with its value: Rust variable holding a value behaves much more like a reference to the value than a variable in C does. It is hard to notice with Copy+Clone types, like integers, but it bites in the ass with others. Variable in C is associated with its memory. When you write `int a;` you create a variable which has `sizeof(int)` bytes of memory. It doesn't work with Rust, a place in memory is an attribute of value not of variable. `let a: i32` doesn't allocate memory, memory will come with a value, when you assign the value to `a`.

I mean it is hard to grasp at first. I came to Rust from a C world, and struggled to get this simple fact for a few months. It seems stupid in retrospect, it was written all over the place in Rust Book, but my mind took the relationship between a variable, memory and a value as a kind of an implicit dogma, and failed to notice how it breaks in Rust.

But I digress. So variable in Rust has some properties of a reference. And then there are "real" references like &, &mut, RefCell, Rc/Arc. These didn't made my life harder, they work like my C-mind expected them to work, but I assume that people who come from a higher level languages can struggle to grasp all the differences.

Unless something gets optimized away, I'm pretty sure "let a: i32" will allocate memory on the stack exactly like C. Do you have some sources?
By default new types in Rust are not Copy, so when you pass values of these types around they are moved, not copied. It leads for example to freedom to do something like:

    fn move_things(a: SomePrettyBigType) -> EvenBiggerType
When I was taught programming, I learnt that it is a bad thing, because passing big values to functions needs a lot of copying and is very slow. Not so with Rust. I personally did all the checks with emit=asm, and noticed how rustc passes an argument's address to a function (instead of copying an argument value) and one more address of a place, where function can build a value of EvenBiggerType.

I never checked whether rustc preallocate memory for uninitialized variable or not. I bet it doesn't even in debug builds, because it knows in advance when variable is used. C compiler can work without this knowledge, but rustc cannot: it needs to check lifetimes and borrowing. Yes, it is normally seen as an optimization in C, but in rust many of C optimizations become a part of a language.

> Do you have some sources?

No, sorry, I have no source, my mental representation of rust code is a creation of my mind (being modest I'd add, that it is a second-hand creation probably, I'm not invented Rust, so someone thought of it before me, and I've read a lot what others think, probably I picked it up somewhere). But I can give you a link[1] to an article comparing C++ and Rust move semantics. There you can see the difference between "variable has no value" and "variable have a value that means 'moved out'". I see this difference as static vs dynamic tracking of "moved out" status (probably the article talks about it, I didn't reread it now and do not remember clearly). And I interpret it as a difference in a variable semantic: rust's variable has no memory, rust's values have. C++ variable on the contrary has a memory which is used to store value of the variable.

I see it as a semantic differences of variables in Rust, because this semantics works. I can predict machine code generated, I can predict when borrow checker will become unhappy, I can track lifetimes in my mind. I mean a scope of a variable and a lifetime of a value are different things. I cannot imagine how anyone can think of a lifetime of a value without believing that memory occupied by a value is an attribute of a value. And at my first months of dealing with Rust I really tried to find a way.

[1] https://www.thecodedmessage.com/posts/cpp-move/

In rust moves are just copies that guarantee that no outstanding references to the old location can be held anywhere and thus the compiler is allowed to optimize away the copy. The same thing can be done by C/C++ compilers under some circumstances.
> I never checked whether rustc preallocate memory for uninitialized variable or not. I bet it doesn't even in debug builds, because it knows in advance when variable is used. C compiler can work without this knowledge, but rustc cannot: it needs to check lifetimes and borrowing. Yes, it is normally seen as an optimization in C, but in rust many of C optimizations become a part of a language.

Local variables just get a slot on the stack. Allocation is done for several at once, and it would be a lot of work to handle it per-variable for negligible benefit.

It would be especially difficult to handle code that has multiple variables and moves one into a Box, chosen by a calculation, then keeps using the rest. I bet none of that memory is going to get reclaimed, it will just sit there marked "do not touch".

But that's all about a stack with optimizations and deliberately vague semantics. We can also learn something by looking at objects. Rust objects work like C++. To move a field out, you have to replace it with a placeholder version of that field.

It seems to me that you believe, that there is some canonical representation of a source code by a machine code, and that optimizations are out of it. I do not think it this way. As I see it, compiler gets source code and generates machine code that have the same side-effects as I asked. Compiler is free to use any way to do it, while it doesn't change side-effects.

I choose to think this way because real generated machine code are more like this. Maybe debug builds are close to an idea of a canonical machine code, but I do not know, because I see no reason to look at machine code of debug builds. Moreover code without optimizations applied is harder to read with all that noise of unnecessary actions.

> Local variables just get a slot on the stack. Allocation is done for several at once, and it would be a lot of work to handle it per-variable for negligible benefit.

Local variables often can be not allocated at all. They can be eliminated by a compiler. I hold a view that it is incorrect to talk about variables at all if we talk about generated machine code. It is just like with a handcrafted assembler code: a counter can be pushed on the stack, then popped, increased, used as an index into an array, then pushed back to be popped once more to check should we do one more iteration of this. It is incorrect to say that counter is on the stack or it is in a register because sometimes it is here and sometimes it is there. The counter exists only in my mind, not in the code. When another person can deduce from my code the existence of the counter it is not because the counter exists in the code, it is because another person can deduce what I was trying to do.

Look at this:

  let s = SomeBigStruct::new();
  return Box::new(s);
With any luck s can be eliminated, value returned by SomeBigStruct::new() will not be stored on the stack at any point. And it is not the thing we can ignore, because SomeBigStruct can be very large and it may be not a good idea to place it on the stack or to spend time copying it from the stack into a Box. Rust allow us to eliminate variably ourselves:

  return Box::new(SomeBigStruct::new());
but it is the same basically: SomeBigStruct::new returns a big value which is then moved into a Box. If rustc was unable to move this value from SomeBigStruct::new into a Box without a call to memcpy, then Rust would be an academical curiosity, not a practical language at all. But sometimes I need to store SomeBigStruct in an intermediate variable for some reason, and Rust must be able to avoid memcpying nevertheless.

> It would be especially difficult to handle code that has multiple variables and moves one into a Box, chosen by a calculation, then keeps using the rest.

Yeah, there are different situations and different situations need different methods.

> But that's all about a stack with optimizations and deliberately vague semantics. We can also learn something by looking at objects.

Objects are objects. They are not variables, they are values. Structure field name is not a variable, it is a way to deal with a value of a variable. When I create a struct or an array, I allocate memory for that value, not for a variable.

> It seems to me that you believe, that there is some canonical representation of a source code by a machine code, and that optimizations are out of it. I do not think it this way. As I see it, compiler gets source code and generates machine code that have the same side-effects as I asked. Compiler is free to use any way to do it, while it doesn't change side-effects.

Well that's literally how C works. There's an "abstract machine" that runs your code, and that's how you know what side effects it's obligated to produce. Though the C abstract machine isn't very concerned with memory locations, so it's not particularly relevant to this discussion.

> Local variables often can be not allocated at all. They can be eliminated by a compiler. I hold a view that it is incorrect to talk about variables at all if we talk about generated machine code.

I'm aware that variables can be optimized out. But so can values! You can have a value in your program that takes up 0 memory even though it gets used to do something.

If we want to talk about variables or values correlating with memory, I think we should ignore optimizations like this. And I really don't think they provide evidence toward a claim that values have memory but variables don't.

> Structure field name is not a variable, it is a way to deal with a value of a variable.

You have a much more narrow definition of "variable" than I do. It has a name, it can hold a value... But fine, let's focus on the kind you consider variables.

> With any luck s can be eliminated, value returned by SomeBigStruct::new() will not be stored on the stack at any point.

> Yeah, there are different situations and different situations need different methods.

Let me try phrasing my argument a different way.

It's often easy for the compiler to avoid allocating memory on the stack at all. But your claim was that you could have uninitialized variables with no memory allocated, then initialize them, and they would now have memory allocated.

What's some code that does that?

Putting something in a Box isn't that. A Box has two parts, the small Box struct itself, and the memory it allocates for the contents. If you have a Box variable, it will be given space for the struct even before you run Box::new. (With the caveat that the compiler might try to shrink the scope where the variable exists, so you might need to do a little bit to prevent that.)

I answered you points below, but then I gone meta and thought about discussion becoming a metaphysical conundrum and decided to go back and tried to come from another angle. And this what I came with:

The worst part of C abstract machine in Rust is lifetimes. Lifetimes becomes hard to think of, borrow checker becomes impossible to satisfy. You move value from x to y, therefore you move memory, now you do not need to track a lifetime or a scope of x, it is irrelevant, it have no memory, it doesn't exist anymore. But you need to track y, because it owns some value, which is the memory. Lifetimes of values are different from scopes of variables, but it is lifetimes that matter to a borrow checker.

And now a lot of words about how I manage to think this way. I'm not sure it is relevant to the discussion, but because I already had typed it and if you might be interested...

> Well that's literally how C works. There's an "abstract machine" that runs your code, and that's how you know what side effects it's obligated to produce. Though the C abstract machine isn't very concerned with memory locations, so it's not particularly relevant to this discussion.

Yes, thinking about it, I agree. With C it is natural to believe in a canonical representation. I thought exactly this way before I moved to rust. Possibly I have in my mind some representation of this even for rust, but I'm not sure. Oftentimes I do not think of functions as of sub-programs that must be called, because mostly they are inlined. Functions are chunks of code that are combined into one to compose a working code. I do not think of variables as of something existing in a machine code, they are compile-time abstractions allowing me to relate to ideas of how to compute their values or to the values itself (it feels like lazy-evaluation mode of thinking, and it is, but evaluations in Rust are not always lazy).

> It's often easy for the compiler to avoid allocating memory on the stack at all. But your claim was that you could have uninitialized variables with no memory allocated, then initialize them, and they would now have memory allocated.

Not necessarily. A value can live in the memory of compiler. It can be computed statically and then used without really forming a structure or something. When you move a value of variable into a Box, it can easily happen. Value was calculated, but have no representation in the machine code, but then when Box is initialized, machine code does a few "mov"s of immediate values into Box.

> Putting something in a Box isn't that. A Box has two parts, the small Box struct itself, and the memory it allocates for the contents. If you have a Box variable, it will be given space for the struct even before you run Box::new.

No, it will not get space before Box::new

  struct Point {
      x: i64,
      y: i64,
  }
  impl Point {
      fn new(x: i64, y: i64) -> Self {
   Self {
       x: x,
       y: y,
   }
      }
  }
  #[inline(never)]
  fn make_a_point() -> Box<Point> {
      let p = Point::new(2, 3);
      Box::new(p)
  }
How does it translates into a machine code?

  _ZN3tmp12make_a_point17h643905a77db82570E:
 .cfi_startproc
 pushq %rax
 .cfi_def_cfa_offset 16
 movl $16, %edi
 movl $8, %esi
 callq *__rust_alloc@GOTPCREL(%rip) ; <-- allocation, now %rax is a pointer to uninitialized Box<Point>
 testq %rax, %rax
 je .LBB10_1
 movq $2, (%rax) ; <--- (%rax).x = 2;
 movq $3, 8(%rax) ; <--- (%rax).y = 3;
 popq %rcx
 .cfi_def_cfa_offset 8
 retq
See? There were not place to store Point { x: 2, y: 3 } before Box was created. And the only place in a program it exists as a Point (which can be addressed for example) it is a memory owned by a Box. So Point::new is evaluated after Box::new, and then inlined Point::new initializes not the memory on the stack but memory of the Box directly. In other case compiler might pass %rax into a constructor ...
Oh is Box just a single value? I only looked at Vec in detail and that stores a few different things.

But the code you wrote there isn't the kind of thing I was talking about. Let me try to do a better job explaining. After a bunch of trouble with the borrow checker, here is some code:

  #[derive(Debug)]
  struct Point {
    x: i64,
    y: i64,
  }
  impl Point {
    fn new(x: i64, y: i64) -> Self {
    Self {
      x: x,
      y: y,
    }
    }
  }
  use rand::Rng;
  extern crate alloc;
  fn rand() -> u8 {
      let mut rng = rand::thread_rng();
      rng.gen()
  }
  fn test() {
    let v1;
    let v2;
    let v3;
    let v4;
    let n: u8 = rand();
    let address;
    if n<30 {
        v1 = vec![Point::new(2, 3)];
        address = &v1 as *const Vec<Point> as usize;
    } else if n<60 {
        v2 = vec![Point::new(2, 3)];
        address = &v2 as *const Vec<Point> as usize;
    } else if n<90 {
        v3 = vec![Point::new(2, 3)];
        address = &v3 as *const Vec<Point> as usize;
    } else {
        v4 = vec![Point::new(2, 3)];
        address = &v4 as *const Vec<Point> as usize;
    }
    println!("{:#x}", address);
    println!("{:p}", &address);
    println!("distance: {:#}", (&n as *const u8 as usize) - address);
  }
  
  fn main() {
    for _ in 0..30 {
      test();
    }
  }

The assembly was annoying to go through so I printed the stack addresses instead. And I used Vec because it's bigger.

This code creates 4 vec variables. It randomly picks one to put a Vec with a Point into, and leaves the other 3 uninitialized.

Then whichever one got initialized has its address stored into "address".

Then it prints out that address, and the address of "n", the next thing on the stack. And it prints out the difference to be extra helpful.

I ran this on the rust playground.

2/3 of the time it uses v4. And 2/3 of the time it prints 31 as the distance.

The rest of the time is evenly split between v1, v2, and v3. And the rest of the outputs are randomly split between 55, 79, and 103.

Therefore I conclude that it allocated space for all four variables at the start of the function, even though there was only one value.

A variable doesn't take up space when the compiler can be clever enough to make it not exist at that moment in time. But that only happens for some code. It's easy to find cases where variables exist as real tangible chunks of memory, even though there aren't values. If you try to touch them, the borrow checker will complain extremely hard.

So here is how I would summarize things: Sometimes the compiler can avoid having variables exist at runtime. Sometimes the compiler can avoid having values exist at runtime. Very often it will reduce the amount of time a variable exists to avoid memory use and to avoid moves and copies. And very often a variable will actually be the final resting place of some value, rather than a default location on the stack. But variables are not just a compile-time concept. They can manifest as chunks of possibly-empty memory when the compiler isn't clever enough or when there's not enough benefit to being clever.

> Above there is an example for this. The Point comes to an existence at some point, while variable p doesn't. So variables do not exist at run time, while values do.

That's not an example of a value not existing at run time. I mean something like setting n=pi*2, then printing n with 2 digits of precision. If the compiler is having a good day, you'll see code that directly prints "6.28" and the value of pi*2 will never exist at run time.

In the most literal sense, neither values nor variables have to exist. Only side-effects exist, and the program could have a completely different implementation from what the source code suggests.

> When you write `int a;` you create a variable which has `sizeof(int)` bytes of memory. It doesn't work with Rust, a place in memory is an attribute of value not of variable. `let a: i32` doesn't allocate memory, memory will come with a value, when you assign the value to `a`.

This just isn't accurate at all. In the corresponding abstract machines, `int a = 0;` and `let a = 0u32;` mean the exact same thing. The only thing that move semantics change is that variables become unassigned when moved from (unless Copy-typed), and the rust frontend tracks unassigned variables and doesn't permit their use. This has nothing to do with memory allocation.

It turns out that where the AM allocates and where optimized code allocates are often wildly different, but do not mistake this for a difference in the languages. If I had to guess, (optimized) C was behaving differently than you believed, but you only noticed this when it came up in rust.

You write:

> Take the first two sections as a perfect example. You decide you want to introduce a single-character control symbol `&` (which is questionable by itself), how do you apply it? In one case you decide that this control symbol indicates that a value is immutable and in another it's part of the declaration of a value as explicitly mutable. You've now introduced a semantic contradiction that a learner has to overcome.

Here you write:

> The author then explains `&var` and `&mut var` not in terms of their nature as references but in their aspects of mutability.

- The angry, tired dog was running

- The angry dog was running

Does the “angry” cause confusion when trying to understand what “tired” means?

`&` never has anything to do with mutability. Only `mut`.

There is no “semantic” contradiction. “Weltschmerz” is a word, and “Welt” and “smerz” are two other words; the fact that they can occur in a compound noun doesn’t mean that “Welt” and “schmerz” by themselves get contaminated by each other.

The fact that they chose to use `&mut` is… syntactic. The semantics would be the same if they used `& mut` or `reference mutable` or `reference which is mutable`.

I’ve sprinkled some ampersands around Rust code in my little time but never because I was confused about how it interacted with mutability.

Fair point. I could have placed a greater emphasis on ownership and mutability being two separate axes.

That said, I think you downplay the ways that references and mutability do interact in Rust. If there is a single outstanding reference to a value, Rust won't let anyone mutate the underlying value. I think this is quite an important part of Rust to understand that's not obvious to someone who's new and especially coming from a language that enforces no such guarantees about mutability.

D's borrow checker simply keys off of the existing syntax:

    const a = 1; // a is immutable
    auto b = 1;   // b is mutable
    const(int)* p; // pointer to const
    int* q;  // pointer to mutable
There's no new syntax, the borrow checker just adds a layer of checks.
Rust references are basically pointers with extra rules, so using a different symbol is reasonable, and rust is immutable by default rather than opt-in (which is an important design decision) so you have to have syntax to mark things as mutable. auto is horribly uninformative for new coders, and var is too overloaded to expect newbies to only use it for mutable variables.
> What I can't wait for is Pythonic Rust where we have a language that treats humans as first class citiziens but offers the guarantees that Rust does.

The innate complexity of the ownership rules make a Pythonic Rust unlikely, someone must manage the memory so you either put up with that complexity, manually do it with malloc/free, or let the garbage collector do it (in which case it's not Rust anymore).

Perhaps you could have a Go-like Rust where everything that escapes the function or breaks ownership is automatically transformed in a reference counted object (which is still a form of garbage collection, but deterministic and compatible with system programming). So that the syntax is simple and programs are easy to compile, but you might get performance bugs when the program does heavy use of the ref count, invalidating cache lines left and right etc.

> or let the garbage collector do it (in which case it's not Rust anymore).

Arc<T> is still Rust. Not ideal, but very much still Rust. I wouldn't be surprised if despite wrapping absolutely every object in Arc, Rust still were to perform better than most high-level languages at whatever given task.

Arc isn't a garbage collector though, Arc is just reference counting. You could write this by hand.

Languages which have a GC may in some cases also do reference counting to reduce the load on the GC, but they need the GC because reference counting doesn't collect cycles.

It's trivial to make a cycle, then destroy all references to it - and in Rust that cycle lives forever, it can't be destroyed because each part of it has references from the rest. With a GC this gets collected, eventually, exactly when will vary which is why in the GC language destructors aren't as useful for resource clean-up as they are in languages like C++ or Rust.

I think the future is in languages that offer "opt-in" borrow checking, so you can use it where it makes sense. After all, most of a program should prefer simplicity over performance, and only optimize where it matters. I think this is the path towards a more Pythonic Rust.

Some languages that are going this direction, including D [0] and Vale [1]. Vale's particular insight is that we can get the benefits of borrow checking with isolates (from Pony) and integrating "pure blocks" and pure functions into the type system, so to speak.

> ... breaks ownership is automatically transformed in a reference counted object ...

Some languages actually do this! Lobster [2] uses borrow-checker-like semantics and falls back on reference counting. And worth mentioning is HVM [3], which falls back to cloning.

[0] https://dlang.org/blog/2019/07/15/ownership-and-borrowing-in...

[1] https://verdagon.dev/blog/zero-cost-memory-safety-regions-pa...

[2] https://www.strlen.com/lobster/

[3] https://github.com/HigherOrderCO/HVM

Thanks for the link, Vale is promising and the "generational references" trick sounds neat. I'm not entirely sure how it impacts developer productivity (i.e. I use a refcount precisely because I don't want to manually manage memory; a cryptic, hard to reproduce runtime crash related to a "generation mismatch" is not much more helpful than a double free, "safety" guarantees that translate to runtime crashes are irrelevant in the real world except where they expose violation of the runtime invariants, such as access past the end of the array; but maybe I don't fully grasp the concept they are proposing).

As a general sentiment, I fully agree that improving developer productivity while maintaining safety, speed and ahead of time compilation & static typing are open problems, and all current languages still suck more or less.

On the other hand though, it brings symmetry to types and values, and makes destructuring feel natural.

  let i: i32 = 1;
  let &i: &i32 = &1;
  let &mut i: &mut i32 = &mut 1;
  let (i, &mut j): (i32, &mut i32) = (1, &mut 2);
I'm not sure how you'd recreate this symmetry without using the same symbol in both places.
`&T` does not mean it's immutable. It means it's borrowed. `&mut T` means it's borrowed mutably `T` and `mut T` have the same logic but for owned variables. I don't get the issue there.
Wait... as a Rust newbie, you bring up a point I could use clarification on.

I get that "&mut T" means it's borrowed mutably, but doesn't that automatically mean that "&T" means it's borrowed immutably?

To a first approximation, yes. To a second approximation, "&T" is a shared borrow and "&mut T" is an exclusive borrow. The key point to bring up in the rephrasing here is that "&T" doesn't imply you can't mutate something. Why? Because interior mutability (for example, RefCell and Cell, but always underpinned by UnsafeCell) permits mutation through "&T". RefCell, for example, can be thought of as "borrow checker, but at runtime."

In the days of yore, an event called the "mutpocalypse" occurred where by (pre Rust 1.0) there was a huge discussion about potentially changing "&mut T" to "&only T" or "&uniq T" because the more precise model was one of sharing vs exclusivity, rather than immutability vs mutability. The immutability vs mutability model is still useful, it's just wrong in more circumstances. However, it does have the benefit of (IMO) being more accessible by describing borrows in terms of the most common effect: to mutate or not. I don't know which choice would have been the right one, but I am at peace with it.

"Rust Atomics and Locks" by Mara Bos has a great discussion of these points toward the beginning of her book, and specifically grounds the choice of which terminology to use in concrete and practical terms, which I really appreciated.

Hmm, OK. I think I follow this.

So, "&mut T" is a bit of a misnomer, then? I can roll with remembering that.

Depends. "&mut T" does give you mutability. It just also gives you something else too: exclusivity. So the fact that it gives you mutability is not wrong. It's just incomplete.

If something is a misnomer here, I'd probably say pronouncing "&T" as "immutable reference" is it. But it really is not entirely wrong either. I can craft a Rust API that only exposes a "&T" and I can make it so you cannot mutate it. Or I can make it so it looks like you aren't mutating anything, but under the covers, mutation is happening.

Take "Regex" for example. All of its methods take "&self." From the perspective of users of that API, thinking of "Regex" as immutable is absolutely the correct model. You, as a user, cannot do anything to that "Regex" to mutate it directly in a way that you can observe via the API. You can't swap out its internal NFA. You can't change its capture group names. You can't change the prefilters it uses. You can't do anything other than use APIs that purportedly tell you "nothing is changing."

Under the covers, stuff is definitely changing. The regex engines need mutable space to operate. But all of that is hidden from you, and there's no way to observe those mutations from the public API. (You can technically make observations in the most strictest sense and make an inference from them that things are being mutated internally. For example, by measuring how long it takes for certain regexes to execute. The first search might be measurably slower than the second. But this is clearly not something that is an API guarantee or is something you can directly observe through the API of the regex. I only mention it for completeness, and because this is the Internet.)

With all that said, in my mind's eye, when I see "&T" do I think "immutable"? No. My internal conception of it is "shared."

Thank you for this. This helps.

> So the fact that it gives you mutability is not wrong. It's just incomplete.

I understand. It means nothing to anybody outside my head, but if I think of "&mut" as a misnomer, that will help me to remember that it's not telling me the whole story.

I think the hardest part of learning any language is those little bits that are "hidden" by the syntax, so I need to find where to hang the lampshades so I remember them.

In addition to what BurntSushi said, it's worth thinking of the interaction as `&mut T` allows unsynchronized mutation. Because it's guaranteed to be unique, nothing else in the program can access the data behind the reference, so you don't need any synchronization to prevent data races.

With a `&T` on the other hand, the entire point is to allow more than one to exist. That means that arbitrarily many parts of the program could be accessing the data at the same time. If you allow unsynchronized mutation in that situation, you can end up with data races and mangled data from those different parts making changes at the same time.

That's where types such as the single-threaded `Cell` and `RefCell`, and the thread-safe `Mutex` and `RwLock` come in. They force you to go through them, and therefore synchronization, in order to mutate.

You've gotten good answers, but my take:

They're duals. Neither is more or less of a misnomer than the other. It's like one of those sculptures where if you look at it one way, it looks like one thing, but from another angle, it looks like something else.

I also think that Rust had no realistic option other than &mut in order to succeed. Rust already has enough weird stuff in it, and while &mut is a little weird, &uniq or similar would be too weird, and the weirdness would be too exposed to new developers too early.

I kind of believe that, as this kind of thing gets more popular, and more people become familiar with this conception of the dual nature of mutability and exclusivity, that future languages could experiment with terminology around exclusivity, and that they would not pay the same sort of price that Rust would have had to pay.

> Neither is more or less of a misnomer than the other.

I understand. But, at least for now, I need to conceptualize it as a misnomer to guard against me being confused by it.

Naming things is very hard. I'm sympathetic.

I kinda wish it had ended up with something like 'exc` for exclusive, or one of the options you mentioned. I think it would have made things easier for people to learn a helpful mental model.
Possibly. It's one of those things that is really difficult to know. It's easy to look at what the possible benefits are because the model is more technically precise, but it's much harder to think about what the possible negatives are. The "&uniq T" model might be more technically precise, but maybe it is also more technically impenetrable and adds just one too many things that newcomers struggle with. Then again, maybe the extra precision of the model would end up helping more than hurting.

It's really hard to say because of set of people for whom "&uniq T" would have helped more is non-empty. But what is the size of the set of people for whom "&mut T" actually helps more? I have no clue. But it could be large.

It is interesting. When I was learning, I found “mut” way more confusing because it is less precise (it’s one level of abstraction higher, mixes “usual intent/goal” with “what does this keyword do”) so it added an extra layer that I had to mentally back out/elide every time I read the keyword.
See Val for a possible step into that direction.

https://www.val-lang.dev/

Or how the Chapel language for HPC is going at it,

https://chapel-lang.org/

I forget but with Val arent value types Move by default?
I think so, however in addition it provides customisation points, and uses Ada/Swift like ways of parameter passing.

The CppCon 2022 talk provides a good overview.

I think your idea of what Pythonic Rust might be goes against one of Rusts goals: not having hidden, costly abstractions, and instead giving the developer explicit control.

If you don't feel you need control over when data is copied, when there should be a mutex, atomic reference counting, and so on, you're probably reaching for the wrong language. IMO giving us explicit control of low level behavior is not a detriment to ergonomics as much as it is a benefit. Add in the package manager and great compiler and it feels like first class dev experience to me.

(comment deleted)
I'm a low level programmer, and programming in rust feels exactly like someone is taking the control I need to do my job away from me. maybe I'm wrong, and I can get it back by reverse engineering the whole stack and getting underneath things like tokio.
As a Rust newbie, here's a chance for my understanding to be corrected, but...

Isn't taking control away from the programmer the entire point of Rust? I thought the premise of it is that programmers can't be trusted.

That sounds like a criticism, but I don't mean it as such. It's just what I hear when I hear people talking about the value of Rust. Is my understanding incorrect?

no. I don't think you're wrong. I was just adding a counterpoint to the above.

as an old lisp programmer I really think systems should be less opaque. make things accessible and simple and useful, but make it easy to keep unwrapping. rust disturbs that journey by #deriving over the underlying truth to make things simple. but when you have to crack that open its a lot stuff to digest.

I don't think rust is _wrong_, I just think that value statement you just made should be more explicit. and maybe rust devotees shouldn't be so quick to tell you that your high level goal is incorrect because its something rust doesn't handle well.

I would disagree. Rust's borrow checker and other static analysis are more akin to the safety guards on a table saw. For 95% of situations they're just protect you by catching your mistakes, but there are times when the guard makes it hard or impossible to do what you need to.

In those situations, if you really need to do the work, you take the guards off for that work (unsafe would be the Rust equivalent), then put them back on afterwards.

Huh. I think perhaps I was thinking a bit wrong about "unsafe". My take on it was that it's a kind of cheating and its use would be disallowed in commercial production environments.

At least, it seems to get badmouthed a lot.

maybe unsafe rust is pretty ok to work in. as you say, I've never been in a shop where that was allowed
> its use would be disallowed in commercial production environments.

Working in embedded, completely banning unsafe would be impossible.

Minimizing it is a good thing, generally. But it's there to be used.

> Isn't taking control away from the programmer the entire point of Rust?

Not in my perspective. It is about managing complexity. Rust allows you to take that "ultimate control," and then wrap it up in an easier to use package that doesn't have sharp edges.

There is nothing you can't do in Rust if you want to. Heck, inline assembly is an actual part of the language, unlike in C and C++, where they're both vendor extensions!

> then wrap it up in an easier to use package

I can't wait until I can start to see how anything about Rust is "easier to use". It's a slog for me right now, and I usually pick up new languages pretty easily.

Again, not criticizing Rust. I'm taking the blame for my struggles myself.

In this context, I'm not talking about usability in a general sense, I'm talking about "You don't need to consider if you're invoking UB with safe functions, unlike with unsafe functions, because the compiler checks things for you, so the number of considerations is smaller."

You're right that maybe given that "easier" can mean many things to many people, I should have maybe chosen a different words.

> I'm taking the blame for my struggles myself.

I don't think you should. It takes time, and effort. There is a hump. I have dedicated a large amount of the last ten years trying to help fix that problem. It is very real.

I very much appreciate your explanations, as well as your reassuring words. Thank you.
> I don't think you should. It takes time, and effort. There is a hump. I have dedicated a large amount of the last ten years trying to help fix that problem. It is very real.

As a somewhat grumpy Rust learner, I appreciate you being clear about this.

To be fair, you can‘t really blame the complexity of a framework on the language. A similar comparison would be having gripes about C++ because of boost.

On the other hand, I am often dissatisfied with tokio‘s incredible complexity and the fact that it‘s the accepted async solution in the ecosystem.

yeah. I don't think 'this sucks lets do something else' is generally well received here.

plus you already have a limited selection of those crates that are async and aligned with the rest of the runtime. losing tokio would pretty much mean 80s style programming with no crates - definite no no

async & tokio can be a problem. Rust is much nicer to work with without them.

My take... I can see their use, but they should be confined to the outer layers of the onion.

Core components need not (should not) be written in this style.

async is just a function returning a promise.

Its convenience at the front end of the application shouldn't be allowed to cause distress at the lower levels.

They thought quite a lot about the user journey. And it's very clear that the user they thought about is an experienced C++ developer.

I bet the Rust developers never even dreamed of their language being easier to learn than C++. But it happened to be, so a lot of people go directly to it, and get the problems you are talking about. Unexpected success always causes some problems.

> And it's very clear that the user they thought about is an experienced C++ developer.

Yes. If you had to work in C or C++, Rust is a huge improvement. If you only know Javascript or Python, Rust seems way too finicky.

(If you had to work in assembler, C was a huge improvement.)

Rust ownership is language support for something C and C++ programmers had to think about anyway.

The real problem with Rust ownership is that backlinks are poorly supported. If you need a tree with links pointing back to the root in Rust, it's very difficult. There's Rc and Weak, but now you have to use .upgrade() on a weak link before you can use it, and it can fail. You can have all the objects owned by a Vec or HashMap, and refer to them by handle, but that's essentially manual pointer manipulation. Single-ownership with backlinks checked at compile time would be a big help for some data structures.

Strongly disagree.

I am the person who hates C/C++ and likes languages like Ruby. I like Rust specifically for the reason that I don't have to learn C++ like "grown ups" and can write my crypto libraries in something much more ergonomic.

When I started writing in Rust, it was 2018 and there was old borrow checker (NLL was experimental) and error messages were much more annoying than today. And still, that was nice language that is easy to write and read once you understand a couple of basic rules.

Here are the rules that you need to learn and always keep in mind, even if Rust had a more verbose syntax. And I bet many people struggling with Rust simply were not lucky enough to have those rules explained to them.

Rule 1. By default, in Rust things are "owned" and "moved", all the time. When you do `x = y`, you literally move "y" to "x". In other popular languages you might expect automatic copying or automatic referencing, but in Rust you get an unusual "move" semantics.

This rule applies universally to all things: be it simple numbers or structs, or references, or weird Rc<T> things or whatever. Once you move "x" to another location, you cannot use it any longer from the old location.

Rule 2. As an optimization, some simple types are marked "Copy" which means that you get a cheap implicit copy whenever you want to continue to use moved value. This applies mostly to numbers and fixed-sized types. This liberates you from explicitly calling ".clone()" on stupid integers.

Rule 3. There are references, mutable and immutable, that let you avoid moving value when you don't need to. At any point of time, each value may have N immutable references or 1 mutable one.

There are lexical references (most commonly used): & and &mut, for these the rule 3 is statically checked at compile-time. And there are runtime counterparts that implement rule 3 in runtime: Cell/RefCell.

So when you design an API or use an API, you should really think about ownership of the values that you pass in according to these 3 rules. You may have these questions and use the rules above to answer them.

- Should that API "consume" (get the value "moved into") that value, or should it have read-only or mutable access to it?

- What part of code (which other value) actually owns the value and is responsible for cleaning it up?

- Is that a dumb value that can be Copy-ied explicitly, or is there non-trivial copying cost that requires explicit references or clones for it?

- What does it mean to say `y = &x`? Per rule 1 we move reference "&x" to "y", but since it's a non-mutable reference, we can have a lot of them, so it implements Copy and &x can be used again per rule 2.

I've been learning Rust because it's clearly going to be an important language moving forward. I get what it's trying to do, but I do have to admit that I'm finding it an unpleasant and ugly language to use.

But it just joins the group of other unpleasant and ugly languages. It is what it is, and in a sense, it's following a tradition that's been around since the beginning.

>Now obviously Rust works, and that's its point, and people love that enough to overcome all the ergonomic challenges the language puts up. What I can't wait for is Pythonic Rust where we have a language that treats humans as first class citiziens but offers the guarantees that Rust does.

Those guarantees in anything more than a hello world application are hard to achieve. When people say "i want extremely high performance and safety but I want it to be simple and guessing what Im doing" are asking for completely contradictory things and normally what you get out is something that leaves you up the creek the moment you are not following their ideal use case.

>The only question in my mind for if this will ever happen is if the fully literate programming modality enabled by LLMs will make it a moot point.

Much like the above. It'll work fine for hello world applications. Anything beyond that it will be much harder because the complexity comes in making an application coherent both internally and with the external systems it interacts with. Which is fine. Plenty of stuff nowadays can be abstracted.

Swift had the potential to be Pythonic Rust but has a horrible server / package story.
Totally spot on. Swift could be this incredible language with all sorts of dialects suited for lower or higher level programming. But the ecosystem and corporate sponsors surrounding it continue to ensure that it remains nothing more than a UI App language for one kind of Mobile phone. It's pretty darn unfortunate IMHO. The state of SourceKit and SPM are pretty sad IMHO.
Agree with all of this. The Nim language might fit the bill of treating humans as first class citizens, and certainly fits better with my need for productivity and performance without over-specifying details.

The language focuses on readability first and borrow checking is an automatic compiler optimisation behind the scenes, with full type bound, constrained borrowing/move semantics being opt in as required.

There's loads of sensible ergonomics like this, for example large immutable parameters are automatically passed as pointers for you while the compiler enforces the immutability in the code, making things simple, efficient, and safe by default, with less cognative overhead or need for these kinds of micro-optimisations leaking into the architecture.

Saying that, Rust‘s multithreading story is better for now as it currently offers wider aliasing protection although arguably at the cost of developer friction.

Suggest an alternative syntax that would be less confusing? The & symbol is familiar to many people because it has similar meaning in C,C++, Go, and other languages.

Rust gets the unique mix of safety and performance by making you have to deal with memory, that is fundamentally more complicated.

Spot on. I think Rust could have been even better if it didn’t have all this ugly syntax. I would like to see something more akin to Swift.
Re: & references generally, esp to &self; one of the biggest initial temptations and early pitfalls one will fall into with Rust is to apply object-oriented design patterns (from C++ or Java world, etc), and tie a lot of functions/behaviours to structs, instead of having free-floating functions.

And it's this that can quickly get you into trouble with excessive & and &mut references where you don't need them, and trouble with borrowing generally. Passing that &self argument around all over the place -- even when you might not need to -- means you end up borrowing or declaring mutability at times when you may not want to. Also leading down the path of ending up resorting to Arc/Rc, or Box etc.

It's one thing I struggled with, moving mostly from C++, but I suspect people coming from a C (not C++) programming background struggle with a lot less as they are used to what I'd call module-function-level programming, rather than binding functions to classes all over the place.

What tripped me up early on (and still does to some extent) isn't the ownership of simple values, but when you get to generics. Do I implement the trait for Option<T> and Option<&T>. When will implementations overlap and when will they not? It becomes even trickier when you realize that a generic T can also represent a reference type &'a mut U or similar. So you can't just blindly implement the trait for both T and &T either.

Basically: even though I know exactly what the ownership does in the context of "it's a clever C" I'm still tripping over myself when I take the same concepts to be "a clever C++".

Please add example for each section ;-) that would help the rust newbie in me :-)

(or links to such examples)

I do not understand why it is

&mut var

Instead of

mut &var

Having two version of mut just doesn’t make sense to me. I love rust, but haven’t gotten nearly enough time to use it, and little things like this use up more memory in my brain than it should for such a well thought out system.

Both are valid Rust. The former means "I don't own var, but I can change it", while the later means "I own a reference that I can change where it points, but I can't change the pointed value."

An argument could be made that it shouldn't have been '&mut' and instead be something like '&exclusive'.

`let mut` is completely unnecessary from perspective of the language's semantics (everything owned exclusively can be mutated safely), but exists because people really really like having an extra lint against accidental reassignment of a variable.