I'm far from a Rust pro, but I think the dismissal of alternatives like Polonius seems too shallow. Yes, it is still in the works, but there's nothing fundamentally wrong about the idea of a borrow checker.
This is true both in theory and in practice, as you can write any program with a borrow checker as you can without it.
TFA also dismisses all the advantages of the borrow checker and focuses on a narrow set of pain points of which every Rust developer is already aware. We still prefer those borrowing pain points over what we believe to be the much greater pain inflicted by other languages.
For the disjoint field issues raised, it’s not that the borrow checker can’t “reason across functions,” it’s that the field borrows are done through getter functions which themselves borrow the whole struct mutably. This could be avoided by making the fields public so they can be referenced directly, or if the fields needs to be passed to other functions, just pass the the field references rather than passing the whole struct.
There are open ideas for how to handle “view types” that express that you’re only borrowing specific fields of a struct, including Self, but they’re an ergonomic improvement, not a semantic power improvement.
The borrow checker is also what I like the least about Rust, but only because I like pattern matching, zero-cost abstractions, the type system, fearless concurrency, algebraic data types, and Cargo even more.
the returned references are, for the purposes of aliasing rules, references to the entire struct rather than to pieces of it. `x` and `y` are implementation details of the struct and not part of its public API. Yes, this is occasionally annoying but I think the inverse (the borrow checker looking into the implementations of functions, rather than their signature, and reasoning about private API details) would be more confusing.
I also disagree with the author that his rejected code:
fn main() {
let mut point = Point { x: 1.0, y: 2.0 };
let x_ref = point.x_mut();
let y_ref = point.y_mut();
*x_ref *= 2.0;
*y_ref *= 2.0;
}
"doesn't even violate the spirit of Rust's ownership rules."
I think the spirit of Rust's ownership rules is quite clear that when calling a function whose signature is
fn f<'a>(param: &'a mut T1) -> &'a mut T2;
`param` is "locked" (i.e., no other references to it may exist) for the lifetime of the return value. This is clear once you start to think of Rust borrow-checking as compile-time reader-writer locks.
This is often necessary for correctness (because there are many scenarios where you need to be guaranteed exclusive access to an object beyond just wanting to satisfy the LLVM "noalias" rules) and is not just an implementation detail: the language would be fundamentally different if instead the borrow checker tried to loosen this requirement as much as it could while still respecting the aliasing rules at a per-field level.
This post pretty much completely ignores the advantages of the borrow checker. I'm not talking about memory safety, which is it's original purpose. I'm talking about the fact that code that follows Rust's tree-style ownership pattern and doesn't excessively circumvent the borrow checker is more likely to be correct.
I don't think that was ever the intent behind the borrow checker but it is definitely an outcome.
So yes, the borrow checker makes some code more awkward than it would be in GC languages, but the benefits are easily worth it and they stretch far beyond memory safety.
> code that follows Rust's tree-style ownership pattern
This is a pretty important qualification. Most low-level systems code doesn't and can't have this ownership structure. It provides a reason why Rust has more traction replacing code that could have been written in Java rather than e.g. C++ in the domains where C++ excels (like database engines).
I don't use Rust much, but I agree with the thrust of the article. However, I do think that the borrowchecker is the only reason Rust actually caught on. In my opinion, it's really hard for a new language to succeed unless you can point to something and say "You literally can't do this in your language"
Without something like that, I think it just would have been impossible for Rust to gain enough momentum, and also attract the sort of people that made its culture what it is.
Otherwise, IMO Rust would have ended up just like D, a language that few people have ever used, but most people who have heard of it will say "apparently it's a better safer C++, but I'm not going to switch because I can technically do all that stuff in C++"
About 20 years ago your choice of language basically boiled down to what you were going to pick for your web server. Your choices were
- Java (popular among people who went to college and learned all about OOP or places that had a lot of "enterprise" software development)
- Ruby on Rails (which was the hot new thing)
- Python or Perl to be the P in your LAMP stack
- C++ for "performance"
All of these were kitchen sink choices because they wound up needing to do everything. If you went back in time and said you were building a language that didn't do something incredibly common and got in the way of your work, no one would pick it up.
Wow. By your other comments I know you "know things". But this...this you do not know.
Rails was "hot" 20 years ago? lol, and you have completely missed IIS, .Net, and (dun dun duuuuuuuuunnnnn!!!!!) PHP. I bet you don't even know what ASP was.
Also...wth with C++ or python on a web server...20 years ago? Ok maybe this is entertaining now. Since you know so much, tell me about the framework libs needed for C++ web apps 20 years ago. eats popcorn <- that...is from BBS, maybe look that up too.
Are you an LLM bot or real person?
You're not just kind of wrong here; you're not just a little wrong; you're "having a bad day" level of wrong.
Take the afternoon off, and drink a Jamba Juice. Maybe call some family and tell them you love them.
It was interesting towards the latter half of the article where the author talks about how much of the correctness may be culturally enforced:
>More amorphous, but not less important is Rust's strong cultural affinity for correctness. For example, go to YouTube and click on some Rust conference channel. You'll see that a large fraction of the talks are on correctness, in some way or another. That's not something I see in Julia or Python conference talks.
And it creates an interesting chicken and egg approach. The borrow checker may indeed be too strict (and of course, has its edge cases and outright bugs), but its existence (rather than the utility it brings) may have in fact attracted and amassed an audience who cares about correctness above all else. Even if we abolished the borrow checker tomorrow, this audience may still maintain a certain style based on such principles, party because the other utilities of Rust were built around it.
It's very intriguing. But like anything else trying to attract people, you need something new and flashy to get people in the door. Even for people who traditionally try to reject obvious sales pitches.
Java, especially after generics were introduced, was a pain to use because of the type system. That’s not my opinion, I always found that claim a bit overwrought but it’s true that it was fairly widespread. Dealing with the type system got progressively a bit easier as the language evolved and certain types could be inferred. From the release notes I’ve seen I’ve gotten the impression that similar things have happened with the borrow checker. But people who have gone through the gauntlet have trouble reframing that experience and it’s always difficult to tell if the reputation is still accurate or not.
I am curious what the second language with a borrow checker will look like.
> In that sense, Rust enables escapism: When writing Rust, you get to solve lots of 'problems' - not real problems, mind you, but fun problems.
This is a real problem across the entire industry, and Rust is a particularly egregious example because you get to justify playing with the fun stimulating puzzle machine because safety—you don't want unsafe code, do you? Meanwhile there's very little consideration to whether the level of rigidity is justified in the problem domain. And Rust isn't alone here, devs snort lines of TypeScript rather than do real work for weeks on end.
I've recently wondered if it's possible to extract a subset of Rust without references and borrow checking by using macros (and a custom stdlib).
In principle, the language already has raw pointers with the same expressive power as in C, and unlike references they don't have aliasing restrictions. That is, so long as you only use pointers to access data, this should be fine (in the sense of, it's as safe as doing the same thing in C or Zig).
Note that this last point is not the same as "so long as you don't use references" though! The problem is that aliasing rules apply to variables themselves - e.g. in safe rust taking a mutable reference to, say, local variable and then writing directly to that variable is forbidden, so doing the same with raw pointers is UB. So if you want to be on the safe side, you must never work with variables directly - you must always take a pointer first and then do all reads and writes through it, which guarantees that it can be aliased.
However, this seems something that could be done in an easy mechanical transform. Basically a macro that would treat all & as &raw, and any `let mut x = ...` as something like `let mut x_storage = ...; let x = &raw mut x_storage` and then patch up all references to `x` in scope to `*x`.
The other problem is that stdlib assumes references, but in principle it should be possible to mechanically translate the whole thing as well...
And if you make it into a macro instead of patching the compiler directly, you can still use all the tooling, Cargo, LSP(?) etc.
If you don't want memory safety, it seems like it'd be easier to use C++ with a static analyzer to disallow the parts you don't like. I suppose the lack of a good package manager would still be a problem.
You can freely alias &Cell<T>, and this would give you memory safety compared to raw pointers. AIUI, &Cell<T> is effectively the moral equivalent (but safe) to T* in C/C++.
This reminds me of something that was popular in some bioinformatics circles years ago. People claimed that Java was faster than C++. To "prove" that, they wrote reasonably efficient Java code for some task, and then rewrote it in C++. Using std::shared_ptr extensively to get something resembling garbage collection. No wonder the real Java code was faster than the Java code written in C++.
I've been writing C++ for almost 30 years, and a few years of Rust. I sometimes struggle with the Rust borrow checker, and it's almost always my fault. I keep trying to write C++ in Rust, because I'm thinking in C++ instead of Rust.
The lesson is always the same. If you want to use language X, you must learn to write X, instead of writing language Y in X.
Using indexes (or node ids or opaque handles) in graph/tree implementations is a good idea both in C++ and in Rust. It makes serialization easier and faster. It allows you to use data structures where you can't have a pointer to a node. And it can also save memory, as pointers and separate memory allocations take a lot of space when you have billions of them. Like when working with human genomes.
There is such a thing of languages that align with human intuition. C++ and Rust are not these languages so you have to really learn these languages in depth. Languages like typescript or python or go align more with intuition and you don't really need to learn as much about the details or patterns as these just naturally flow from your intuition. This is a huge huge thing as it makes the language literally take about a week to develop proficiency and two weeks to develop mastery. A language like C++... you can't even develop mastery in a year.
That is not to say these languages are better. Intuition is just one trade off.
The closest language to "rust without borrowchecker" is probably MoonBit [0] - weirdly niche, practical, beautifully designed language.
When I was going through its docs I was impressed with all those good ideas one after the other. Docs itself are really good (high information density that reads itself).
> In that sense, Rust enables escapism: When writing Rust, you get to solve lots of 'problems' - not real problems, mind you, but fun problems.
If this is true for Rust, it's 10x more true for C++!
Lifetime issues are puzzles, yes, but boring and irritating ones.
But in C++? Select an appetizer, entree, and desert (w/ bottomless breadsticks) from the Menu of Meta Programming. An endless festival of computer science sideshows living _in the language itself_ that juices the dopamine reward of figuring out a clever way of doing something.
Came here to comment on the same thing. I've never been able to articulate this as well as the author did, and it is so true! Every programming language requires you to solve some puzzles that are just in the way of the real problems you are trying to solve, but some much more than others.
People have compared Rust to C++ and others have argued that they really aren't alike, but I think it's in these puzzles that they are more alike than any other two languages. Even just reading rust code is a brain teaser for me!
I think this is why C and Zig get compared too. They apparently have roughly the same level of "fun problems" to solve.
I think there's a lot love for the borrowchecker because a lot of people in the Rust community are working on ecosystems (eg https://github.com/linebender) which means they are building up an api over many years. In that case having a very restrictive language is really great, because it kinda defines the shape the api can have at the language level, meaning that familiarity with Rust also means quick familiarity with your api. In that sense it doesn't matter if the restrictions are "arbitrary" or useful.
The other end of the spectrum is something like gamedev: you write code that pretty explicitly has an end-date, and the actual shape of the program can change drastically during development (because it's a creative thing) so you very much don't want to slowly build up rigidity over time.
If a friend told me they liked Rust but didn't like the borrow checker, I'd probably point them to Gleam and Moonbit, which both seem awesome in their own niches.
Both have rust-like flavor and neither has a borrow checker.
I can't really get over Gleam's position that nobody really needs type-based polymorphism, real programmers write their own vtables by hand.
(It also needs some kind of reflection-like thing, either compile-time or runtime, so that there can be an equivalent of Rust's Serde, but at least they admit that that needs doing.)
>The first time someone gave be this advice, I had to do a double take. The Rust community's whole thing is commitment to compiler-enforced correctness, and they built the borrowchecker on the premise that humans can't be trusted to handle references manually. When the same borrowchecker makes references unworkable, their solution is to... recommend that I manually manage them, with zero safety and zero language support?!? The irony is unreal. Asking people to manually manage references is so hilariously unsafe and unergonomic, the suggestion would be funny if it wasn't mostly sad.
Indices aren't simply "references but worse". There are some advantages:
- they are human readable
- they are just data, so can be trivially serialized/deserialized and retain their meaning
- you can make them smaller than 64 bits, saving memory and letting you keep more in cache
Also I don't see how they're unsafe. The array accesses are still bounds-checked and type-checked. Logical errors, sure I can see that. But where's the unsafety?
Different meanings of "unsafety". A cool thing about Rust's references, when used idiomatically, is that not only are you guaranteed no memory corruption, you're also guaranteed no runtime panics. These are not "unsafe" by Rust's definition, but they're still a correctness violation and it's good to prevent them statically when you can. Indices don't give you this protection.
Reminder that this is what the author said about safety: "it manages to combine speed and low-level control with a high level of bug-resistance, more commonly referred to as safety"
What's the alternative though? If you're fine with garbage collection, just use garbage collection. If you're _not_ fine with garbage collection (because you want deterministic performance, or you have resources that aren't just memory) then Rust's borrow checker seems like the best thing going.
You can use Zig, a faster, safer C with best-in-class metaprogramming for a systems-level language. It doesn't guarantee safety to the same extent as Rust but gets you 80% of the benefit with 20% of the pain.
struct Id(u32);
fn main() {
let id = Id(5);
let mut v = vec![id];
println!("{}", id.0);
}
isn't even legit in modern C++. That's just move semantics. When you move it, it's gone at the old name.
He does point out two significant problems in Rust. When you need to change a program, re-doing the ownership plumbing can be quite time-consuming. Losing a few days on that is a routine Rust experience. Rust forces you to pay for your technical debt up front in that area.
The other big problem is back references. Rust still lacks a good solution in that area. So often, you want A to own B, and B to be able to reference A. Rust will not allow that directly.
There are three workarounds commonly used.
- Put all the items in an array and refer to them by index. Then write run-time code to manage all that. The Bevy game engine is an example of a large Rust system which does this. The trouble is that you've re-created dangling pointers, in the form of indices kept around after they are invalid. Now you have most of the problems of raw pointers. They will at least be an index to some structure of the right type, but that's all the guarantee you get. I've found bugs in that approach in Rust crates.
- Unsafe code with raw pointers. That seldom ends well. Crates which do that are almost the only time I've had to use a debugger on Rust code.
- Rc/RefCell/run-time ".borrow()". This moves all the checking to run time. It's safe, but you panic at run time if two things borrow the same item.
This is a fundamental problem in Rust. I've mentioned this before. What's needed to fix this is an analyzer that checks the scope of explicit .borrow() and .borrow_mut() calls, and determines that all scopes for the same object are disjoint. This is not too hard conceptually if all the .borrow() calls produce locally scoped results. It does mean a full call chain analysis. It's a lot like static detection of deadlock, which is a known area of research [1] but something not seen in production yet.
I've discussed this with some of the Rust developers. The problem is generics. When you call a generic, the calling code has no idea what code the generic is going to generate. You don't know what it's going to borrow. You'd have to do this static analysis after generic expansion. Rust avoids that; generics either compile for all cases, or not at all. Such restricted generic expansion avoids the huge compile error messages from hell associated with C++ template instantiation fails. Post template expansion static analysis is thus considered undesirable.
Fixing that could be done with annotation, along the lines of "this function might borrow 'foo'". That rapidly gets clunky. People hate doing transitive closure by hand. Remember Java checked exceptions.
This is a good PhD topic for somebody in programming language theory. It's a well-known hard problem for which a solution would be useful. There's no easy general fix.
> isn't even legit in modern C++. That's just move semantics. When you move it, it's gone at the old name.
Exactly the opposite actually.
Rust has destructive move while modern C++ has nondestructive move.
So in Rust, an object is dead after you move out of it, and any further attempts to use it are a compiler diagnosed error. In contrast, a C++ object is remains alive after the move, and further use of it isn't forbidden by the language, although some or all uses might be forbidden by the specific user provided move function - you'll have to reference the documentation for that move function to find out.
Rust borrow checker is designed to enforce "one owner" model (a tree).
When you need to have more than one reference, you can use Rc + Weak[0]. Example DoubleLinkedList implementation:
Moreover, if you have cycles instead of trees, you can use a garbage collector with support for cycles, like rust-cc[1].
So yes, it's cannot be done statically, because Rust is not designed for that.
However, problem disappears when 'static lifetime is used (or arenas). Nodes can be marked as deleted, instead of dropping them, so pointers are always valid.
In same vein, when nodes are deleted rarely, they can be simply marked as deleted, without dropping them completely (until sibling nodes are updated, at least):
I just had a thought! It might make languages like Rust more ergonomic if movement could also update the reference to the new location so that moving a local variable into a container could also update the variable to reference the target also.
The "broken" example can be trivially fixed:
fn main() {
let id = Id(5);
let mut v = vec![id];
let id = v[0].0; // just use the new name!
println!("{}", id );
}
But what if the language supported "move and in-place update the reference"?
Something like:
let mut v = vec![@id]; // Some new symbol
Where '@' (or whatever) is a new operator that says: move the object and update the reference `id` to point to the moved value. This could only be used for parameters marked with some sort of attribute indicating that this is possible.
I don't agree with the examples in the post. To me, they all seem to support the case that the compiler is doing the right thing and flagging potential issues. In a larger and more complex program (or a library to be used by others), it's a lot harder to reason about such things. Frankly, why should I be keeping all that in my mind when the compiler can do it for me and warn when I'm about to do something that can't verified as safe.
Of course, designing for safety is quite complex and easy to get wrong. For example, Swift's "structured concurrency" is an attempt to provide additional abstractions to try to hide some complexity around life times and synchronization... but (personally) I think the results are even more confusing and volatile.
To the author, I would be a borrow checker apologist or perhaps extremist. I will take that mantle gladly: I am very much of the opinion that a systems programming language without a borrow checker[^1] will not find itself holding C++-like hegemony anymore (once/if C++ releases the scepter, that is). I guess I would even be sad if C++ kept the scepter for the rest of my life, or was replaced by another language that didn't have something like a borrow checker.
It doesn't need to be Rust: Rust's borrow checker has (mostly reasonable) limitations that eg. make some interprocedural things impossible while being possible within a single function (eg. &mut Vec<u32> and &mut u32 derived from it, both being used at the same time as shared references, and then one or the other being used as exclusive later). Maybe some other language will come in with a more powerful and omniscient borrow checker[^1], and leave Rust in the dust. It definitely can happen, and if it does then I suppose we'll enjoy that language then.
But: it is my opinion that a borrow checker is an absolutely massive thing in a (non-GC) programming language, and one that cannot be ignored in the future. (Though, Zig is proving me wrong here and it's doing a lot of really cool things. What memory safety vulnerabilities in the Ziglang world end up looking like remains to be seen.) Memory is always owned by some_one_, its validity is always determined by some_one_, and having that validity enforced by the language is absolutely priceless.
Wanting GC for some things is of course totally valid; just reach for a GC library for those cases, or if you think it's the right tool for the job then use a GC language.
[^1]: Or something even better that can replace the borrow checker; maybe Graydon Hoare's original idea of path based aliasing analysis would've been that? Who knows.
The author's motivation for writing this is well-founded. However, the author doesn't take into account the full spirit of rust and the un-constructive conclusion doesn't really help anyone.
A huge part of the spirit of rust is fearless concurrency. The simple seeming false positive examples become non-trivial in concurrent code.
The author admits they don't write large concurrent - which clearly explains why they don't find much use in the borrow checker. So the problem isn't that the rust doesn't work for them - it's that a central language feature of rust hampers them instead of helping them.
The conclusion for this article should have been: if you're like me and don't write concurrent programs, enums and matches are great. The language would be work better for me if the arc/box syntax spam went away.
As a side note, if your code is a house of cards, it's probably because you prematurely optimized. A good way to get around this problem is to arc/box spam upfront with as little abstraction as possible, then profile, then optimize.
Yeah, the author's theoretical "Rust but with garbage collector" would gain a whole bunch of concurrency bugs. It wouldn't be Rust anymore, just c# with a more functional syntax.
"Fearless concurrency" is one of the best things the borrow checker gives us, and I think a lot of people undervalue it.
> [The pain of the borrow checker is felt] when your existing project requires a small modification to ownership structure, and the borrowchecker then refuses to compile your code. Then, once you pull at the tiny loose fiber in your code's fabric, you find you have to unspool half your code before the borrowchecker is satisfied.
Probably I just haven't been writing very "advanced" rust programs in the sense of doing complicated things that require advanced usages of lifetimes and references. But having written rust professionally for 3 years now, I haven't encountered this once. Just putting this out there as another data point.
Of course, partial borrows would make things nicer. So would polonius (which I believe is supposed to resolve the "famous" issue the post mentions, and maybe allow self-referential structs a long way down the road). But it's very rare that I encounter a situation where I actually need these. (example: a much more common need for me is more powerful consteval.)
Before writing Rust professionally, I wrote OCaml professionally. To people who wish for "rust, but with a garbage collector", I suggest you use OCaml! The languages are extremely similar.
I guess I'm a bit confused how you can write rust professionally dor 3 years and never encounter this. When I started writing rust in ~2020/2021 i already had issues with the brorow checker.
Maybe its an idiom you already picked up in OCaml and did it mostly right in rust too?
I've mostly experienced it when moving from borrowing to ownership and vice versa. E.g. having a struct that takes ownership over its fields, and then moving it to a borrow with a lifetime.
It's not super common though, especially if the code is not in the hot path which means you can just keep things simple and clone.
- Marking and sweeping cause latency spikes which may be unacceptable if your program must have millisecond responsiveness.
- GC happens intermittently, which means garbage accumulates until each collection, and so your program is overall less memory efficient.
With modern concurrent collectors like Java's ZGC, that's not the case any longer. They show sub-millisecond pause times and run concurrently. The trade-off is a higher CPU utilization and thus reduced overall throughput, which if and when it is a problem can oftentimes be mitigated by scaling out to more compute nodes.
Rust is pretty good target for Claude Code and the like. I don't write much Rust but I have to say of the langs I used Claude Code with Rust experience was among the best. If default async runtime was not work stealing I prob would use Rust way more.
Regarding Indexes: "When the same borrowchecker makes references unworkable, their solution is to... recommend that I manually manage them, with zero safety and zero language support?!?"
Language support: You can implement extension traits on an integer so you can do things like current_node.next(v) (like if you have an integer named 'current_node' which is an index into a vector v of nodes) and customize how your next() works.
Also, I disagree there is 'zero safety', since the indexes are into a Rust vector, they are bounds checked by default when "dereferencing" the index into the vector (v[i]), and the checking is not that slow for vast majority of use cases. If you go out of bounds, Rust will panic and tell you exactly where it panicked. If panicking is a problem you could theoretically have custom deference code that does something more graceful than panic.
But with using indexes there is no corruption of memory outside of the vector where you are keeping your data, in other words there isn't a buffer overflow attack that allows for machine instructions to be overwritten with data, which is where a huge amount of vulnerabilities and hacks have come from over the past few decades. That's what is meant by 'safety' in general.
I know people stick in 'unsafe' to gain a few percent speed sometimes, but then it's unsafe rust by definition. I agree that unsafe rust is unsafe.
Also you can do silly optimization tricks like if you need to perform a single operation on the entire collection of nodes, you can parallelize it easily by iterating thru the vector without having to iterate through the data structure using next/prev leaf/branch whatever.
88 comments
[ 2.3 ms ] story [ 47.8 ms ] threadThis is true both in theory and in practice, as you can write any program with a borrow checker as you can without it.
TFA also dismisses all the advantages of the borrow checker and focuses on a narrow set of pain points of which every Rust developer is already aware. We still prefer those borrowing pain points over what we believe to be the much greater pain inflicted by other languages.
There are open ideas for how to handle “view types” that express that you’re only borrowing specific fields of a struct, including Self, but they’re an ergonomic improvement, not a semantic power improvement.
I also disagree with the author that his rejected code:
"doesn't even violate the spirit of Rust's ownership rules."I think the spirit of Rust's ownership rules is quite clear that when calling a function whose signature is
`param` is "locked" (i.e., no other references to it may exist) for the lifetime of the return value. This is clear once you start to think of Rust borrow-checking as compile-time reader-writer locks.This is often necessary for correctness (because there are many scenarios where you need to be guaranteed exclusive access to an object beyond just wanting to satisfy the LLVM "noalias" rules) and is not just an implementation detail: the language would be fundamentally different if instead the borrow checker tried to loosen this requirement as much as it could while still respecting the aliasing rules at a per-field level.
I don't think that was ever the intent behind the borrow checker but it is definitely an outcome.
So yes, the borrow checker makes some code more awkward than it would be in GC languages, but the benefits are easily worth it and they stretch far beyond memory safety.
This is a pretty important qualification. Most low-level systems code doesn't and can't have this ownership structure. It provides a reason why Rust has more traction replacing code that could have been written in Java rather than e.g. C++ in the domains where C++ excels (like database engines).
Without something like that, I think it just would have been impossible for Rust to gain enough momentum, and also attract the sort of people that made its culture what it is.
Otherwise, IMO Rust would have ended up just like D, a language that few people have ever used, but most people who have heard of it will say "apparently it's a better safer C++, but I'm not going to switch because I can technically do all that stuff in C++"
- Java (popular among people who went to college and learned all about OOP or places that had a lot of "enterprise" software development)
- Ruby on Rails (which was the hot new thing)
- Python or Perl to be the P in your LAMP stack
- C++ for "performance"
All of these were kitchen sink choices because they wound up needing to do everything. If you went back in time and said you were building a language that didn't do something incredibly common and got in the way of your work, no one would pick it up.
Rails was "hot" 20 years ago? lol, and you have completely missed IIS, .Net, and (dun dun duuuuuuuuunnnnn!!!!!) PHP. I bet you don't even know what ASP was.
Also...wth with C++ or python on a web server...20 years ago? Ok maybe this is entertaining now. Since you know so much, tell me about the framework libs needed for C++ web apps 20 years ago. eats popcorn <- that...is from BBS, maybe look that up too.
Are you an LLM bot or real person?
You're not just kind of wrong here; you're not just a little wrong; you're "having a bad day" level of wrong.
Take the afternoon off, and drink a Jamba Juice. Maybe call some family and tell them you love them.
>More amorphous, but not less important is Rust's strong cultural affinity for correctness. For example, go to YouTube and click on some Rust conference channel. You'll see that a large fraction of the talks are on correctness, in some way or another. That's not something I see in Julia or Python conference talks.
And it creates an interesting chicken and egg approach. The borrow checker may indeed be too strict (and of course, has its edge cases and outright bugs), but its existence (rather than the utility it brings) may have in fact attracted and amassed an audience who cares about correctness above all else. Even if we abolished the borrow checker tomorrow, this audience may still maintain a certain style based on such principles, party because the other utilities of Rust were built around it.
It's very intriguing. But like anything else trying to attract people, you need something new and flashy to get people in the door. Even for people who traditionally try to reject obvious sales pitches.
That's exactly what people say about Rust: just get good, use some tools, and be careful - and you can achieve the same memory safety.
I am curious what the second language with a borrow checker will look like.
This is a real problem across the entire industry, and Rust is a particularly egregious example because you get to justify playing with the fun stimulating puzzle machine because safety—you don't want unsafe code, do you? Meanwhile there's very little consideration to whether the level of rigidity is justified in the problem domain. And Rust isn't alone here, devs snort lines of TypeScript rather than do real work for weeks on end.
In principle, the language already has raw pointers with the same expressive power as in C, and unlike references they don't have aliasing restrictions. That is, so long as you only use pointers to access data, this should be fine (in the sense of, it's as safe as doing the same thing in C or Zig).
Note that this last point is not the same as "so long as you don't use references" though! The problem is that aliasing rules apply to variables themselves - e.g. in safe rust taking a mutable reference to, say, local variable and then writing directly to that variable is forbidden, so doing the same with raw pointers is UB. So if you want to be on the safe side, you must never work with variables directly - you must always take a pointer first and then do all reads and writes through it, which guarantees that it can be aliased.
However, this seems something that could be done in an easy mechanical transform. Basically a macro that would treat all & as &raw, and any `let mut x = ...` as something like `let mut x_storage = ...; let x = &raw mut x_storage` and then patch up all references to `x` in scope to `*x`.
The other problem is that stdlib assumes references, but in principle it should be possible to mechanically translate the whole thing as well...
And if you make it into a macro instead of patching the compiler directly, you can still use all the tooling, Cargo, LSP(?) etc.
I've been writing C++ for almost 30 years, and a few years of Rust. I sometimes struggle with the Rust borrow checker, and it's almost always my fault. I keep trying to write C++ in Rust, because I'm thinking in C++ instead of Rust.
The lesson is always the same. If you want to use language X, you must learn to write X, instead of writing language Y in X.
Using indexes (or node ids or opaque handles) in graph/tree implementations is a good idea both in C++ and in Rust. It makes serialization easier and faster. It allows you to use data structures where you can't have a pointer to a node. And it can also save memory, as pointers and separate memory allocations take a lot of space when you have billions of them. Like when working with human genomes.
That is not to say these languages are better. Intuition is just one trade off.
When I was going through its docs I was impressed with all those good ideas one after the other. Docs itself are really good (high information density that reads itself).
[0] https://www.moonbitlang.com
If this is true for Rust, it's 10x more true for C++!
Lifetime issues are puzzles, yes, but boring and irritating ones.
But in C++? Select an appetizer, entree, and desert (w/ bottomless breadsticks) from the Menu of Meta Programming. An endless festival of computer science sideshows living _in the language itself_ that juices the dopamine reward of figuring out a clever way of doing something.
People have compared Rust to C++ and others have argued that they really aren't alike, but I think it's in these puzzles that they are more alike than any other two languages. Even just reading rust code is a brain teaser for me!
I think this is why C and Zig get compared too. They apparently have roughly the same level of "fun problems" to solve.
The other end of the spectrum is something like gamedev: you write code that pretty explicitly has an end-date, and the actual shape of the program can change drastically during development (because it's a creative thing) so you very much don't want to slowly build up rigidity over time.
Both have rust-like flavor and neither has a borrow checker.
(It also needs some kind of reflection-like thing, either compile-time or runtime, so that there can be an equivalent of Rust's Serde, but at least they admit that that needs doing.)
Indices aren't simply "references but worse". There are some advantages:
- they are human readable
- they are just data, so can be trivially serialized/deserialized and retain their meaning
- you can make them smaller than 64 bits, saving memory and letting you keep more in cache
Also I don't see how they're unsafe. The array accesses are still bounds-checked and type-checked. Logical errors, sure I can see that. But where's the unsafety?
He does point out two significant problems in Rust. When you need to change a program, re-doing the ownership plumbing can be quite time-consuming. Losing a few days on that is a routine Rust experience. Rust forces you to pay for your technical debt up front in that area.
The other big problem is back references. Rust still lacks a good solution in that area. So often, you want A to own B, and B to be able to reference A. Rust will not allow that directly. There are three workarounds commonly used.
- Put all the items in an array and refer to them by index. Then write run-time code to manage all that. The Bevy game engine is an example of a large Rust system which does this. The trouble is that you've re-created dangling pointers, in the form of indices kept around after they are invalid. Now you have most of the problems of raw pointers. They will at least be an index to some structure of the right type, but that's all the guarantee you get. I've found bugs in that approach in Rust crates.
- Unsafe code with raw pointers. That seldom ends well. Crates which do that are almost the only time I've had to use a debugger on Rust code.
- Rc/RefCell/run-time ".borrow()". This moves all the checking to run time. It's safe, but you panic at run time if two things borrow the same item.
This is a fundamental problem in Rust. I've mentioned this before. What's needed to fix this is an analyzer that checks the scope of explicit .borrow() and .borrow_mut() calls, and determines that all scopes for the same object are disjoint. This is not too hard conceptually if all the .borrow() calls produce locally scoped results. It does mean a full call chain analysis. It's a lot like static detection of deadlock, which is a known area of research [1] but something not seen in production yet.
I've discussed this with some of the Rust developers. The problem is generics. When you call a generic, the calling code has no idea what code the generic is going to generate. You don't know what it's going to borrow. You'd have to do this static analysis after generic expansion. Rust avoids that; generics either compile for all cases, or not at all. Such restricted generic expansion avoids the huge compile error messages from hell associated with C++ template instantiation fails. Post template expansion static analysis is thus considered undesirable.
Fixing that could be done with annotation, along the lines of "this function might borrow 'foo'". That rapidly gets clunky. People hate doing transitive closure by hand. Remember Java checked exceptions.
This is a good PhD topic for somebody in programming language theory. It's a well-known hard problem for which a solution would be useful. There's no easy general fix.
[1] https://dl.acm.org/doi/10.1145/3540250.3549110
Exactly the opposite actually.
Rust has destructive move while modern C++ has nondestructive move.
So in Rust, an object is dead after you move out of it, and any further attempts to use it are a compiler diagnosed error. In contrast, a C++ object is remains alive after the move, and further use of it isn't forbidden by the language, although some or all uses might be forbidden by the specific user provided move function - you'll have to reference the documentation for that move function to find out.
This article explains the difference well: https://www.foonathan.net/2017/09/destructive-move/
So yes, it's cannot be done statically, because Rust is not designed for that.
However, problem disappears when 'static lifetime is used (or arenas). Nodes can be marked as deleted, instead of dropping them, so pointers are always valid.
In same vein, when nodes are deleted rarely, they can be simply marked as deleted, without dropping them completely (until sibling nodes are updated, at least):
When node is deleted (its payload is dropped), linked list is still walkable.[0]: https://doc.rust-lang.org/std/rc/struct.Weak.html
[1]: https://github.com/frengor/rust-cc
(Emphasis mine.)
I just had a thought! It might make languages like Rust more ergonomic if movement could also update the reference to the new location so that moving a local variable into a container could also update the variable to reference the target also.
The "broken" example can be trivially fixed:
But what if the language supported "move and in-place update the reference"?Something like:
Where '@' (or whatever) is a new operator that says: move the object and update the reference `id` to point to the moved value. This could only be used for parameters marked with some sort of attribute indicating that this is possible.Of course, designing for safety is quite complex and easy to get wrong. For example, Swift's "structured concurrency" is an attempt to provide additional abstractions to try to hide some complexity around life times and synchronization... but (personally) I think the results are even more confusing and volatile.
It doesn't need to be Rust: Rust's borrow checker has (mostly reasonable) limitations that eg. make some interprocedural things impossible while being possible within a single function (eg. &mut Vec<u32> and &mut u32 derived from it, both being used at the same time as shared references, and then one or the other being used as exclusive later). Maybe some other language will come in with a more powerful and omniscient borrow checker[^1], and leave Rust in the dust. It definitely can happen, and if it does then I suppose we'll enjoy that language then.
But: it is my opinion that a borrow checker is an absolutely massive thing in a (non-GC) programming language, and one that cannot be ignored in the future. (Though, Zig is proving me wrong here and it's doing a lot of really cool things. What memory safety vulnerabilities in the Ziglang world end up looking like remains to be seen.) Memory is always owned by some_one_, its validity is always determined by some_one_, and having that validity enforced by the language is absolutely priceless.
Wanting GC for some things is of course totally valid; just reach for a GC library for those cases, or if you think it's the right tool for the job then use a GC language.
[^1]: Or something even better that can replace the borrow checker; maybe Graydon Hoare's original idea of path based aliasing analysis would've been that? Who knows.
A huge part of the spirit of rust is fearless concurrency. The simple seeming false positive examples become non-trivial in concurrent code.
The author admits they don't write large concurrent - which clearly explains why they don't find much use in the borrow checker. So the problem isn't that the rust doesn't work for them - it's that a central language feature of rust hampers them instead of helping them.
The conclusion for this article should have been: if you're like me and don't write concurrent programs, enums and matches are great. The language would be work better for me if the arc/box syntax spam went away.
As a side note, if your code is a house of cards, it's probably because you prematurely optimized. A good way to get around this problem is to arc/box spam upfront with as little abstraction as possible, then profile, then optimize.
"Fearless concurrency" is one of the best things the borrow checker gives us, and I think a lot of people undervalue it.
Probably I just haven't been writing very "advanced" rust programs in the sense of doing complicated things that require advanced usages of lifetimes and references. But having written rust professionally for 3 years now, I haven't encountered this once. Just putting this out there as another data point.
Of course, partial borrows would make things nicer. So would polonius (which I believe is supposed to resolve the "famous" issue the post mentions, and maybe allow self-referential structs a long way down the road). But it's very rare that I encounter a situation where I actually need these. (example: a much more common need for me is more powerful consteval.)
Before writing Rust professionally, I wrote OCaml professionally. To people who wish for "rust, but with a garbage collector", I suggest you use OCaml! The languages are extremely similar.
Maybe its an idiom you already picked up in OCaml and did it mostly right in rust too?
It's not super common though, especially if the code is not in the hot path which means you can just keep things simple and clone.
- Marking and sweeping cause latency spikes which may be unacceptable if your program must have millisecond responsiveness.
- GC happens intermittently, which means garbage accumulates until each collection, and so your program is overall less memory efficient.
With modern concurrent collectors like Java's ZGC, that's not the case any longer. They show sub-millisecond pause times and run concurrently. The trade-off is a higher CPU utilization and thus reduced overall throughput, which if and when it is a problem can oftentimes be mitigated by scaling out to more compute nodes.
Language support: You can implement extension traits on an integer so you can do things like current_node.next(v) (like if you have an integer named 'current_node' which is an index into a vector v of nodes) and customize how your next() works.
Also, I disagree there is 'zero safety', since the indexes are into a Rust vector, they are bounds checked by default when "dereferencing" the index into the vector (v[i]), and the checking is not that slow for vast majority of use cases. If you go out of bounds, Rust will panic and tell you exactly where it panicked. If panicking is a problem you could theoretically have custom deference code that does something more graceful than panic.
But with using indexes there is no corruption of memory outside of the vector where you are keeping your data, in other words there isn't a buffer overflow attack that allows for machine instructions to be overwritten with data, which is where a huge amount of vulnerabilities and hacks have come from over the past few decades. That's what is meant by 'safety' in general.
I know people stick in 'unsafe' to gain a few percent speed sometimes, but then it's unsafe rust by definition. I agree that unsafe rust is unsafe.
Also you can do silly optimization tricks like if you need to perform a single operation on the entire collection of nodes, you can parallelize it easily by iterating thru the vector without having to iterate through the data structure using next/prev leaf/branch whatever.