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).
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.
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.
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'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.
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.
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.
> 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.
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.
> 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.
`&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.
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.
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.
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.
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.
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.
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.
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.
> 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).
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.
> 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.
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.
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.
>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.
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.
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.
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.
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.
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.
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 &¶meter 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.
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!"
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.
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.
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.
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++.
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?
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++".
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.
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
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.
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.
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.
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.
Deleted Comment
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.
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
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.
I get that "&mut T" means it's borrowed mutably, but doesn't that automatically mean that "&T" means it's borrowed immutably?
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.
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.
Edited: However, I don't see why my parent got down-voted.
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.
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.
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.
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).
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.
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.
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.
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.
https://www.val-lang.dev/
Or how the Chapel language for HPC is going at it,
https://chapel-lang.org/
Rust gets the unique mix of safety and performance by making you have to deal with memory, that is fundamentally more complicated.
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.
Dead Comment
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.
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.
https://without.boats/blog/the-registers-of-rust/
https://en.wikipedia.org/wiki/Register_(sociolinguistics)
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.
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!"
> 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.
It seems the latter is more common than the former, and that would avoid to have to add "&" everywhere.
Currently the types are:
You're suggesting that instead you skip the "&" and write some sigil for owned types, say "@": 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.
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.
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.
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.
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.
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.
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++".
[0] https://tafttest.com/
Dead Comment
For those of us who are allergic to JavaScript.