31 ms·
The borrowchecker is what I like the least about Rust
- airstrike 1y agoI'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.
- School-Cotton 1y agoPolonius 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.
- ameliaquining 1y agoThat's not true of the get_default example, which is fixable without fundamental type-system changes but requires Polonius.
- alilleybrinker 1y agoFor 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 1y ago> 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 1y agoSee Rust's golden rule: https://steveklabnik.com/writing/rusts-golden-rule https://steveklabnik.com/writing/rusts-golden-rule
- majormajor 1y agoThis 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 1y agoMany 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 1y ago> 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.
- airstrike 1y agoThe 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.
- School-Cotton 1y agoFearless concurrency is only possible because of the borrow checker.
- ameliaquining 1y agoThis isn't really right; Pony does static concurrency safety without borrow checking, and while Swift sort of has borrow checking it's mostly orthogonal to how static concurrency safety works there. The relationship between borrow checking and concurrency safety in Rust is closer to "they rhyme" than "they use the exact same mechanism".
- airstrike 1y agoThanks for all your clarifying comments. I appreciate them and have learned lots. If you have a blog, I'd love to read whatever you write.
- tialaramex 1y agoYou've provided reasons why other languages have other properties, but Rust's fearless concurrency requires borrow checking. The concurrency focused trait Send only makes sense because we have control over references, through the borrow checker. Thread #1 can give Thread #2 this Doodad because it no longer has any references to it, if you try to give it a Doodad you're still referring to, then the borrow checker will reject your code. The Send trait guarantees that this (giving the Doodad to a different thread) is an OK thing to do - but only if you don't have outstanding borrows so you won't be able to look at the Doodad once you give it to Thread #2
- deleted 1y ago[deleted]
- School-Cotton 1y agoIMO, 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 1y agoI 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 1y agoThe 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.
- IshKebab 1y agoThis 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 1y agoThere'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 1y ago> 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 1y agoI 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 1y agoWe 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.
- eigenspace 1y agoI 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 1y agoThis 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 1y agoHobby 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 1y agoRealistically 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.
- School-Cotton 1y agoThere is also Ahrefs.
- dismalaf 1y agoYou 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.
- Spivak 1y ago> 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 1y agoHave 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 1y agoYes, 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 1y ago> Yes, and you can choose to use any language with a garbage collector Uh, no thanks. > and get the same benefit. Not quite.
- creata 1y agoThat'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.
- nine_k 1y agoThe 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.
- olq 1y agoSkill issue
- deleted 1y ago[deleted]
- lblume 1y agoIt 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 1y agoI 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?
- School-Cotton 1y agoThe 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 1y agoAh 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).
- School-Cotton 1y agoI 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.
- forrestthewoods 1y agoOne 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 1y ago> No bumpalo doesn't count. Mind explaining why? I have made good experiences with bumpalo.
- forrestthewoods 1y agoEverytime 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.
- marcianx 1y agoTo make sure I understand correctly: did you want to read a `String` and have lots of references to slices within the same string without having to deal with lifetimes? If so, would another variant of `Rc<str>` which supports substrings that also update the same reference count have worked for you? Looking through crates.io, I see multiple libraries that seem to offer this functionality: [1]: https://crates.io/crates/arcstr https://crates.io/crates/arcstr [2]: https://crates.io/crates/imstr https://crates.io/crates/imstr
- int_19h 1y agoI'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 1y agoI'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 1y agoWhy compile to Rust for this? Many people that build transpilation languages target C directly.
- jltsiren 1y agoThis 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 1y agoIf 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."
- mmoskal 1y agoI think this is like unsafe - most of your code won’t have it, so you get the benefits of borrow checker (memory safety and race freedom) elsewhere.
- sundarurfriend 1y agoAn important saving grace that `unsafe` has is that it's local and clearly demarcated. If a core data structure of your program can be compared to `unsafe` and has to be manually managed for correctness, it's very valid to ask whether the hoops Rust makes you jump through are actually gaining you anything.
- mirekrusin 1y agoThe 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 https://www.moonbitlang.com
- int08h 1y ago> 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 1y agoYou're right about C++. A fairer comparison would be to a simpler garbage-collected language like Go.
- krupan 1y agoCame here to comment on the same thing. I've never been able to articulate this as well as the author did, and it is so true! Every programming language requires you to solve some puzzles that are just in the way of the real problems you are trying to solve, but some much more than others. People have compared Rust to C++ and others have argued that they really aren't alike, but I think it's in these puzzles that they are more alike than any other two languages. Even just reading rust code is a brain teaser for me! I think this is why C and Zig get compared too. They apparently have roughly the same level of "fun problems" to solve.
- Fraterkes 1y agoI 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 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 1y agoIf 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 1y agoSomeone 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.
- ameliaquining 1y agoI can't really get over Gleam's position that nobody really needs type-based polymorphism, real programmers write their own vtables by hand. (It also needs some kind of reflection-like thing, either compile-time or runtime, so that there can be an equivalent of Rust's Serde, but at least they admit that that needs doing.)
- _dain_ 1y ago>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 1y agoIf 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_ 1y agoMy comment was implicitly about safe Rust. Obviously if you're using `unsafe`, you have to deal with unsafety ...
- ameliaquining 1y agoDifferent meanings of "unsafety". A cool thing about Rust's references, when used idiomatically, is that not only are you guaranteed no memory corruption, you're also guaranteed no runtime panics. These are not "unsafe" by Rust's definition, but they're still a correctness violation and it's good to prevent them statically when you can. Indices don't give you this protection.
- cibyr 1y agoWhat'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.
- linkage 1y agoYou can use Zig, a faster, safer C with best-in-class metaprogramming for a systems-level language. It doesn't guarantee safety to the same extent as Rust but gets you 80% of the benefit with 20% of the pain.
- Animats 1y agoOne 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 https://dl.acm.org/doi/10.1145/3540250.3549110
- isodev 1y agoI 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 1y agoTo 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 1y ago> 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 1y agoThe 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.
- phire 1y agoYeah, the author's theoretical "Rust but with garbage collector" would gain a whole bunch of concurrency bugs. It wouldn't be Rust anymore, just c# with a more functional syntax. "Fearless concurrency" is one of the best things the borrow checker gives us, and I think a lot of people undervalue it.
- ChadNauseam 1y ago> [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.
- luckystarr 1y agoI believe it. I experienced this once, as I tried to have everything owned. Now I just clone around as if there's no tomorrow and tell myself I'll optimize later.
- Buttons840 1y agoI need to do this, but I get fixated on interesting but premature optimizations. Just clone everything, profile, and remove the clones that take significant time.
- ramon156 1y agoI guess I'm a bit confused how you can write rust professionally dor 3 years and never encounter this. When I started writing rust in ~2020/2021 i already had issues with the brorow checker. Maybe its an idiom you already picked up in OCaml and did it mostly right in rust too?
- 1y ago
- gunnarmorling 1y agoGC also has its downsides: - Marking and sweeping cause latency spikes which may be unacceptable if your program must have millisecond responsiveness. - GC happens intermittently, which means garbage accumulates until each collection, and so your program is overall less memory efficient. With modern concurrent collectors like Java's ZGC, that's not the case any longer. They show sub-millisecond pause times and run concurrently. The trade-off is a higher CPU utilization and thus reduced overall throughput, which if and when it is a problem can oftentimes be mitigated by scaling out to more compute nodes.
- qaq 1y agoRust is pretty good target for Claude Code and the like. I don't write much Rust but I have to say of the langs I used Claude Code with Rust experience was among the best. If default async runtime was not work stealing I prob would use Rust way more.
- amelius 1y agoI suspect that people like and choose Rust mostly because of its modern build and package system which are clear improvements over C++.
- don-bright 1y agoRegarding Indexes: "When the same borrowchecker makes references unworkable, their solution is to... recommend that I manually manage them, with zero safety and zero language support?!?" Language support: You can implement extension traits on an integer so you can do things like current_node.next(v) (like if you have an integer named 'current_node' which is an index into a vector v of nodes) and customize how your next() works. Also, I disagree there is 'zero safety', since the indexes are into a Rust vector, they are bounds checked by default when "dereferencing" the index into the vector (v[i]), and the checking is not that slow for vast majority of use cases. If you go out of bounds, Rust will panic and tell you exactly where it panicked. If panicking is a problem you could theoretically have custom deference code that does something more graceful than panic. But with using indexes there is no corruption of memory outside of the vector where you are keeping your data, in other words there isn't a buffer overflow attack that allows for machine instructions to be overwritten with data, which is where a huge amount of vulnerabilities and hacks have come from over the past few decades. That's what is meant by 'safety' in general. I know people stick in 'unsafe' to gain a few percent speed sometimes, but then it's unsafe rust by definition. I agree that unsafe rust is unsafe. Also you can do silly optimization tricks like if you need to perform a single operation on the entire collection of nodes, you can parallelize it easily by iterating thru the vector without having to iterate through the data structure using next/prev leaf/branch whatever.
- deleted 1y ago[deleted]
- noodletheworld 1y ago> If you go out of bounds, Rust will panic and tell you exactly where it panicked This arguement has a long history. It is a widely used pattern in rust. It is true that panics are memory safe, and there is nothing unsafe about having your own ref ids. However, I believe thats its both fair and widely acknowledged that in general this approach is prone to bugs that cause panics for exactly this reason, and thats bad. Just use Arc or Rc. Or, an existing crate that implements a wrapper around it. Its enormously unlikely that most applications need the performance of avoiding them, and very likely that if you are rolling your own, youll get caught up by edge cases. This is a prime example of a rust antipattern. You shouldnt be implementing it in your application code.
- pie_flavor 1y agoIf you notice a rule for the first time, restricting what you want to do and making you jump through a hoop, it can be hard to see what the rule is actually for. The thrust of the piece is 'there should not be so many rules, let me do whatever I want to do if the code would make sense to me'. This does not survive contact with (say) an Enterprise Java codebase filled with a billion cases of defensive strict immutability and defensive copies, because the language lacks Rust's rules and constructs about shared mutability and without them you have to use constant discipline to prevent bugs. `derive(Copy)` as One More Pointless Thing You Have To Do only makes sense if you haven't spent much time code-reviewing someone's C++. If you try to write Java in Rust, you will fail. Rust is no different in this regard from Haskell, but method syntax feels so friendly that it doesn't register that this is a language you genuinely have to learn instead of picking up the basics in a couple hours and immediately start implementing `increment_counter`-style interfaces. And this is an inexperienced take, no matter how eloquently it's written. You can see it immediately from the complaint about CS101 pointer-chasing graph structures, and apoplexy at the thought of index-based structures, when any serious graph should be written with an index-based adjacency list and writing your own nonintrusive collection types is pretty rare in normal code. Just Use Petgraph. A beginner is told to 'Just' use borrow-splitting functions, and this feels like a hoop to jump through. This is because it's not the real answer. The real answer is that once you have properly learned Rust, once your reflexes are procedural instead of object-oriented, you stop running into the problem altogether; you automatically architect code so it doesn't come up (as often). The article mentions this point and says 'nuh uh', but everyone saying it is personally at this level; 'intermittent' Rust usage is not really a good Learning Environment.
- someone_19 1y agoMy first thought when I was learning Rust was "Why don't they use a different operator for move?", something like: let a <= String::from("Hi"); let b <= a; let j = 0; // Copy let k = j; This may not be convenient, but it could be useful for educational purposes.
- steveklabnik 1y agoA long time ago, you would use the move keyword for this. The issue is that it’s just redundant, so it feels like unnecessary boilerplate.
- merksoftworks 1y agoI think the borrow checker doesn't get enough credit for supporting one of rusts other biggest selling points - it's ecosystem. In C and C++ libraries often go through pains to pass as little allocated memory over the API barrier as possible, communicating about lifetime constraints and ownership is flimsy and frequently causes crashes. In Rust if a function returns Vec<Foo> then every Foo is valid until dropped, and if it's not someone did something unsafe.
- andai 1y agoI used Rust for about an hour and immediately ran into an issue I found amusing. The compiler changed the type of my variable based on its usage. Usage in code I didn't write. There was no warning about this (even with clippy). The program crashed at runtime. I found this amusing because it doesn't happen in dynamic languages, and it doesn't happen in languages where you have to specify the types. But Rust, with its emphasis on safety, somehow lured me into this trap within the first 15 minutes of programming. I found it more amusing because in my other attempts at Rust, the compiler rejected my code constantly (which was valid and worked fine), but then also silently modified my program without warning to crash at runtime. I saw an article by the developers of the Flow language, which suffered from a similar issue until it was fixed. They called it Spooky Action at a Distance. This being said, I like Rust and its goals overall. I just wish it was a little more explicit with the types, and a little more configurable on the compiler strictness side. Many of its errors are actually just warnings, depending on your program. It feels disrespectful for a compiler to insist it knows better than the programmer, and to refuse to even compile the program.
- palata 1y agoI choose a language that is as ergonomic as possible, but as performant as necessary. If e.g. Kotlin is fine, there is no way I will choose Rust. Many projects are written in Rust that would absolutely be fine in Go, Swift or a JVM language. And I don't understand: it is nicer to write in those other languages, why choose Rust? On the other hand, Rust is a lot nicer than C/C++, so I see it as a valid alternative there: I'm a lot happier having to make the borrow-checker happy than tracking tricky memory errors in C.
- logdahl 1y ago> it is nicer to write in those other languages I think this is a matter of preference. Nowadays I cannot stand environments like Java (or especially Kotlin). "Tricky memory errors" is in my opinion nicer than a borrow-checker refusing sound code. I guess I really hate 'magic'...
- ameliaquining 1y agoI mostly agree with you, but ironically the article's own conclusion answers this: the language's general ethos is designed for correctness-oriented programming in the large, and this gives rise to lots of other features that garbage-collected languages could adopt but mostly don't. Like, it's way harder to avoid runtime panics in Go. (Swift does okay but people don't have faith that its custodians will do an adequate job of prioritizing the needs of developers using it for anything other than client-side apps on Apple platforms, and also it's slightly too low-level in that it doesn't have a tracing garbage collector either.)
- crackrook 1y agoPersonally I like my programs to be as performant as is reasonable, not just as is necessary. Rust's low-cost abstractions strike the right balance for me where I feel like I'm getting pretty solid ergonomics (most of the time) while also enjoying near-peak performance as the default. > It is nicer to write in those other languages, why choose Rust? Honestly I don't think it is nicer to write in those other languages you mention. I might still prefer Rust if performance was removed from the equation entirely. That is just to say I think preference and experience matters just as much, if not more, than the language's memory model.
- eldenring 1y ago> Borrowchecker frustration is like being brokenhearted - you can't easily demonstrate it, you have to suffer it yourself to understand what people are talking about. Real borrowchecker pain is not felt when your small, 20-line demonstration snippet fails to compile. As someone who writes Rust professionally this sentence is sus. Typically, the borrow checker is somewhere between 10th and 100th in the list with regards to things I think about when programming. At the end of the day, you could in theory just wrap something in a reference counter if needed, but even that hasn't happened to me yet.
- pornel 1y agoThere are some artificial limitations, but I love the upside: I don't need defensive programming! When my function gets an exclusive reference to an object, I know for sure that it won't be touched by the caller while I use it, but I can still mutate it freely. I never need to make deep copies of inputs defensively just in case the caller tries to keep a reference to somewhere in the object they've passed to my function. And conversely, as a user of libraries, I can look at an API of any function and know whether it will only temporarily look at its arguments (and I can then modify or destroy them without consequences), or whether it keeps them, or whether they're shared between the caller and the callee. All of this is especially important in multi-threaded code where a function holding on to a reference for too long, or mutating something unexpectedly, can cause painful-to-debug bugs. Once you know the limitations of the borrow checker, and how to work with or around them, it's not that hard. Dealing with a picky compiler is IMHO still preferable to dealing with mysterious bugs from unexpectedly-mutated state. In a way, borrow checker also makes interfaces simpler. The rules may be restrictive, but the same rules apply to everything everywhere. I can learn them once, and then know what to expect from every API using references. There are no exceptions in libraries that try to be clever. There are no exceptions for single-threaded programs. There are no exceptions for DLLs. There are no exceptions for programs built with -fpointers-go-sideways. It may be tricky like a game of chess, but I only need to consider the rules of the game, and not odd stuff like whether my opponent glued pieces to the chessboard.
- klauserc 1y agoYes! One of the worst bugs to debug in my entire career boiled down to a piece of Java mutating a HashSet that it received from another component. That other component had independently made the decision to cache these HashSet instances. Boom! Spooky failure scenarios where requests only start to fail if you previously made an unrelated request that happened to mutate the cached object. This is an example where ownership semantics would have prevented that bug. (references to the cached HashSets could have only been handed out as shared/immutable references; the mutation of the cached HashSet could not have happened). The ownership model is about much more than just memory safety. This is why I tell people: spending a weekend to learn rust will make you a better programmer in any language (because you will start thinking about proper ownership even in GC-ed languages).
- kmeisthax 1y ago[dead]
- YmiYugy 1y agoI think under the constrained of not using GC and not defaulting to unsafe memory that the borrow checker is a decent design. The constraints mean that you need some form of formal verification of your lifetimes. These can get very complex and difficult to use. So it might make sense to limit their expressiveness and instead rely on unsafe escape hatches. I think Rust's borrow checker provides a decent tradeoff between easy of use and how often unsafe is needed. There are rough edges and progress has been slow, but I have yet to see anything radically better. Other system PLs require a lot more unsafe usage (e.g. Zig) and I'm not aware of any mainstream system PLs that offer much more expressive verification.
- duped 1y ago> My examples code above may not be persuasive to experienced Rustaceans. They might argue that the snippets don't show there is any real ergonomic problem, because the solutions to make the snippets compile are completely trivial. In the last example, I could just derive Clone + Copy for Id. No, you could use destructuring. This doesn't work for all cases but it does for your examples without needing to derive copy or clone. Here's a more complex but also compelling example of the problem: struct Graph { nodes: BTreeMap<u32, Node>, } struct Node { edges: Vec<u32>, } impl Graph { fn visit_mut(&mut self, visit: impl Fn(&mut Node, &mut Node)) { let mut visited = BTreeSet::new(); let mut stack = vec![0]; while let Some(id) = stack.pop() { if !visited.insert(id) { continue; } let curr = self.nodes.get_mut(&id); for id in source.edges.clone() { let next = self.nodes.get_mut(&id); visit(curr, next); stack.push(id); } } } } We're doing everything in the "Rust" way here. We're using IDs instead of pointers. We're cloning a vec even if it's a bit excessive. But the bigger problem is we actually _do_ need to have multiple mutable references to two values owned by a collection that we know don't transitively reference the collection. We need to wrap these in an RefCell or UnsafeCell and unsafe { } block to actually get mutable references to the underlying data to correctly implement visit_mut(). This is a problem that shows up all the time when using collections, which Rust encourages within the ecosystem.
- ethan_smith 1y agoYour example has an undefined 'source' variable - you likely meant 'curr' - but more importantly, this pattern can be solved with indices + interior mutability or split_at_mut() rather than unsafe, giving you safe simultaneous mutable access to different nodes.
- duped 1y agoThat requires you use a Vec or similar data structure which may not be appropriate/forces you to reinvent garbage collection or deal with tombstoning/generations/etc for unused slots. And I pointed out you can use interior mutability. Still sucks because the code is guaranteed sound, the compiler just can't prove it. IMO the correct choice is UnsafeCell and unsafe {}.
- LunicLynx 1y agoRust is pain when you want to be super basic with your types. This is actually a learning lesson for the user to understand that the bugs one has seen in languages like c++ are inherent to using simple types. The author goes about mentioning python. If you do change all your types to python equivalents, ref counted etc. Rust becomes as easy. But you don’t want to do that and so it becomes pain, but pain with a gain. You must decide if that gain is worth it. From my point of view the issue is that rust defaults to be a system programming language. Meaning, simple types are written simple (i32, b32, mut ..), complex types are written complex (ref, arc, etc.). And because of that one wants to use the simple types, which makes the solutions complex. Let’s imagine a rust dialect, where every type without annotation is ref counted and if you want the simple type you would have to annotate your types, the situation would change. What one must realize is that verifiable correctness is hard , the simplicity of the given problematic examples is a clear indication of how close those screw ups are even with very simple code. And exactly why we are still seeing issues in core c libs after decades of fixing them.
- seivan 1y ago[dead]
- legobmw99 1y agoI was surprised to have to scroll this far to see someone bring up that the comparison point seems to be Python... I mean, I have absolutely no difficulty believing the statement > For every time I experience a bug in Python that would have been prevented in Rust by its borrowchecker, I experience maybe twenty borrowchecker issues. Now, if you replace "Python" with C or C++, my guess is the ratio at _least_ equalizes if not flips entirely
- yahoozoo 1y agoFor those who program a lot (daily) in Rust, how often do you run into borrow checker issues? Or are lifetime scopes second nature at this point?
- airstrike 1y agoVery rarely, but it depends on the domain and/or the part of the code I'm writing. For context, I mostly write GUI apps using `iced` which is inspired by Elm, so the separation between reading and writing state is front and center and makes it easy for me to avoid a whole set of issues.
- AquilaFasciata 1y agoI'm a hobbiest, so I only write in it 4 times a week, but I only run into borrow checker issues when I refactor large portions of existing code.
- someone_19 1y agoMaybe once or twice a month I'll get some language issue (like no negative trait bounds, restrictions on generic const, or something related to lifetime). I might spend an hour or more on each such case.
- e-dant 1y agoThe author does point out that these problems come up when implementing various data structures. It might be surprising to some folks, but there is a lot of unsafe code in Rust, and a lot of that is in the standard’s data structure implementations. Also — Common in network programming, the pain of lifetimes, is in async. The model sort of keels over and becomes obtuse when every task requires ownership of its data with static lifetimes.
- ordu 1y ago> Data races in multithreaded code are elegantly and statically prevented by Rust. I have never written a large concurrent Rust program, and I'll grant the possibility that the borrowchecker is miraculous for that use case and easily pays for its own clunkiness. Data races prevented not just in concurrent programs. I personally was hit once by a panic from RefCell. I have coded a data race, but I wouldn't notice it without RefCell. I borrowed the value and then called some function, and a several stack frames deeper I tried to borrow it mutably.
- pjmlp 1y agoThat is the whole point of using a language like Rust, otherwise use OCaml, Haskell, F#, Scala, Standard ML.
- conradludgate 1y agoI personally don't think I would want to use a language that doesnt have a borrow checker ever again. I would like a Rust-like language with GC, but I still want the borrow checker. Every time I write Go, I find it so annoying all the defensive deep copying I see. In JS I always find myself getting confused on whether a mutation is safe (as in my program won't break some assumption) to do or not. Marking arguments as shared or exclusive is really great for me to know what kind of access I can have. It needs to be enforced so that the owner also doesn't accidentally mutate while a shared borrow is still active (again, not just for memory safety but for my own invariants). The classic example could be inserting into a collection while iterating over it I think the borrow checker is necessary if you have ADTs like Rust and you want memory safety. You could pattern match a union into one of its variants and get a pointer to one of the fields, but without a borrow checker there's nothing to stop you from changing the variant stored in the union. This would obviously cause issues and the only way you'd solve this with GC alone is by allocating each variant individually where the union is just a tagged pointer.
- pluto_modadic 1y agoThe syntax is what I don't like. You can create some intensely cursed 3 line segments.
- pwdisswordfishz 1y agoThe point about inter-procedural borrow checking is fair -- and in fact not a matter of "sufficiently smart" borrow checker, but a rather fundamental type system limitation. But the flaw here is pretty manageable -- sometimes you can get away with calling .split_at_mut(...), sometimes you can borrow the field directly instead of going through a wrapper method, sometimes you have to be mindful to provide a split_at_mut equivalent yourself. Last I checked there was some work being done on "partial borrows", which could solve this. Most of the other criticisms are pretty disappointing, though. > The following example is a famous illustration of how it can't properly reason across branches, either: Except that function body would have been better rewritten as map.entry(key).or_default() -- which passes the borrow checker just fine and is more performant as it avoids multiple lookups. I suspect many other examples would benefit from being re-written into higher-level primitives that can be easier to borrow-check in this manner. > 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. What if Id represents a file descriptor that ought to be closed when the value ceases to exist? Or some other type of handle? This is an ownership problem: these are not limited to memory safety issues. For very good API stability reasons, without an explicit #[derive(Clone,Copy)] the compiler is not free to assume the type represents pure information that can be copied at will, but can only treat it as one potentially containing owned resources, that just happens not to include any at this time. > References to temporary values, e.g. values created in a closure, are forbidden even though it's obvious to a human that the solution is simply to extend the lifetime of the value to its use outside the closure. Which is to say, to what? The type signature does not say whether a closure will be called immediately or in another thread after two hours. And in which stack frame should the value be stored, the soon-to-be-exiting closure's?
- alfiedotwtf 1y agolol, it’s the one thing that keeps me coming back!
- kwar13 1y agoSomeone once told me "easy things are hard in rust, and hard things are easy". There's some truth to that and the author has highlighted borrowchecker as probably the best example of this.
- tmtvl 1y agoFunny that, because I really like the borrow checker. In my opinion Rust only has 2 problems: 1. The culture of licensing stuff under MIT by default rather than (A)GPL. 2. The syntax. I'd much rather have S-expressions.
- anonair 1y agoAllowing this code: struct Id(u32); fn main() { let id = Id(5); let mut v = vec![id]; println!("{}", id.0); } would break ‘zero cost abstractions’.
- amluto 1y ago> They might argue that the snippets don't show there is any real ergonomic problem, because the solutions to make the snippets compile are completely trivial. In the last example, I could just derive Clone + Copy for Id. I’ll bite a little bit. If I write: struct Thing(u32); Then I need to make a choice: is Thing a plain value that its holders can freely clone or is Thing an affine object that is gone when consumed? I think it’s great that Rust makes this explicit, even if it’s a tiny bit odd that one option is a default and the other option is rather verbose. If I could have a unicorn, too, I’d also like to be able to request “linear” behavior, i.e. disallow code that fails to consume the object. Sure, I suppose someone could invent a language where an LLM reads the type definitions and fills in the blanks as to what uses are valid, but this seems like a terrible idea. Or one could use a language without affine types (most languages, and even C++’s move feature entirely fails to enforce any sort of good behavior).
- jyounker 1y agoThe author's first example seems to undermine the thesis. Their Point class allows two separate locations to independently update the X and Y fields, leaving the object in an inconsistent state. It seems to me that this is exactly the sort of thing that Rust is intended to prevent, and it makes complete sense to reject the code.
- torginus 1y agoI'd argue the very reason Rust was created to get rid of the aliasing problem that's plaguing C-family languages, (meaning that there's always a chance 2 pointers refer to the same piece of memory, meaning all writes might potentially invalidate all variables). This isn't really solvable in C/C++, and its worked around with a bunch of hacks, which might be overly convervative at times, and at others, generates buggy code. Rust managed to fix this issue elegantly, but I'm wondering if the solution might be worse than the problem. It's essentially impossible to write a Rust program without relying on many of its escape hatches like RefCell and unsafe, that make the borrow checker go away. Essentially any program, that needs to communicate or store data in some sort of persistent structure, which is then accessed in some other part of the program, has multiple mutable references to said abstract central point, which is not allowed in Rust, without the aforementioned workarounds.
- airstrike 1y ago> It's essentially impossible to write a Rust program without relying on many of its escape hatches like RefCell and unsafe, that make the borrow checker go away. I get this may be hyperbole but it's also just factually incorrect. Not only that, why is this even a goal? RefCell isn't a wart. It is part of Rust and meant to be used where it makes sense.
- torginus 1y agoNot really hyperbole - I can't iamgine how one would build a fully borrow-checked app, that doesn't use these escape hatches (either directly or through a library). The only thing you could do maybe is to pass down everything that could possible be mutable as parameters everywhere, but that would not exactly be a nice way to program. And what's wrong with RefCell (and friends)? It's extra complexity, memory footprint, and runtime cost. Also you gave up on the ability of the compiler to check you program's correctness. Granted it's not much, but neither is std::shared_ptr or Swift's automatic reference counting. Once you go from zero overhead to some overhead, there's a ton of quality of life features you can offer to the programmer. If Rust's borrow checker was smart enough to allow multiple mutable borrows of the same variable, provided some conditions are met (like if you're borrowing struct S, you could borrow it's fields), it would eliminate the need for much of RefCell hacks, as well as solve the gripes in the article.
- jiyinyiyong 1y agoagain https://www.moonbitlang.com/ https://www.moonbitlang.com/
- Boxxed 1y ago> Use fewer references and copy data. Or: "Just clone". > This is generally good advice. Usually, extra allocations are fine, and the resulting performance degradation is not an issue. But it is a little strange that it allocations are encouraged in an otherwise performance-forcused language, not because the program logic demands it, but because the borrowchecker does. I often end up writing code that (seems) to do a million tiny clones. I've always been a little worried about fragmentation and such but it's never been that much of an issue -- I'm sure one day it will be. I've often wanted a dynamically scoped allocator for that reason.
- oconnor663 1y ago> It's perfectly clear to a human that this code is fine and doesn't have any actual ownership issues. I think there's another part of the story in a few of these examples that the author might be missing. Sometimes Rust doesn't want to infer/allow things because of the Halting Problem or whatever, but other times it's because that inference would amount to a compatibility hazard that the user might not've realized they were creating. "Types implement `Copy` automatically" is a good example of one of these. It would be an easy rule to implement (and it is in fact how `Send` and `Sync` and some other things work), but it would also mean that adding a non-Copy private field to a struct that didn't previously have one could break callers. I wouldn't call it a slam-dunk case for the rules being what they are -- and you could argue that it prioritizes the needs of library code over the needs of application code -- but anyway it's some context.
- AquilaFasciata 1y ago> it manages to combine speed and low-level control with a high level of bug-resistance, more commonly referred to as safety. This article didn't even make it 2 paragraphs before being incorrect. Safety isn't bug resistance - Safety is ensuring memory integrity. Bugs are still easy to write, they're just going to be logic bugs instead of use after frees, double frees, etc. The first example of a function and access to fields misses point as well. If you just wanted to edit a field locally, you can do that without references. If you're sharing the struct around, then you don't want more than one mutable reference at a time anyway.