[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)