[HN Gopher] The borrowchecker is what I like the least about Rust
       ___________________________________________________________________
        
       The borrowchecker is what I like the least about Rust
        
       Author : jakobnissen
       Score  : 87 points
       Date   : 2025-07-19 19:27 UTC (3 hours ago)
        
 (HTM) web link (viralinstruction.com)
 (TXT) w3m dump (viralinstruction.com)
        
       | airstrike wrote:
       | 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.
        
         | umanwizard wrote:
         | Polonius will not fix the "issues" the author is complaining
         | about, because contrary to his assertion, they are actual
         | fundamental properties of how the Rust ownership/borrowing
         | model is supposed to work, not shortcomings of an
         | insufficiently smart implementation.
        
       | alilleybrinker wrote:
       | 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.
        
         | mirashii wrote:
         | > 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
         | 
         | Right, and even more to the point, there's another important
         | property of Rust at play here: a function's signature should be
         | the only thing necessary to typecheck the program; changes in
         | the body of a function should not cause a caller to fail. This
         | is why you can't infer types in function signatures and a
         | variety of other restrictions.
        
           | sowbug wrote:
           | See Rust's golden rule:
           | https://steveklabnik.com/writing/rusts-golden-rule
        
             | majormajor wrote:
             | This seems to be a golden rule of many languages? `return
             | 3` in a function with a signature that says it's going to
             | return a string is going to fail in a lot of places,
             | especially once you exclude bolted-on-after-the-fact type
             | hinting like what Python has.
             | 
             | It's easier to "abuse" in some languages with casts, and of
             | course borrow checking is not common, but it also seems
             | like just "typed function signatures 101".
             | 
             | Are there common exceptions to this out there, where you
             | can call something that says it takes or returns one type
             | but get back or send something entirely different?
        
               | mirashii wrote:
               | Many functional and ML-based languages, such as Haskell,
               | OCaml, F#, etc. allow the signature of a function to be
               | inferred, and so a change in the implementation of a
               | function can change the signature.
        
               | mirashii wrote:
               | > Are there common exceptions to this out there, where
               | you can call something that says it takes or returns one
               | type but get back or send something entirely different?
               | 
               | I would personally consider null in Java to be an
               | exception to this.
        
               | tnh wrote:
               | In C++, the signature of a function template doesn't
               | necessarily tell you what types you can successfully call
               | it with, nor what the return type is.
               | 
               | Much analysis is delayed until all templates are
               | instantiated, with famously terrible consequences for
               | error messages, compile times, and tools like IDEs and
               | linters.
               | 
               | By contrast, rust's monomorphization achieves many of the
               | same goals, but is less of a headache to use because once
               | the signature is satisfied, codegen isn't allowed to
               | fail.
        
               | spacechild1 wrote:
               | > In C++, the signature of a function template doesn't
               | necessarily tell you what types you can successfully call
               | it with, nor what the return type is.
               | 
               | That's the whole point of Concepts, though.
        
           | JoshTriplett wrote:
           | Exactly. We've talked about fixing this, but doing so
           | _without_ breaking this encapsulation would require being
           | able to declare something like (syntax is illustrative only)
           | ` &mut [set1] self` and `&mut [set2] self`, where `set1` and
           | `set2` are defined as non-overlapping sets of fields in the
           | definition of the type. (A type with private fields could
           | declare semantic non-overlapping subsets without actually
           | exposing which fields those subsets consist of.)
        
         | saghm wrote:
         | It's super easy to demonstrate your point with the first
         | example the article gives as well; instead of separate methods,
         | nothing prevents defining a method `fn x_y_mut(&mut self) ->
         | (&mut f64, &mut 64)` to return both and use that in place of
         | separate methods, and everything works! This obviously doesn't
         | scale super well, but it's also not all that common to need to
         | structure this way in the first place.
        
       | airstrike wrote:
       | 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.
        
         | umanwizard wrote:
         | Fearless concurrency is only possible because of the borrow
         | checker.
        
       | umanwizard wrote:
       | IMO, it is reasonable that in the example given:
       | struct Point {           x: f64,           y: f64,       }
       | impl Point {           fn x_mut(&mut self) -> &mut f64 {
       | &mut self.x           }                fn y_mut(&mut self) ->
       | &mut f64 {               &mut self.y           }       }
       | 
       | 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.
        
         | jrpelkonen wrote:
         | I found the arguments in this article disingenuous. First, the
         | author complains that borrowchecker examples are toys, then
         | proceeds to support their case with rather contrived examples
         | themselves. For instance, the map example is not using the
         | entry api. They'd be better served by offering up some real
         | world examples.
        
           | timmytokyo wrote:
           | The author explained why he used contrived examples. It's
           | because the pain arises most acutely only after your project
           | has become large and mature but demands a small ownership-
           | impacting change. The toy examples demonstrate the problem in
           | the small, but they generalize to larger and more complex
           | scenarios.
           | 
           | He's basically talking about the rigidity that Rust's borrow
           | checking imposes on a program's data design. Once you've got
           | the program following all the rules, it can be
           | extraordinarily difficult to make even a minor change without
           | incurring a time-consuming and painful refactor.
           | 
           | This is an argument about the language's ergonomics, so it
           | seems like a fair criticism.
        
         | lblume wrote:
         | It would not just be "confusing". It would be fundamentally
         | unacceptable because there would just be no local reasoning
         | anymore, and a single private field change might trigger a
         | whole cascade of nonlocal borrowing errors.
         | 
         | Unfortunately, this behavior does sometimes occur with Send
         | bounds in deeply nested async code, which is why I mostly
         | restrain from using colored-function style asynchronous code at
         | all in favor of explicit threadpool management which the borrow
         | checker excels at compared to every other language I used.
        
       | IshKebab wrote:
       | 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.
        
         | mikepurvis wrote:
         | There's the practical end goal benefit of safer and more robust
         | programs, but I think there's also the piece that pg talks
         | about in Beating The Averages which is that learning how to
         | cooperate with these conventions and think like there's the
         | borrow checker there _makes you a better programmer even when
         | you return to languages that don 't have it_.
        
           | al_fanta wrote:
           | > makes you a better programmer
           | 
           | If a language is bad, but you must use it, then yes learn it.
           | But, if the borrowchecker is a source of pain in Rust, why
           | not andmit it needs work instead of saying that "it makes you
           | better"?
           | 
           | I'm not going to start writing brainfuck because it makes me
           | a better programmer.
        
             | lblume wrote:
             | I believe that codebases written in Rust, with borrow
             | checking in mind, are often very readable and allow local
             | reasoning better than most other languages. The potential
             | hardness might not stem from "making people better
             | programmers" but from "making programmers write better
             | code, perhaps at the cost of some level of convenience".
        
             | IshKebab wrote:
             | We _do_ admit it needs work. The issues the author
             | highlights _can_ be annoying, a smarter borrow checker
             | could maybe solve them.
             | 
             | The point is the borrow checker has already gone beyond the
             | point where the benefits outweigh those annoyances.
             | 
             | It's like... Static typing. Obviously there are cases where
             | you're like "I know the types are correct! Get out of my
             | way compiler!" but static types are still vastly superior
             | because of all the benefits they convey in spite of those
             | occasional times when they get in the way.
        
               | creata wrote:
               | > The issues the author highlights can be annoying, a
               | smarter borrow checker could maybe solve them
               | 
               | I don't think a smarter borrow checker could solve most
               | of the issues the author raises. The author wants borrow
               | checking to be an interprocedural analysis, but it isn't
               | one by design. Everything the borrow checker knows about
               | a function is in its signature.
        
         | eigenspace wrote:
         | He's not ignoring them. The point of the article is that the
         | author doesn't experience those things as concrete advantages
         | for them. Like sure, there are advantages to those things, but
         | the author says he doesn't feel it's worth the trouble in his
         | experience for the sorts of code he's writing.
        
           | nine_k wrote:
           | One good RCE in production could alter this perception quite
           | a bit. "The mosquito repellent is useless, I see too few
           | mosquitos around me anyway."
        
             | eigenspace wrote:
             | The author is a bioinformatician writing scientific
             | software, and often switching back and forth between Rust,
             | Julia, and Python. His concerns and priorities are not the
             | same as people doing systems-level programming.
        
               | nine_k wrote:
               | Maybe Rust, a systems language, is just a wrong tool for
               | bioinformatic tasks. Go, Java, Typescript, Ocaml, Scala,
               | Haskell easily offer a spectrum from extreme simplicity
               | to extreme expressiveness, with good performance and
               | library support, but without needing to care about memory
               | allocation and deallocation. (Python, if you use it as
               | the frontend to pandas / polars, also counts.)
        
       | eigenspace wrote:
       | 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++"
        
         | mr_00ff00 wrote:
         | This is also somewhat backed up by the fact that OCaml (to my
         | understanding) is basically GC Rust without a borrow checker,
         | and yet it's basically a hobby language.
        
           | dismalaf wrote:
           | Hobby language? Plenty of commercial and important software
           | has been written in OCaml.
           | 
           | Hell, the early versions of the Rust compiler were written in
           | OCaml...
        
             | littlestymaar wrote:
             | Realistically unless you want to work at Jane Street or
             | Inria (the French computer science lab where Ocaml was
             | made), if you want to use Ocaml, it's going to be as a
             | hobby.
        
               | umanwizard wrote:
               | There is also Ahrefs.
        
               | dismalaf wrote:
               | You can say that for almost any language that's not
               | C/C++, C#, Java, Python and JS. Rust is just barely
               | beginning to become "corporate". Even Ruby, which is
               | pretty mainstream, has relatively few jobs compared to
               | the big corporate languages.
        
               | littlestymaar wrote:
               | Non-hobby languages is a narrow club, yes.
               | 
               | Your list is at least missing PHP, Typescript, Swift, Go,
               | Lua, Ruby and Rust though.
               | 
               | But Ocaml really doesn't belong anywhere close to this
               | list.
        
             | vlovich123 wrote:
             | How may be the wrong word, but it's definitely a niche
             | language and significantly less software is written in it
        
             | mr_00ff00 wrote:
             | Maybe I'm wrong, but I only really know of Jane Street for
             | OCaml, meanwhile FAANG all has at least some rust code.
             | 
             | Also I would argue the rust compiler started as a hobby
             | project
        
               | airstrike wrote:
               | Also Bloomberg:
               | https://news.ycombinator.com/item?id=28278553
        
               | dismalaf wrote:
               | Facebook Messenger's backend was/is OCaml... React was
               | originally written in SML, then OCaml, then whatever it
               | is now. And a bunch of places use it for various things.
               | 
               | https://ocaml.org/industrial-users
        
               | amelius wrote:
               | So you think of Jane Street as a bunch of hobbyists?
        
           | nine_k wrote:
           | Rather, an academia language.
           | 
           | Also, OCaml had trouble with multithreading for quite some
           | time, which was a limiting factor for many applications.
           | 
           | Facebook made a large effort to thrust OCaml into the
           | limelight, and even wrote a nice alternative frontend
           | (Reason). Sadly, it did not stick.
        
             | creata wrote:
             | I had the impression that SML was more popular in academia,
             | and OCaml in industry.
             | 
             | Old but funny comparison: http://adam.chlipala.net/mlcomp/
        
               | pyrale wrote:
               | SML was a generation before ocaml. I would say the two
               | languages from the same generation that competed for
               | academia's mindshare were ocaml and Haskell.
        
               | creata wrote:
               | Timeline-wise, sure, but I was referring to their
               | present-day use.
        
           | creata wrote:
           | I think there are two other big differences that also helped
           | Rust become popular:
           | 
           | * Rust has a C++-flavored syntax, but OCaml has a relatively
           | alien ML-flavored syntax.
           | 
           | * Rust has the backing of Mozilla, but I don't think OCaml
           | had comparable industry backing. (Jane Street, maybe?)
        
           | pyrale wrote:
           | > and yet it's basically a hobby language.
           | 
           | The difference between academia languages such as ocaml or
           | haskell and industry languages such as Java or C# is hundreds
           | of millions of dollar in advertising. It's not limited to the
           | academy: plenty of languages from other horizons failed, that
           | weren't backed by companies with a vested interest in you
           | using their language.
           | 
           | You should probably not infer too much from a language's
           | success or failure.
        
             | estebank wrote:
             | You're making it sound like the success of a language is
             | determined purely by its advertising budget by pointing at
             | languages that had financial backing, which disregards that
             | financial backing allows for more resources to solve
             | technical problems. Java and C# have excellent developer
             | tools which wouldn't have existed in their current state
             | without lots of money being thrown around, and the
             | languages' adoption trajectory wouldn't have looked the way
             | they did if their tooling hasn't been as good as it was. A
             | new language with 3 people behind it can come up with great
             | ideas and excellent execution, but if you can't get enough
             | of the scaffolding built in order to gain development
             | momentum and adoption, then it is very hard to become
             | mainstream, and money can help with that.
        
           | mavelikara wrote:
           | The first version of Rust compiler, I think, was written in
           | OCaml.
        
           | amelius wrote:
           | Idiomatic programming in a functional language requires
           | garbage collection. There is a reason languages like OCaml
           | and Haskell have a garbage collector. Without it, programming
           | in these languages would be completely different.
           | 
           | If you look at it from that perspective, then Rust is the
           | hobby language.
        
         | roland35 wrote:
         | I'm not sure.. without the borrow checker you could have a
         | pretty nice language that is like a "pro" version of golang,
         | with better typing, concise error handling syntax, and sum
         | types. If you only use things like String and Arc objects, you
         | basically can do this, but it'd be nice to make that not
         | required!
        
           | eigenspace wrote:
           | That's my whole point. Without the borrow checker it would
           | have been a nice language, but I believe it would not have
           | gotten popular, because being nice isnt enough to be popular
           | in the current programming language landscape.
        
       | Spivak wrote:
       | > 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.
        
         | hollerith wrote:
         | Have you _tried_ to assure yourself that this or that piece of
         | software (your primary text editor for example) doesn 't need
         | to be memory safe because it won't ever receive as input any
         | data that might have been crafted by an attacker? In my
         | experience, doing that is harder than satisfying the borrow
         | checker.
        
           | Spivak wrote:
           | Yes, and you can choose to use any language with a garbage
           | collector and get the same benefit. The list of memory safe
           | languages at your disposal is endless and they come in every
           | flavor you can imagine.
        
             | airstrike wrote:
             | _> Yes, and you can choose to use any language with a
             | garbage collector_
             | 
             | Uh, no thanks.
             | 
             |  _> and get the same benefit._
             | 
             | Not quite.
        
             | nine_k wrote:
             | The cost of it is spending more CPU and more RAM on the GC.
             | Often it's the cost you don't mind paying; a ton of good
             | software is written in Java, Kotlin, TS/JS, OCaml, etc.
             | 
             | Sometimes you can't afford that though, from web browsers
             | to MCUs to hardware drivers to HFT.
        
             | creata wrote:
             | That's true (with some qualifications), but everyone seems
             | to continue using C and C++ for everything, even for
             | applications like text editors, where the performance of a
             | GC language would presumably be good enough. I wonder why.
        
         | ok123456 wrote:
         | Typescript has escape hatches so you can just say "I don't
         | care, or don't know."
         | 
         | With Rust, you're battling a compiler that has a very
         | restrictive model, that you can't shut up. You will end up
         | performing major refactors to implement what seem like trivial
         | additions.
        
           | aapoalas wrote:
           | You can always use `Box<dyn Any>` to get the same result in
           | Rust :)
        
             | bryanlarsen wrote:
             | Or use clone everywhere. I am not ashamed of having lots of
             | clones everywhere outside of inner loops.
        
       | olq wrote:
       | Skill issue
        
         | lblume wrote:
         | It is true that any sufficiently complicated technology
         | requires a certain skill level to use it adequately. The
         | question remains whether the complexity of the technology is
         | justified, and the author presents an argument why this might
         | not be the case. Remarking their supposed lack of skill does
         | not seem particularly productive.
        
       | littlestymaar wrote:
       | I really struggle to understand the PoV of the author in his _The
       | rules themselves are unergonomical_ section:
       | 
       | > But what's the point of the rules in this case, though? Here,
       | the ownership rules does not prevent use after free, or double
       | free, or data races, or any other bug. It's perfectly clear to a
       | human that this code is fine and doesn't have any actual
       | ownership issues
       | 
       | I mean, of course there is an obvious ownership issue with the
       | code above, how are the destructors supposed to be ran without
       | freeing the _Id_ object twice?
        
         | umanwizard wrote:
         | The whole point is that `Id` doesn't have a destructor (it's
         | purely stack-allocated); that is, conceptually it _could_ be
         | `Copy`.
         | 
         | A more precise way to phrase what he's getting at would be
         | something like "all types that _can_ implement `Copy` should do
         | so automatically unless you opt out", which is not a crazy
         | thing to want, but also not very important (the ergonomic
         | effect of this papercut is pretty close to zero).
        
           | littlestymaar wrote:
           | Ah I see.
           | 
           | > A more precise way to phrase what he's getting at would be
           | something like "all types that _can_ implement `Copy` should
           | do so automatically unless you opt out", which is not a crazy
           | thing to want,
           | 
           | From a memory safety PoV it's indeed entirely valid, but from
           | a programming logic standpoint it sounds like a net
           | regression. Rust's move semantics are such a bliss compared
           | to the hidden copies you have in Go (Go not having pointer
           | semantics by default is one of my biggest gripe with the
           | language).
        
             | umanwizard wrote:
             | I think you are misunderstanding what Copy means, and
             | perhaps confusing it with Clone. A type being Copy has no
             | effect on what machine code is emitted when you write "x =
             | y". In either case, it is moved by copying the bit pattern.
             | 
             | The only thing that changes if the type is Copy is that
             | after executing that line, you are still allowed to use y.
        
               | littlestymaar wrote:
               | I'm not misunderstanding. Ore confusing the two. _Copy:
               | Clone_.
               | 
               | Yes when an item is Copy-ed, you are still allowed to use
               | it, but it means that you now have two independent copies
               | of the same thing, and you may edit one, then use the
               | other, and be surprised that it hasn't been updated.
               | (When I briefly worked with Go, junior developers with
               | mostly JavaScript or Python experience would fall into
               | this trap _all the time_ ). And given that most languages
               | nowadays have pointer semantics, having default copy
               | types would lead to a very confusing situation: people
               | would need to learn about value semantics AND about move
               | semantics for objects with a destructor (including all
               | collections).
               | 
               | No thanks. Rust is already complex enough for beginners
               | to grasp.
        
               | umanwizard wrote:
               | Got it. Indeed, I misunderstood your point. I agree with
               | you now that you clarified.
        
           | aapoalas wrote:
           | Auto-deriving Copy would also mean that there needs to be an
           | escape-hatch: eg. Vec would auto-derive Copy.
        
             | umanwizard wrote:
             | Yes you would need an escape hatch, but your example is
             | wrong. Vec can't be Copy, because it has a destructor.
             | 
             | This program fails to compile:
             | #[derive(Clone, Copy)]       struct S;            impl Drop
             | for S {           fn drop(&mut self) {}       }
             | fn main() {}
        
               | aapoalas wrote:
               | Oh, good point yeah; I wasn't thinking of Drop clashing
               | with Copy, but just about the fields that make up a
               | `Vec`.
        
               | aapoalas wrote:
               | Actually; I'm not sure I'm wrong. If Copy was
               | automatically derived based on fields of a struct
               | (without the user explicitly asking for it with
               | `#[derive(Copy)]` that is, as the parent comment
               | suggested the OP is asking for), then your example S and
               | the std Vec would both automatically derive Copy. Then,
               | implementing Drop on them would become a compile error
               | that you would have to silence by using the escape hatch
               | to "un-derive" Copy from S/Vec.
               | 
               | So, whenever you wanted to implement Drop you'd need to
               | engage the escape hatch.
        
               | umanwizard wrote:
               | What I suggested OP was asking for was:
               | 
               | > all types that _can_ implement `Copy` should do so
               | automatically unless you opt out
               | 
               | , which was explicitly intended to exclude types with
               | destructors, not
               | 
               | > types should auto-derive `Copy` based purely on an
               | analysis of their fields.
        
               | tonyedgecombe wrote:
               | So if you have some struct that you use extensively
               | through an application and you need to extend it by
               | adding a vector you are stuck because the change would
               | need to touch so much code.
        
             | sapiogram wrote:
             | Copy is already banned for any type that directly or
             | indirectly contains a non-Copy type, and Vec contains a
             | `*const T`, which is not Copy.
        
               | umanwizard wrote:
               | _const T is Copy, actually.
               | 
               | https://doc.rust-
               | lang.org/std/primitive.pointer.html#impl-Co..._
        
       | forrestthewoods wrote:
       | One day I will write a blog post called "The Rust borrow checker
       | is overrated, kinda".
       | 
       | The borrow checker is certainly Rust's claim to fame. And a
       | critical reason why the language got popular and grew. But it's
       | probably not in my Top 10 favorite things about using Rust. And
       | if Rust as it exists today existed without the borrow checker
       | it'd be a great programming experience. Arguably even better than
       | with the borrow checker.
       | 
       | Rust's ergonomics, standardized cargo build system, crates.io
       | ecosystem, and community community to good API design are
       | probably my favorite things about Rust.
       | 
       | The borrow checker is usually fine. But does require a staunch
       | commitment to RAII which is not fine. Rust is absolute garbage at
       | arenas. No bumpalo doesn't count. So Rust w/ borrow checker is
       | not _strictly_ better than C. A Rust without a borrow checker
       | would probably be strictly better than C and almost C++. Rust
       | generics are mostly good, and C++ templates are mostly bad, but I
       | do badly wish at times that Rust just had some damn template
       | notation.
        
         | lblume wrote:
         | > No bumpalo doesn't count.
         | 
         | Mind explaining why? I have made good experiences with bumpalo.
        
           | forrestthewoods wrote:
           | Everytime I try to use bumpalo I get frustrated, give up, and
           | fallback to RAII allocation bullshit.
           | 
           | My last attempt is I had a text file with a custom DSL.
           | Pretend it's JSON. I was parsing this into a collection of
           | nodes. I wanted to dump the file into an arena. And then have
           | all the nodes have &str living in and tied to the arena. I
           | wanted zero unnecessary copies. This is trivially safe code.
           | 
           | I'm sure it's possible. But it required an ungodly amount of
           | ugly lifetime 'a lifetime markers and I eventually hit a wall
           | where I simply could not get it to compile. It's been awhile
           | so I forget the details.
           | 
           | I love Rust. But you really really have to embrace the RAII
           | or your life is hell.
        
         | bobajeff wrote:
         | This is something I've been thinking about lately. I do think
         | memory safety is an important trait that rust has over c and
         | other languages with manual memory management. However, I think
         | Rust also has other attractive features that those older
         | languages don't have:
         | 
         | * a very nice package manager
         | 
         | * Libraries written in it tend to be more modular and
         | composable.
         | 
         | * You can more confidently compile projects without worrying
         | too much about system differences or dependencies.
         | 
         | I think this is because:
         | 
         | * It came out during the Internet era.
         | 
         | * It's partially to do with how cargo by default encourages
         | more use of existing libraries rather than reinventing the
         | wheel or using custom/vendored forks of them.
         | 
         | * It doesn't have dynamic linking unless you use FFI. So rust
         | can still run into issues here but only when depending on non-
         | rust libraries.
        
           | forrestthewoods wrote:
           | Agree on all points
        
       | int_19h wrote:
       | 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.
        
         | dejawu wrote:
         | I've similarly thought about building a language that compiles
         | to Rust, but handles everything around references and borrowing
         | and abstracts that away from the user. Then you get a language
         | where you don't have to think about memory at all, but the
         | resulting code "should" still be fairly fast because Rust is
         | fast (kind of ending up in the same place as Go).
         | 
         | I haven't written a ton of Rust so maybe my assumptions of
         | what's possible are wrong, but it is an idea I've come back to
         | a few times.
        
           | vlovich123 wrote:
           | Why compile to Rust for this? Many people that build
           | transpilation languages target C directly.
        
             | nine_k wrote:
             | A C compiler won't complain if your _generated_ code does
             | certain horrible things.
        
             | lblume wrote:
             | Think of Rust as a kind of kernel guaranteeing correctness
             | of your program, the rules of which your transpiler should
             | not have to reimplement. This may be compared to how proof
             | assistants are able to implement all sorts of complicated
             | simplification and resolution techniques while not
             | endangering correctness of the generated proofs at all due
             | to them having a small kernel that implements all of
             | verification, and as long as that kernel is satisfied with
             | your chain of reasoning, the processes behind its
             | generation can be entirely disregarded.
        
           | nine_k wrote:
           | Why, macros that put Arc<Box<T>> everywhere might just be it.
        
             | lblume wrote:
             | Arc<Box<T>> is redundant, for the contents of the Arc are
             | already stored on the heap. You may be thinking of
             | Arc<Mutex<T>> for multithreaded access or Rc<RefCell<T>>
             | for singlethreaded access. Both enable the same "feature"
             | of moving the compile-time borrow checking to runtime
             | (Mutex/RefCell) and using reference-counting instead of
             | direct ownership (Arc/Rc).
        
         | norskeld wrote:
         | Very tangential, but I couldn't help but remember Crust [1].
         | This tsoding madlad even wrote a B compiler [2] using these...
         | rules. Or lack thereof?
         | 
         | [1]: https://github.com/tsoding/Crust
         | 
         | [2]: https://github.com/tsoding/b
        
       | jltsiren wrote:
       | 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.
        
         | timmytokyo wrote:
         | If using indices is going to be your answer, then it seems to
         | me you should at least contend with the OP's argument that this
         | approach violates the very reason the borrowchecker was
         | introduced in the first place.
         | 
         | From the post:
         | 
         | "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."
        
       | mirekrusin wrote:
       | 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).
       | 
       | [0] https://www.moonbitlang.com
        
       | int08h wrote:
       | > 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.
        
         | timmytokyo wrote:
         | You're right about C++. A fairer comparison would be to a
         | simpler garbage-collected language like Go.
        
       | Fraterkes wrote:
       | 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.
        
       | dhbradshaw wrote:
       | 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.
        
         | lblume wrote:
         | Someone should create a DAG of programming languages with edges
         | denoting contextual influence and changes in design and
         | philosophy, such that every time a PL is critized for a feature
         | (or lack thereof), the relevant alternatives exactly
         | considering this would be readily available. It could even have
         | a great interactive visualization.
        
       | _dain_ wrote:
       | _> 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?
        
         | aapoalas wrote:
         | If you start making assumptions based on indices, you can turn
         | logical errors into memory safety errors. ie. whenever you use
         | unsafe with the SAFETY comment above it mentioning an index,
         | you'd better be damn sure that index is valid.
         | 
         | This goes for not only unchecked indexing but also eg.
         | transmuting based on a checked index into a &[u8] or such. If
         | those indexes move in and out of your API and you do some kind
         | of GC on your arrays / vectors, then you might run into indices
         | being use-after-free and now those SAFETY comments that
         | previously felt pretty obvious, even trivial, may no longer be
         | quite so safe to be around of.
         | 
         | I've actually written about this previously w.r.t. the borrow
         | checker and implementing a GC system based on indices /
         | handles. My opinion was that unless you're putting in ironclad
         | lifetimes on your indices, all assumptions based on indices
         | must be always checked before use.
        
           | _dain_ wrote:
           | My comment was implicitly about safe Rust. Obviously if
           | you're using `unsafe`, you have to deal with unsafety ...
        
       | cibyr wrote:
       | 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.
        
       | Animats wrote:
       | One of his examples of a borrow checker excess:
       | 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.
       | 
       | [1] https://dl.acm.org/doi/10.1145/3540250.3549110
        
         | creata wrote:
         | > The trouble is that you've re-created dangling pointers
         | 
         | That's true, but as a runtime mitigation, adding a generational
         | counter (maybe only in debug builds) to allocations can catch
         | use-after-frees.
         | 
         | And at least it's less likely to be a security vulnerability,
         | unless you put sensitive information inside one of these
         | arrays.
        
       | isodev wrote:
       | 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.
        
       | aapoalas wrote:
       | 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.
        
         | creata wrote:
         | > just reach for a GC library for those cases
         | 
         | Imo a GC needs _some_ cooperation from the language
         | implementation, at least to find the rootset. Workarounds are
         | either inefficient or unergonomic. I guess inefficient GC is
         | fine in plenty of scenarios, though.
        
       | vineethy wrote:
       | 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.
        
       | ChadNauseam wrote:
       | > [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.
        
       ___________________________________________________________________
       (page generated 2025-07-19 23:00 UTC)