A pattern in these is code that compiles until you change a small thing. A closure that works until you capture a variable, or code that works in the main thread but not in a separate thread. Or works until you move the code into an if/else block.
My experience with Rust is like this: make a seemingly small change, it balloons into a compile error that requires large refactoring to appease the borrow checker and type system. I suppose if you repeat this enough you learn how to write code that Rust is happy with first-time. I think my brain just doesn't like Rust.
I’ll add that—having paid that upfront cost—I am happily reaping the rewards even when I write code in other languages. It turns out the way that Rust “wants” to be written is overall a pretty good way for you to organize the relationships between parts of a program. And even though the borrow checker isn’t there looking out for you in other languages, you can code as if it is!
I had a similar experience with Erlang/Elixir. The primary codebase I work with in $DAYJOB is C++ but structured very OTP-like with message passing and threads that can crash (via exceptions, we’re not catching defaults for example) and restart themselves.
Because of the way we’ve set up the message passing and by ensuring that we don’t share other memory between threads we’ve virtually eliminated most classes of concurrency bugs.
I'm fortunate enough not to have to often write code in other languages anymore, but my experience that writing code in ways that satisfies the compiler actually ends up being code I prefer anyhow. I was somewhat surprised at the first example because I haven't run into something like that, but it's also not really the style I would write that function personally (I'm not a big fan of repetitions like having `Some(x)` repeated both as a capture pattern and a return value), so on a whim I tried what would have been the way I'd write that function, and it doesn't trigger the same error:
I wouldn't have guessed that this happened to be a way around a compiler error that people might run into with other ways of writing it; it just genuinely feels like a cleaner way for me to implement a function like that.
As someone who is interested in getting more serious with Rust, could you explain the essence of how you should always approach organizing code in Rust as to minimize refactors as the code grows?
> It turns out the way that Rust “wants” to be written is overall a pretty good way for you to organize the relationships between parts of a program
That's what it promised not to do, though! Zero cost abstractions aren't zero cost when they force you into a particular design. Several of the cases in the linked article involve actual runtime and code size overhead vs. the obvious legacy/unchecked idioms.
Maybe I'm just brainwashed, but most of the time for me, these "forced refactors" are actually a good thing in the long run.
The thing is, you can almost always weasel your way around the borrow checker with some unsafe blocks and pointers. I tend to do so pretty regularly when prototyping. And then I'll often keep the weasel code around for longer than I should (as you do), and almost every time it causes a very subtle, hard-to-figure-out bug.
I think the problem isn't that the forced changes are bad, it's that they're lumpy. If you're doing incremental development, you want to be able to to quickly make a long sequence of small changes. If some of those changes randomly require you to turn your program inside-out, then incremental development becomes painful.
Some people say that after a while, they learn how to structure their program from the start so that these changes do not become necessary. But that is also partly giving up incremental development.
Rust definitely forces you to make more deliberate changes in your design. It took me about 6 months to get past hitting that regularly. Once you do get past it, rust is awesome though.
I genuinely don’t know where you’ve gotten the idea that you can “never change your mind” about anything.
I have changed my mind plenty of times about my Rust programs, both in design and implementation. And the language does a damn good job of holding my hand through the process. I have chosen to go through both huge API redesigns and large refactors of internals and had everything “just work”. It’s really nice.
If Rust were like you think it is, you’re right, it wouldn’t be enjoyable to use. Thankfully it is nothing like that.
My recommendation is that you do whatever you feel like with ownership when you first write the code, but then if something forces you to come back and change how ownership works, seriously consider switching to https://jacko.io/object_soup.html.
> make a seemingly small change, it balloons into a compile error that requires large refactoring to appease the borrow checker and type system
Same experience, but this is actually why I like Rust. In other languages, the same seemingly small change could result in runtime bugs or undefined behavior. After a little thought, it's always obvious that the Rust compiler is 100% correct - it's not a small change after all! And Rust helpfully guides me through its logic and won't let my mistake slide. Thanks!
Yo, everyone's interpreting parent's comment in the worst way possible: assuming they're trying to do unsound refactorings. There are plenty of places where a refactoring is fine, but the rust analyzer simply can't verify the change (async `FnOnce` for instance) gives up and forces the user to work around it.
I love Rust (comparatively) but yes, this is a thing, and it's bad.
Yeah, Rust-analyzer's palette of refactorings is woefully underpowered in comparison to other languages / tooling I've used (e.g. Resharper, IntelliJ). There's a pretty high complexity bar to implementing these too unfortunately. I say this as someone that has contributed to RA and who will contribute more in the future.
I don’t know anyone who has gotten Rust first time around. It’s a new paradigm of thinking, so take your time, experiment, and keep at it. Eventually it will just click and you’ll be back to having typos in syntax overtake borrow checker issues
This is because programming is not a work in a continuous solution space. Think in this way; you're almost guaranteed to introduce obvious bugs by randomly changing just a single bit/token. Assembler, compiler, stronger type system, etc etc all try to limit this by bringing a different view that is more coherent to human reasoning. But computation has an inherently emergent property which is hard to predict/prove at compile time (see Rice's theorem), so if you want safety guarantee by construction then this discreteness has to be much more visible.
> A pattern in these is code that compiles until you change a small thing.
I think that's a downstream result of the bigger problem with the borrow checker: nothing is actually specified. In most of the issues here, the changed "small thing" is a change in control flow that is (1) obviously correct to a human reader (or author) but (2) undetectable by the checker because of some quirk of its implementation.
Rust set out too lofty a goal: the borrow checker is supposed to be able to prove correct code correct, despite that being a mathematically undecidable problem. So it fails, inevitably. And worse, the community (this article too) regards those little glitches as "just bugs". So we're treated to an endless parade of updates and enhancements and new syntax trying to push the walls of the language out further into the infinite undecidable wilderness.
I've mostly given up on Rust at this point. I was always a skeptic, but... it's gone too far at this point, and the culture of "Just One More Syntax Rule" is too entrenched.
If you are trying overly hard to abstract things or work against that language, then yes, things can be difficult to refactor. Here's a few things I've found:
- Generics
- Too much Send + Sync
- Trying too hard to avoid cloning
- Writing code in an object oriented way instead of a data oriented way
Most of these have to do with optimizing too early. It's better to leave the more complex stuff to library authors or wait until your data model has settled.
This is the issue I see a certain type of new rustaceans struggle with. People get so used to being able to chuck references around without thinking about what might actually be happening at run-time. They don't realize that they can clone, and even clone more than what might "look good", and that it is super reasonable to intentionally make a clone, and still get incredibly acceptable performance.
"Writing code in an object oriented way instead of a data oriented way"
The enterprise OOP style code habits also seem to be a struggle for some but usually ends up really liberating people to think about what their application is actually doing instead of focusing on "what is the language to describe what we want it to do".
Yeah I think this becomes more true the closer your type system gets to "formal verification" type systems. It's essentially trying to prove some fact, and a single mistake anywhere means it will say no. The error messages also get worse the further along that scale you go (Prolog is infamous).
Not really unique to Rust though; I imagine you would have the same experience with e.g. Lean. I have a similar experience with a niche language I use that has dependent types. Kind of a puzzle almost.
It is more work, but you get lots of rewards in return (including less work overall in the long term). Ask me how much time I've spent debugging segfaults in C++ and Rust...
That’s not what OP is discussing. OP is discussing corner cases in Rust’s typesystem that would be sound if the typesystem were more sophisticated, but are rejected because Rust’s type analysis is insufficiently specific and rejects blanket classes of problems that have possible valid solutions, but would need deeper flow analysis, etc.
Lean is far more punishing even for simple imperative code. The following is rejected:
/- Return the array of forward differences between consecutive
elements of the input. Return the empty array if the input
is empty or a singleton.
-/
def diffs (numbers : Array Int) : Array Int := Id.run do
if size_ok : numbers.size > 1 then
let mut diffs := Array.mkEmpty (numbers.size - 1)
for index_range : i in [0:numbers.size - 2] do
diffs := diffs.push (numbers[i+1] - numbers[i])
return diffs
else
return #[]
And in C++, those changes would likely shoot yourself in the foot without warning. The borrow checker isn't some new weird thing, it's a reification of the rules you need to follow to not end up with obnoxious hard to debug memory/threading issues.
But yeah, as awesome as Rust is in many ways it's not really specialized to be a "default application programming language" as it is a systems language, or a language for thorny things that need to work, as opposed to "work most of the time".
C++ allows both more incorrect and correct programs. That's what can be a little frustrating about the BC. There are correct programs which the BC will block and that can feel somewhat limiting.
My big complaint about Rust's borrow checking is that back references need to be handled at compile time, somehow.
A common workaround is to put items in a Vec and pass indices around. This doesn't fix the problem. It just escapes lifetime management. Lifetime errors then turn into index errors, referencing the wrong object. I've seen this three times in Rust graphics libraries. Using this approach means writing a reliable storage allocator to allocate array slots. Ad-hoc storage allocators are often not very good.
I'm currently fixing some indexed table code like that in a library crate. It crashes about once an hour, and has been doing that for four years now. I found the bug, and now I have to come up with a conceptually sound fix, which turns out to be a sizable job. This is Not Fun.
Another workaround is Arc<Mutex<Thing>> everywhere. This can result in deadlocks and memory leaks due to circularity. Using strong links forward and weak links back works better, but there's a lot of reference counting going on. For the non-threaded case, Rc<RefCell<Thing>>, with .borrow() and .borrow_mut(), it looks possible to do that analysis at compile time. But that would take extensions to the borrow checker. The general idea is that if the scope of .borrow() results of the same object don't nest, they're safe. This requires looking down the call chain, which is often possible to do statically. Especially if .borrow() result scopes are made as small as possible. The main objection to this is that checking may have to be done after expanding generics, which Rust does not currently do. Also, it's not clear how to extend this to the Arc multi-threaded case.
Then there are unsafe approaches. The "I'm so cool I don't have to write safe code" crowd. Their code tends to be mentioned in bug reports.
> A common workaround is to put items in a Vec and pass indices around. This doesn't fix the problem. It just escapes lifetime management. Lifetime errors then turn into index errors, referencing the wrong object.
That people seriously are doing this is so depressing... if you build what amounts to a VM inside of a safe language so you can do unsafe things, you have at best undermined the point of the safe language and at worse disproved the safe language is sufficient.
This is a common pattern everywhere, not just in Rust. Indices, unlike pointers to elements, survive a vector reallocation or serialization to disk. IDs are used to reference items in an SQL database, etc.
Yes the "fake pointer" pattern is a key survival strategy. Another one I use often is the command pattern. You borrow a struct to grab some piece of data, based on it you want to modify some other piece of the struct, but you can't because you have that first immutable borrow still. So you return a command object that expresses the mutation you want, back up the call stack until you're free to acquire a mutable reference and execute the mutation as the command instructs. Very verbose to use frequently, but often good for overall structure for key elements.
Neat. It's still run-time checking. A good idea, though. The one-owner, N users case is common. The trick is checking that the users don't outlive the owner.
2. Being async is suffering: I think this is addressed by async closures, due to be stabilized in Rust 2024/Rust 1.85: https://rust.godbolt.org/z/9MWr6Y1Kz
4. Send checker is not control flow aware: There seems to be (somewhat) active work to address this? No idea if there are major roadblocks, though. https://github.com/rust-lang/rust/pull/128846
Case 1 is definitely addressed by the Polonius-related work. There's a post [1] on the official Rust blog from 2023 about that, and this post [2] from Niko Matsakis' blog in June 2024 mentions that they were making progress on it, though the timeline has stretched out.
(Side note)
That's odd, lobste.rs seems to be down for me, and has been like that for a couple of months now -- I literally cannot reach the site.
Is that actually just me??
EDIT: just tried some things, very weird stuff: curl works fine. Firefox works fine. But my usual browser, Brave, does not, and complains that "This site can't be reached (ERR_INVALID_RESPONSE)". Very very very weird, anyone else going through this?
One approach to solving item 1 is to think about the default as not being a separate key to the HashMap, but being a part of the value for that key, which allows you to model this a little more explicitly:
The article makes the 'default' key with push_str("-default"), and given that, your approach should work. But i think that's a placeholder, and a bit of an odd one - i think it's more likely to see something like (pardon my rusty Rust) k = if let Some((head, _)) = k.split_once("_") { head.to_owned() } else { k } - so for example a lookup for "es_MX" will default to "es". I don't think your approach helps there.
Yeah, true. But that (assuming you're saying give me es_MX if it exists otherwise es) has a similar possible solution. Model your Language and variants hierarchically rather than flat. So languages.get("es_MX") becomes
let language = languages.get_language("es");
let variant = language.get_variant("MX");
There's probably other more general ideas where this can't be fixed (but there's some internal changes to the rules mentioned in other parts of this thread somewhere on (here/reddit/lobsters).
I didn’t get past the first limitation before my brain started itching.
Wouldn’t the approach there be to avoid mutating the same string (and thus reborrowing Map) in the first place? I’m likely missing something from the use case but why wouldn’t this work?
// Construct the fallback key separately
let fallback = format!("{k}-default");
// Use or_else() to avoid a second explicit `if map.contains_key(...)`
map.get_mut(k)
.or_else(|| map.get_mut(&fallback))
I see how that helps with the usual case of inserting a value under the original key if it wasn't there, but I don't see how it helps in this case of checking a different key entirely if it wasn't there.
It definitely needs get_mut(k) changed to get_mut(&k), but even after doing that, it still fails to compile, with an error similar to the one the original code gets.
I am pretty sure the example 2 doesn't work because of the move and should be fixed in the next release when async closure are stabilized (I am soooo looking forward to that one).
The limitation is the borrow checker itself. I think it restricts too much. clang implements lifetimebound, for example, which is not viral all the way down and solves some typical use cases.
I find that relying on values and restricted references and when not able to do it, in smart pointers, is a good trade-off.
Namely, I find the borrow-checker too restrictive given there are alternatives, even if not zero cost in theory. After all, the 80/20 rule helps here also.
A borrow checker that isn't "viral all the way down" allows use-after-free bugs.
Pointers don't stop being dangling just because they're stashed in a deeply nested data structure or passed down in a way that [[lifetimebound]] misses. If a pointer has a lifetime limited to a fixed scope, that limit has to follow it everywhere.
The borrow checker is fine. I usually see novice Rust users create a "viral" mess for themselves by confusing Rust references with general-purpose pointers or reference types in GC languages.
The worst case of that mistake is putting temporary references in structs, like `struct Person<'a>`. This feature is incredibly misunderstood. I've heard people insist it is necessary for performance, even when their code actually returned an address of a local variable (which is a bug in C and C++ too).
People want to avoid copying, so they try to store data "by reference", but Rust's references don't do that! They exist to forbid storing data.
Rust has other reference types (smart pointers) like Box and Arc that exist to store by reference, and can be moved to avoid copying.
> Pointers don't stop being dangling just because they're stashed in a deeply nested data structure or passed down in a way that [[lifetimebound]] misses
This is the typical conversation where it is shown what Rust can do by shoehorning: if you want to borrow-borrow-borrow from this data structure and reference-reference-reference from this function, then you need me.
Yes, yes, I know. You can also litter programs with globals if you want. Just avoid those bad practices. FWIW, references break local reasoning in lots of scenarios. But if you really, really need that borrowing, limit it to the maximum and make good use of smart pointers when needed. And you will not have this problem.
It looks to me like Rust sometimes it is a language looking for problems to give you the solution. There are patterns that are just bad or not adviced in most of your code and hence, not a problem in practice. If you code by referencing everything, then Rust borrow-checker might be great. But your program will be a salad of references all around, which is bad in itself. And do not get me started in the refactorings you will need every time you change your mind about a reference deep somewhere. Bc Rust is great, yes, you can do that cool thing. But at what cost? Is it even worth?
I also see all the time people showing off the Send+Sync traits. Yes, very nice, very nice. Magic abilities. And what? I do my concurrent code by sharing as little as possible all the time. So the patterns of code where things can be messed up are quite localized.
Because of this, the borrow checker is basically something that gets a lot in the way but does not add a lot of value. It might have its value in hyper-restricted scenarios where you really need it, and I cannot think of a single scenario where that would be really mandatory and really useful for safety except probably async programming (for which you can do structured concurrency and async scopes still in C++ and I did it successfully myself).
So no, I would say the borrow checker is a solution looking for problems because it promotes programming styles that are not clean from the get go. And only in this style it is where the borrow checker shines actually.
Usually the places where the borrow checker is useful has alternative coding patterns or lifetime techniques and for the few ones where you really want something like that, probably the code spots are small and reviewable anyway.
Also, remember that Rust gives you safety from interfaces when you use libraries, except when not, bc it basically hides unsafe underneath and that makes it as dangerous as any C or C++ code (in theory). However, it should be easier to spot the problems which leads more safety in practice. But still, this is not guaranteed safety.
The borrow checker is a big toll in my opinion and it promotes ways of coding that are very unergonomic by default. I'd rather take something like Swift or even Hylo any day, if it ever reaches maturity.
That is your personal decision to make. I would say this, despite this article pointing out 4 issues with certain design patterns there is a lot of good commercial software being written in the language every single day.
My personal experience has been one where after I spent the time learning Rust I am now able to write correct code much faster than GC based languages. Its pretty close to replacing python for me these days. I am also very grateful to not deal with null/nil ever again, error handling is so convenient, simple 1 offs run super fast and don't need me to go back to fix perf/re-write, and my code is way easier to read.
To each their own, but I wouldn't let a niche technical articles sway you from considering Rust as something to learn, use, and enjoy.
My opinion about Rust is not leaning on this article actually. I might need to give it a bigger try to see how it feels but I am pretty sure I am not going to ever like so heavy lifetime annotations for a ton of reasons.
I think a subset of the borrow checking with almost no annotations and promoting other coding patterns would make me happier.
You usually pass around bigger types through the heap internally encapsulated in an object with RAII in C++, for example. I do not think this is low-perf per se.
The one I run into most frequently: Passing field A mutably, and field B immutably, from the same struct to a function. The naive fix is, unfortunately, a clone. There are usually other ways as well that result in verbosity.
The borrow checker is smart enough to track disjointed field borrows individually and detect that's fine, but if you have two methods that return borrows to a single field, there's no way of communicating to the compiler that it's not borrowing the entire struct. This is called "partial borrows", the syntax is not decided, and would likely only work on methods of the type itself and not traits (because trait analysis doesn't need to account for which impl you're looking at, and partial borrows would break that).
The solution today is to either change the logic to keep the disjointed access in one method, provide a method that returns a tuple of sub-borrows, have a method that takes the fields as arguments, or use internal mutability.
My experience with Rust is like this: make a seemingly small change, it balloons into a compile error that requires large refactoring to appease the borrow checker and type system. I suppose if you repeat this enough you learn how to write code that Rust is happy with first-time. I think my brain just doesn't like Rust.
I’ll add that—having paid that upfront cost—I am happily reaping the rewards even when I write code in other languages. It turns out the way that Rust “wants” to be written is overall a pretty good way for you to organize the relationships between parts of a program. And even though the borrow checker isn’t there looking out for you in other languages, you can code as if it is!
Because of the way we’ve set up the message passing and by ensuring that we don’t share other memory between threads we’ve virtually eliminated most classes of concurrency bugs.
That's what it promised not to do, though! Zero cost abstractions aren't zero cost when they force you into a particular design. Several of the cases in the linked article involve actual runtime and code size overhead vs. the obvious legacy/unchecked idioms.
That's like Stockholm Syndrome.
The thing is, you can almost always weasel your way around the borrow checker with some unsafe blocks and pointers. I tend to do so pretty regularly when prototyping. And then I'll often keep the weasel code around for longer than I should (as you do), and almost every time it causes a very subtle, hard-to-figure-out bug.
Some people say that after a while, they learn how to structure their program from the start so that these changes do not become necessary. But that is also partly giving up incremental development.
But this assumes that your specifications do not change.
Which we know couldn't be further from the truth in the real world.
Perhaps it's just me, but a language where you can never change your mind about something is __not__ a fun language.
Also, my manager won't accept it if I tell him that he can't change the specs.
Maybe Rust is not for me ...
I have changed my mind plenty of times about my Rust programs, both in design and implementation. And the language does a damn good job of holding my hand through the process. I have chosen to go through both huge API redesigns and large refactors of internals and had everything “just work”. It’s really nice.
If Rust were like you think it is, you’re right, it wouldn’t be enjoyable to use. Thankfully it is nothing like that.
Same experience, but this is actually why I like Rust. In other languages, the same seemingly small change could result in runtime bugs or undefined behavior. After a little thought, it's always obvious that the Rust compiler is 100% correct - it's not a small change after all! And Rust helpfully guides me through its logic and won't let my mistake slide. Thanks!
I love Rust (comparatively) but yes, this is a thing, and it's bad.
I think that's a downstream result of the bigger problem with the borrow checker: nothing is actually specified. In most of the issues here, the changed "small thing" is a change in control flow that is (1) obviously correct to a human reader (or author) but (2) undetectable by the checker because of some quirk of its implementation.
Rust set out too lofty a goal: the borrow checker is supposed to be able to prove correct code correct, despite that being a mathematically undecidable problem. So it fails, inevitably. And worse, the community (this article too) regards those little glitches as "just bugs". So we're treated to an endless parade of updates and enhancements and new syntax trying to push the walls of the language out further into the infinite undecidable wilderness.
I've mostly given up on Rust at this point. I was always a skeptic, but... it's gone too far at this point, and the culture of "Just One More Syntax Rule" is too entrenched.
- Generics
- Too much Send + Sync
- Trying too hard to avoid cloning
- Writing code in an object oriented way instead of a data oriented way
Most of these have to do with optimizing too early. It's better to leave the more complex stuff to library authors or wait until your data model has settled.
This is the issue I see a certain type of new rustaceans struggle with. People get so used to being able to chuck references around without thinking about what might actually be happening at run-time. They don't realize that they can clone, and even clone more than what might "look good", and that it is super reasonable to intentionally make a clone, and still get incredibly acceptable performance.
"Writing code in an object oriented way instead of a data oriented way" The enterprise OOP style code habits also seem to be a struggle for some but usually ends up really liberating people to think about what their application is actually doing instead of focusing on "what is the language to describe what we want it to do".
Not really unique to Rust though; I imagine you would have the same experience with e.g. Lean. I have a similar experience with a niche language I use that has dependent types. Kind of a puzzle almost.
It is more work, but you get lots of rewards in return (including less work overall in the long term). Ask me how much time I've spent debugging segfaults in C++ and Rust...
Deleted Comment
But yeah, as awesome as Rust is in many ways it's not really specialized to be a "default application programming language" as it is a systems language, or a language for thorny things that need to work, as opposed to "work most of the time".
A common workaround is to put items in a Vec and pass indices around. This doesn't fix the problem. It just escapes lifetime management. Lifetime errors then turn into index errors, referencing the wrong object. I've seen this three times in Rust graphics libraries. Using this approach means writing a reliable storage allocator to allocate array slots. Ad-hoc storage allocators are often not very good.
I'm currently fixing some indexed table code like that in a library crate. It crashes about once an hour, and has been doing that for four years now. I found the bug, and now I have to come up with a conceptually sound fix, which turns out to be a sizable job. This is Not Fun.
Another workaround is Arc<Mutex<Thing>> everywhere. This can result in deadlocks and memory leaks due to circularity. Using strong links forward and weak links back works better, but there's a lot of reference counting going on. For the non-threaded case, Rc<RefCell<Thing>>, with .borrow() and .borrow_mut(), it looks possible to do that analysis at compile time. But that would take extensions to the borrow checker. The general idea is that if the scope of .borrow() results of the same object don't nest, they're safe. This requires looking down the call chain, which is often possible to do statically. Especially if .borrow() result scopes are made as small as possible. The main objection to this is that checking may have to be done after expanding generics, which Rust does not currently do. Also, it's not clear how to extend this to the Arc multi-threaded case.
Then there are unsafe approaches. The "I'm so cool I don't have to write safe code" crowd. Their code tends to be mentioned in bug reports.
That people seriously are doing this is so depressing... if you build what amounts to a VM inside of a safe language so you can do unsafe things, you have at best undermined the point of the safe language and at worse disproved the safe language is sufficient.
1. Checking does not take match and return into account: I think this should be addressed by Polonius? https://rust.godbolt.org/z/8axYEov6E
2. Being async is suffering: I think this is addressed by async closures, due to be stabilized in Rust 2024/Rust 1.85: https://rust.godbolt.org/z/9MWr6Y1Kz
3. FnMut does not allow reborrowing of captures: I think this is also addressed by async closures: https://rust.godbolt.org/z/351Kv3hWM
4. Send checker is not control flow aware: There seems to be (somewhat) active work to address this? No idea if there are major roadblocks, though. https://github.com/rust-lang/rust/pull/128846
[0]: https://lobste.rs/s/4mjnvk/four_limitations_rust_s_borrow_ch...
[1]: https://old.reddit.com/r/rust/comments/1hjo0ds/four_limitati...
[1]: https://blog.rust-lang.org/inside-rust/2023/10/06/polonius-u...
[2]: https://smallcultfollowing.com/babysteps/blog/2024/06/02/the...
Is that actually just me??
EDIT: just tried some things, very weird stuff: curl works fine. Firefox works fine. But my usual browser, Brave, does not, and complains that "This site can't be reached (ERR_INVALID_RESPONSE)". Very very very weird, anyone else going through this?
https://news.ycombinator.com/item?id=42353473
Dead Comment
Wouldn’t the approach there be to avoid mutating the same string (and thus reborrowing Map) in the first place? I’m likely missing something from the use case but why wouldn’t this work?
I find that relying on values and restricted references and when not able to do it, in smart pointers, is a good trade-off.
Namely, I find the borrow-checker too restrictive given there are alternatives, even if not zero cost in theory. After all, the 80/20 rule helps here also.
The borrow checker is fine. I usually see novice Rust users create a "viral" mess for themselves by confusing Rust references with general-purpose pointers or reference types in GC languages.
The worst case of that mistake is putting temporary references in structs, like `struct Person<'a>`. This feature is incredibly misunderstood. I've heard people insist it is necessary for performance, even when their code actually returned an address of a local variable (which is a bug in C and C++ too).
People want to avoid copying, so they try to store data "by reference", but Rust's references don't do that! They exist to forbid storing data. Rust has other reference types (smart pointers) like Box and Arc that exist to store by reference, and can be moved to avoid copying.
This is the typical conversation where it is shown what Rust can do by shoehorning: if you want to borrow-borrow-borrow from this data structure and reference-reference-reference from this function, then you need me.
Yes, yes, I know. You can also litter programs with globals if you want. Just avoid those bad practices. FWIW, references break local reasoning in lots of scenarios. But if you really, really need that borrowing, limit it to the maximum and make good use of smart pointers when needed. And you will not have this problem.
It looks to me like Rust sometimes it is a language looking for problems to give you the solution. There are patterns that are just bad or not adviced in most of your code and hence, not a problem in practice. If you code by referencing everything, then Rust borrow-checker might be great. But your program will be a salad of references all around, which is bad in itself. And do not get me started in the refactorings you will need every time you change your mind about a reference deep somewhere. Bc Rust is great, yes, you can do that cool thing. But at what cost? Is it even worth?
I also see all the time people showing off the Send+Sync traits. Yes, very nice, very nice. Magic abilities. And what? I do my concurrent code by sharing as little as possible all the time. So the patterns of code where things can be messed up are quite localized.
Because of this, the borrow checker is basically something that gets a lot in the way but does not add a lot of value. It might have its value in hyper-restricted scenarios where you really need it, and I cannot think of a single scenario where that would be really mandatory and really useful for safety except probably async programming (for which you can do structured concurrency and async scopes still in C++ and I did it successfully myself).
So no, I would say the borrow checker is a solution looking for problems because it promotes programming styles that are not clean from the get go. And only in this style it is where the borrow checker shines actually.
Usually the places where the borrow checker is useful has alternative coding patterns or lifetime techniques and for the few ones where you really want something like that, probably the code spots are small and reviewable anyway.
Also, remember that Rust gives you safety from interfaces when you use libraries, except when not, bc it basically hides unsafe underneath and that makes it as dangerous as any C or C++ code (in theory). However, it should be easier to spot the problems which leads more safety in practice. But still, this is not guaranteed safety.
The borrow checker is a big toll in my opinion and it promotes ways of coding that are very unergonomic by default. I'd rather take something like Swift or even Hylo any day, if it ever reaches maturity.
My personal experience has been one where after I spent the time learning Rust I am now able to write correct code much faster than GC based languages. Its pretty close to replacing python for me these days. I am also very grateful to not deal with null/nil ever again, error handling is so convenient, simple 1 offs run super fast and don't need me to go back to fix perf/re-write, and my code is way easier to read.
To each their own, but I wouldn't let a niche technical articles sway you from considering Rust as something to learn, use, and enjoy.
I think a subset of the borrow checking with almost no annotations and promoting other coding patterns would make me happier.
I wish this not-a-proposal turned into a real proposal:
https://smallcultfollowing.com/babysteps//blog/2021/11/05/vi...
https://play.rust-lang.org/?version=stable&mode=debug&editio...
The solution today is to either change the logic to keep the disjointed access in one method, provide a method that returns a tuple of sub-borrows, have a method that takes the fields as arguments, or use internal mutability.