A theme I’m noticing more frequently as Rust gains popularity is people trying to use Rust even though it doesn’t fit their preferred way of coding.
There is a learning curve for everyone when they pick up a new language: You have to learn how to structure your code in ways that work with the language, not in the ways you learned from previous languages. Some transitions are easier than others.
There should come a point where the new language clicks and you don’t feel like you’re always running into unexpected issues (like the author of this article is). I might have empathized more with this article in my first months with Rust, but it didn’t resonate much with me now. If you’re still relying on writing code and waiting for the borrow checker to identify problems every time, you’re not yet at the point where everything clicks.
The tougher conversation is that for some people, some languages may never fully agree with their preferred style of writing code. Rust is especially unforgiving for people who have a style that relies on writing something and seeing if it complies, rather than integrating a mental model of the language so you can work with it instead of against it.
In this case, if someone reaches a point where they’re so frustrated that they have to remember the basic rules of the language, why even force yourself to use that language? There are numerous other languages that have lower mental overhead, garbage collection, relaxed type rules, and so on in ways that match different personalities better. Forcing yourself to use a language you hate isn’t a good way to be productive.
> A theme I’m noticing more frequently as Rust gains popularity is people trying to use Rust even though it doesn’t fit their preferred way of coding.
I could've expressed the sentiment in this blog post back when I started playing with Rust ~2016. Instead, I ended up learning why I couldn't pass a mutable reference to a hashmap to a function I'd looked up via that hashmap (iterator invalidation lesson incoming!).
The kind of bug I was trying to add exists in many languages. We can only speak in general terms about the code the blog post is talking about, since we don't have it, but couching it in terms of "doesn’t fit their preferred way of coding" misses that the "preferred way of coding" for many people (me included) involved bugs I didn't even realise could exist.
> The kind of bug I was trying to add exists in many languages
Any example except C++? BTW, the closest thing possible in C# is modifying a collection from inside foreach loop which iterates over the collection. However, standard library collections throw “the collection was modified” exception when trying to continue enumerating after the change, i.e. easy to discover and fix.
This is exactly what the mutable map pointer was for, for the function to be able to modify the collection; C++ would result in potentially iterating garbage, C# it sounds like would throw an exception (and so show the design wouldn't work when I tried to test it), Python definitely didn't do a graceful thing when I tried it just now. And if I had a collection struct in C, I'm sure I could've done some horrible things with it when trying.
The best of those outcomes is C#, which would've shown me my design was bad when I ran it; that could be as early as the code was written if tests are written at every step. But it could be quite a bit later, after several other interacting parts get written, and you rely on what turns out to be an invalid assumption. For Rust, the compiler alerted me to the problem the moment I added the code.
FTR I ended up accumulating changes to the map during the loop, and only applying them after the loop had finished.
EDIT: Python did do something sensible: I didn't expect pop'ing an element from the list to echo the element popped in the REPL, and got a printed interleaved from front to back, which does makes sense.
Python defined the semantics of modifying lists while iterating over them, which is better than c/c++ just calling it undefined behavior, but I've basically never seen it not be a bug. Either I end up creating a copy of the list to iterate over, or figure out a way to defer the modifications, or write a new list. Imo the c# /Java behavior of detecting and throwing is probably the best option for non borrow checked languages.
Python will raise a runtime exception if you modify the dict you are iterating over. You can work around that by copying a snapshot of the whole thing or just the keys and iterating over that.
C++ will laugh in invalidated iterators.
Of course you can erase from the container you are iterating over, but you have to make sure to continue the iteration using the iterator returned from the erase function and avoid storing copies of .end()
I'm pretty sure most languages just make the reasonable assumption that you want to map to the object so it uses the pointer for hashing and not actually hash its value.
What did the Rust community expect to happen if they so strongly and often claimed that Rust is easy to learn and user-friendly?
And how did we get from that to “for some people, some languages may never fully agree with their preferred style of writing code”?
If a C++ programmer (ideal Rust learner) says four years in that the ergonomics of Rust are bad, then based on my own experience I will believe them.
I don’t write something to see if it compiles either, I design every single line of code. But with Rust (and a lesser extent C++) a lot of that design is for memory safety and not necessary for the algorithms.
Well there's your problem. Rust does look like a semi-colon language, that's intentional, but if that's all you understand you're probably going to struggle.
The "ideal Rust learner" would have an ML, such as Ocaml or F#, maybe some Lisp or a Scheme something like that, as well as a semi-colon language like C or Java not just the weird forced perspective from C++
One experiment I probably won't get to try is to teach Rust as First Language for computer science students. Some top universities (including Oxbridge) teach an ML as first language, but neither teaches Rust because of course Rust is (relatively) new. The idea is, if you teach Rust as first language your students don't have to un-learn things from other languages. You can teach the move assignment semantic not as a weird special case but as it it really is in Rust - the obvious default assignment semantic. I pass a Goose to a function, obviously the Goose is gone, I gave it to the function. Nobody is surprised that when they pass a joint they don't have the joint any more, so why are we surprised when we pass a String to a function and the String is gone now? And once we've seen this, the motivation for borrows (reference types) is obvious, often we don't want to give you the string, we just want to tell you about a string that's still ours. And so on.
> Well there's your problem. Rust does look like a semi-colon language, that's intentional, but if that's all you understand you're probably going to struggle.
This is a silly point to make. What makes a C++ programmer a C++ programmer is not an uncanny ability to find a semicolon on the keyboard. It's stuff like using low-level constructs, having a working mental model of how to manage resources at a low level down and how to pass and track ownership of resources across boundaries. This is not a syntax issue.
It's absurd. You have people claiming that Rust is the natural progression for C++ programmers because their skillsets, mental models, and application domain overlap, but here are you negating all that and try to portray it as a semicolon issue?
Rust is easy to learn and user friendly. But if you are stuck in your ways and insist on writing code exactly like you did in another language you will have a hard time. This is true for every language on earth. Rust will refuse to compile, whereas other languages give you much more freedom to not use them as they are meant to.
> Rust is easy to learn and user friendly. But if you are stuck in your ways (...)
It's high time that people in the Rust community such as you just stop with this act.
The Rust community itself already answered that they find "Rust too difficult to learn or learning will take too much time" as a concern to not use Rust. The community also flagged Rust being too difficult as one of the main reasons they stopped using Rust.
Anyone who has managed to become proficient in C++ is smart enough to be proficient in Rust. The only too difficult part is adapting to the rust way of doing things and some refuse to.
> Anyone who has managed to become proficient in C++ is smart enough to be proficient in Rust.
I'm not sure you're paying attention. The people who are saying Rust is too hard are the Rust community itself. They said so in Rusty's annual survey. The ones who participate in it are Rust users who feel strongly about the topic to participate in their governance.
It's Rust users who say Rust is too hard. There is no way around this fact.
You just seem to have missed the point the author was making. I will try to clarify it: when you find out that a decision you made early on didn't pan out and you're forced to change the lifetime of some data, this will incur major refactoring in Rust and that will cause you to lose a lot of time. It's nearly impossible to avoid mistakes like this, not because you don't know Rust enough, that's almost completely irrelevant... it's because you just can't predict the direction your design will go to after many iterations and changes in requirements, which are unavoidable in the real world.
Replying to that with "Rust is easy to learn" just makes it sound like you didn't even understand what you're trying to reply to.
Rust is not easy to learn. Stop saying that. It is hard. It is harder than C#, harder than Python, harder than Java, harder than PHP, harder than JS, Harder than TS.
Saying it is easy to learn is just delusional and does a disservice to the language. Rust has many advantages, but trying to get people learning the language by lying about why they should learn it is just dumb.
I have memorised the UB rules for C. Or rather, more accurately, I have memorised the subset of UB rules I need to memorise to be productive in the language and am very strict in sticking to only writing code which I know is well defined (and know my way around the C standard at a level where any obscure code I sometimes need to write can be verified to be well defined without too much hassle).
I think Rust may be difficult
But, if I forget something, or make a mistake, I'm screwed. Yes there's ubsan, there's tests, but ubsan and tests aren't guaranteed to work when ub is involved.
This is why I call C a minefield.
On that note, C++ has such an explosion of UB that I don't generally believe anyone who claims to know C++ because it seems to me to be almost infeasible to both learn all the rules, or at least the subset required to be productive, and then to write/modify code without getting lost.
With rust, the amount of rules I need to learn to understand rust's borrow checker is about the same or even less. And if I forget the rules, the borrow checker is there to back me up.
I still think that unless you need the performance, you should use a higher level language which hides this from you. It's genuinely easier to think about.
That being said, writing correct rust which is going to a: work as I intended and b: not have UB is much less mentally taxing, even when I have to reach for unsafe.
If you find it more taxing than writing C or C++ it's probably either because you haven't internalised the rules of the borrow checker, or because your C or C++ are riddled with various kinds of serious issues.
The ISO document for C has an appendix which lists all the known categories of Undefined Behaviour. It's not exactly a small list, but it's something you could memorize if you wanted to, like the list of all US interstates, where they start and where they end.
There has been a proposal to attempt this for C++ but IMO the progress on making such an appendix is slower than the rate of the change for the language, making it a never ending task. It was also expanded by the fact that on top of Undefined Behaviour C++ also explicitly has IFNDR, programs which it declares to be Ill-formed (ie they are not C++) but No Diagnostic is required (ie your compiler doesn't know that it's not C++). This is much worse than UB.
That's the appendix containing documented UB. The standard also explicitly states that any behaviour not explicitly defined by the standard is undefined meaning that there are things which aren't in that list. And I can confirm, there are things which you can do in C which are UB but which are not on that list.
This only makes sense if one wants to write a Phd on C++ UB and needs the exhaustive list.
For the rest of us, there’s cppreference, UBsan and quite a few books on writing correct C++ code. Of course, these will still not suffice to write 100% memory safe code, which is a pretty arbitrary goal that just happens to match what Rust offers and is pushed a lot by Rust advocates.
It’s a nice goal, but not everybody works on software that’s attacked all day every day.
Also, memory safety isn't the only "bug" - I'd even argue that the majority of "memory" issues in unsafe languages like C are actually the result of a logic error or mismatch of interface expectations, and a memory error is often the "first noticed failure". In the trivial example strcpy() examples people love to use, unexpectedly truncating a string often means the program has "failed" in it's intended task just as much as a segfault or other memory corruption.
I'm extremely positive on highlighting as many of these problems before it gets to the user's hands, even more so if it's as early as a compile step as in the borrow checker, but lets not delude ourselves that they are the only possible issue software has. Or that in many languages it's a tooling issue (or culture issue accepting that tooling...) rather than a fundamental language difference.
On a side node, with the prevalence of things WASM I feel some people are just redefining what "memory safety" is. Defining a block of memory and using offsets within that is just reinventing pointers, the runtime ensuring that any offsets are within that block just mirroring the MMU and process isolation. We should really be looking at why that isn't well used rather than just reimplementing a new version on top for "security", as if those reasons aren't really "technical" (IE poor isolation between "Trusted" and "Untrusted" data processing in separate processes due to it being "Easier") we need to ensure we don't just do the same things again, and if they are technical we can fix them.
> I still think that unless you need the performance, you should use a higher level language which hides this from you.
Exporting and consuming the full c abi with very little effort is also another huge thing in rust's favor. Languages have opted heavily for supporting calling into the c abi and being hosted by the c abi, so naturally support for rust on the same terms comes for free. There's even rust in linux now.
After reading the article, it’s clear the author approves of the fact Rust has these rules (and prefers it over C++). They’re highlighting the natural challenges that brings so future iterations or competitors can see what needs to be improved.
> Those tools can't reliably identify undefined behaviour.
I'm sorry, can you explain what leads you to believe your hypothetical scenario is an argument rejecting the use of static code analysis tools?
I mean, I'm stating the fact that there are many many tools out there that can pick up these problems. This is a known fact. You're saying that hypothetically perhaps they might not catch each and every single hypothetical case. So what?
They're a good idea, but not a substitute for knowing the rules. And they don't just miss theoretical cases, they miss problems in practice even when used rigourously.
> They're a good idea, but not a substitute for knowing the rules.
It's a good thing no one made that claim, then.
The whole point is that were seeing people in this thread making all sort of wild claims on how it's virtually impossible to catch these errors in C++ even though back in reality there are a myriad of static analysis and memory checker tools that do just that.
Your average developer also knows how to type in a space character but still it's a good idea to onboard linters and automatic code formatters.
You’re missing #3, which accounts for an absolutely enormous amount of loss:
3. The fact that an inappropriate write through a pointer results in behavior that is so undefined that it can lead to remote code execution and hence do literally anything.
No amount of additional specification can fix #3, and masochism cannot explain it.
One could mitigate #3 to some extent with techniques like control flow integrity or running in a strongly sandboxed environment.
There's nothing really you can do with out-of-bounds write in C except say that it can do "anything". This UB is unavoidable.
I'm talking more about the nonsense like "c++ + ++c". There's no reason but masochism to keep it undefined. Just pick one unambiguous option and codify it.
An example of #2 is stuff like signed overflow. There are only so many ways to handle it: wraparound, saturate, error out. So C should just document them and provide a way to detect which behavior is active (like it does with endianness).
It's someone disingenuous to purposefully ignore what is the most common kind of UB in C. It's also ultimately not a very useful dichotomy, especially because it misunderstands why behavior ends up being undefined. For example:
> I'm talking more about the nonsense like "c++ + ++c". There's no reason but masochism to keep it undefined. Just pick one unambiguous option and codify it.
It's because there's an underlying variance in what the compilers (and the hardware [1]) translated for expressions like that, and codifying any option would have broken several of them, which was anathema in the days of ANSI C standardization. (It's still pretty frowned upon, but "get one person to change behavior so that everybody gets a consistent standard" is something the committees are more willing to countenance nowadays).
> An example of #2 is stuff like signed overflow. There are only so many ways to handle it: wraparound, saturate, error out.
Funnily enough, none of the ways you mention turn out to be the way it's actually implemented in the compiler nowadays.
As for why UB actually exists, there are several reasons. Sometimes, it's essential because the underlying behavior is impossible to rationally specify (e.g., errant pointer dereferences, traps). Sometimes, it's because you have optimization hints where you don't want to constrain violation of those hints (e.g., restrict, noreturn). Sometimes, it's erroneous behavior that's hard to consistently diagnose (e.g., signed overflow). Sometimes, it's for explicit implementation-defined behavior, but for various reasons, the standard authors didn't think it could be implemented as unspecified or implementation-defined behavior.
[1] Remember, this is the days of CISC, and not the x86 only-very-barely-not-RISC kind of CISC, the heady days of CISC where things like "*p++ = --q" is a single instruction.
That's not missing, I think they left it out of the "most" criticism on purpose. A dangling pointer is one of the few really good cases for UB. (Though good arguments can be made to give the compiler less leeway in that situation.)
> The fact that an inappropriate write through a pointer results in behavior that is so undefined that it can lead to remote code execution
This is a strange way to look at it. You'd get remote code execution only if the result of writing through the pointer was exactly what you'd expect: that the value you tried to write was copied into the memory indexed by the pointer.
I think you’re missing the author’s point, but OTOH he undermined it himself by stating that learning the rules helps: because Rust requires that the ownership and relationships are encoded in the type system, it requires significant design changes when those relationships change.
Learning the rules only partly mitigates this, because sometimes one does exploratory programming and isn’t sure what the final types are or they just want to change something.
Rust thrives on over-specification which calcifies the APIs.
Anyway, just as the author’s allegedly holding Rust wrong, one could say that you’re holding C++ wrong - the right approach is to learn how to write correct code and then the exceptions.
Also accept and be at peace with the fact that your code will have some bugs. I don’t know why the average Rust developer is so obsessed with getting things perfect and no less with memory safety when the overall software quality is the way it is. I mean if someone’s researching the topic or works on Rust, sure, be the Stallman of memory correctness.
I think unless your code is guaranteed to never interact with any untrusted input it is nowadays an increasingly unacceptable compromise to just accept that your program might have serious flaws which can lead to remote code execution or worse.
Moreover, it becomes increasingly unpleasant and unworkable to deal with code which progressively gets more and more unreliable.
It's expected that if the complexity of a program grows, the state space that the program can occupy grows with it. But with UB you can run into by accident that state space seems to grow exponentially in comparison to a language like Rust.
If you are required to write code at that low level, I would not use anything other than something like rust.
If you are not required to write code at that level. There are many languages with much less uncertainty than C++ which are much more productive than either C++ or rust.
I think it's telling that whenever someone raises concerns about any element of Rust, no matter how constructively, they're always met with a wall of "you must not truly get the borrow checker," or "you're using Rust wrong," or "stop trying to write <C/C++/Java/etc> in Rust!", usually with zero evidence that that is in fact what is happening. There's never anything to improve on Rust, it's always user error / a skill issue. If there ever surfaces any audio of Linus Torvalds and Ken Thompson discussing the pros and cons of the borrow checker, I expect a sea of patronising anime avatars to show up, seeking to explain Rust's invention of the concept of ownership to them.
Rust is really nifty, but there are still (many) things that could be improved in Rust, and we'd all benefit from more competition in this space, including Rust! This is not a zero sum game.
Honestly, I also think many people just want a nice ML-like with a good packaging story, and just put up with the borrow checker to get friendly C-like syntax for the Option monad, sum types with exhaustive matching, etc. This is a use case that could very much benefit from a competitor with a more conventional memory model.
> I think it's telling that whenever someone raises concerns about any element of Rust, no matter how constructively (...)
I'm far from a Rust expert, but to me if someone is whining about how it is hard to track lifecycle rules of an object because they are passing it through long chains of function calls across all sorts of boundaries, what this tells me is that you're creating your own problems that you could avoid if you simply passed the object by value instead of by reference. I mean, if tracking life cycles is a problem then why not prevent it from being a problem? Not all code lives in the hot path. I'm sure your performance benchmarks can spare a copy somewhere.
> I mean, if tracking life cycles is a problem then why not prevent it from being a problem?
So you're suggesting that people should just wrap everything in Arc or make copies everywhere to avoid lifetimes? At that point why not just use Java/OCaml/Swift/your-favourite-GC-lang?
> So you're suggesting that people should just wrap everything in Arc (...)
You're the only one who managed to come up with this nonsense. No one else did, and clearly you did not pick that from what I wrote because I definitely did no wrote that.
you mean except the original poster often implying exactly that in their articles or the personal experience of the commenter that nearly always whenever they ran into borrow checker problems it was due to exactly that reason
> There's never anything to improve on Rust, it's always user error / a skill issue.
RFCs get accepted and implemented nearly every week, like in any language there is always a lot to improve
the problem is that the complains of such articles are often less about aspects where you can improve things but more about "the borrow checker is bad" on a level of detail which if you consider the borrow checker a fundamental component basically is "rust is fundamentally bad"
Yes, and that's why the comments usually are in some sense "you don't use the BC right", because they don't want a BC. And that's fine, but you can't blame Rust for this
No one thinks there's nothing to improve in Rust, there are lots of features it is missing, some of which are in nightly or on the roadmap. But the borrow checker and the concepts that underpin it are pretty fundamental to Rust and what separates it from other languages. If you like Rust except for the borrow checker, then I would think you don't really like Rust.
> If you like Rust except for the borrow checker, then I would think you don't really like Rust.
The sentence in my original post that this addresses is supportive of the emergence of an alternative language to Rust for people with this use case, so I think we're just agreeing. (Although I wouldn't go so far as to tell others what they do or do not like based on my own ideas of what is essential and what isn't.)
I am A-OK with someone not liking Rust. I do, but it’s still only my 3rd-most used language behind the Python and TypeScript I write at work.
It’s just that time after time I’ve heard people criticize Rust because they were, in fact, trying to write their pet language in Rust. It’s similar to how many complaints I’ve heard about Python because “it’s weakly typed”. What? Feel free not to like either of them, but make it for the right reasons, not because of a misunderstanding of how they work.
Now, the author of this post may be doing everything right and Rust just isn’t good at the things they want to use it for. The complaint about constantly bumping against the borrow checker leads me to wonder.
> It’s similar to how many complaints I’ve heard about Python because “it’s weakly typed”. What? Feel free not to like either of them, but make it for the right reasons, not because of a misunderstanding of how they work.
Are you sure you're not just being harsh to people whose grasp of CS vocab is weaker than yours? If someone tells me that Python is 'weakly typed', I translate it in my head to 'dynamically typed', and the rest of their complaint generally makes sense to me, in that the speaker presumably prefers static typing. Which is a valid opinion to hold, not necessarily the result of any misunderstanding.
Reasonably sure. If it’s clear they actually mean dynamically typed, fine. That’s down to preference, and I won’t say they’re wrong any more than I’ll argue that chocolate is better than strawberry.
However, I’ve heard lots of utterly wrong criticisms of Python (and Rust and…) that were based on factual misunderstandings and not just a vocabulary mistake.
> whenever someone raises concerns about any element of Rust, no matter how constructively,
I don't think that it is a very constructive article. The author's critique of Rust raises questions like "how to do it better" but there are not answers.
> they're always met with a wall of "you must not truly get the borrow checker,"
Yeah, it is frustrating in this case particularly. The author openly states that he doesn't want to learn all the quirks of the borrow checker, and people respond to it with "you just don't get the borrow checker". I can see how this answer could be helpful, but if it was expanded constructively, if there was an explanation how it can become easy to deal with problems the author faces if you understood the borrow checker. OTOH I cannot see how such an argument can be constructed without a real example with the real code and the history of failed changes to it.
I personally feel, that the borrow checker is simple, if you got it. And the author's struggles just go away, if you got it. You can easily predict what will happen if you try this or that changes to the code, and you know how to do something so the borrow checker will be happy. But I cannot elaborate and to make it clear how it works.
I'd characterise it as a gentle criticism of the way the Rust community tends to react to anything other than effusive praise.
Rust is a nifty language, albeit with room for improvement, that falls into the (sadly overpopulated) category of 'neat thing, somewhat obnoxious fan club'.
Rust was a pain in the ass until I stopped trying to write C code in it and started writing idiomatic Rust. I don’t know the author of this blog, but he mentions extensive C++ experience which makes me wonder if he’s trying to write C++ in Rust.
Maybe not! Maybe it’s truly just Rust being stubborn and difficult. However, it’s such an easy trap to fall into that I’ve gotta think it’s at least possible.
> Rust was a pain in the ass until I stopped trying to write C code in it and started writing idiomatic Rust.
This is the #1 problem I see with people trying to learn a new language (not just Rust).
I’ve watched enough people try to adopt different languages at companies over the years that I now lean pessimistic by default about people adopting new languages. Many times it’s not that they can’t learn the new language, it’s that they really like doing things the way they learned in another language and they don’t want to give up those patterns.
Some people like working in a certain language and there’s nothing wrong with that. The problems come when they try to learn a new language without giving up the old ways.
Like you, I’m getting similar vibes from the article. The author wants to write Rust, but the entire premise of the article is about not wanting to learn the rules of Rust.
> The problems come when they try to learn a new language without giving up the old ways
In Python, I frequently see the same problem from the other side. Instead of C/C++ programmers learning Rust and "not wanting to learn the rules of Rust", it's Java/C# programmers learning Python and not wanting to unlearn the rules of Java/C#. They write three times as much code as they need to - introducing full class hierarchies where a few duck-typed functions would do.
> This is the #1 problem I see with people trying to learn a new language (not just Rust).
Definitely! I've also noticed people will learn a group of similar languages, like Java, C#/.Net, then Kotlin as the most distant relation. Now, they think they know many languages, but they mainly have the same core idea. So when they try something new like Haskell or Swift or Rust, they think it's doing something different from the "norm" in a really irritating way.
Trying to convince developers from "classical" OO that not everything needs to be a class in JavaScript has been a major thorn in my side for years. Your little procedure with no state? That can just be an exported function.
Oh boy. I see bugs everywhere in C and why the borrow checker exists.
It really forces you to understand what happens under the hood.
The most issues in Rust are indeed related the expressions - you don't know how to describe some scenario for compiler well-enough, in order to prove that this is actually possible - and then your program won't compile.
In C, you talk more to the computer with the language syntax, whereas in Rust you talk to the compiler.
> Oh boy. I see bugs everywhere in C and why the borrow checker exists.
Any examples that you could provide? I have been dealing with C/C++ for close to 30 years. Number of times I have shot myself with undefined/unspecified behavior is less than 5.
In 30+ years of experience in C, you haven't used a free()d variable or written past the end of a buffer more than 5 times? If that's true, then you have more care and attention than 99.99% of all C experts.
Of course, I have done such mistakes, but they were caught early in the dev. process. I am talking about bugs that were caught in production due to misunderstanding of C compilers on 16/32 bit processors.
I also avoid idioms like `*p` instead write `p[i]` whereever possible.
The number of times you shot yourself in the foot that you know about. Some of those bullets just haven't landed yet. C and C++ give you very interesting foot-guns: sometimes they go off even when you don't touch them (compiler upgrade, dependencies changing, building on a new architecture, ...)
The borrow checker isn't just about UB, it is mostly about memory safety.
I'm sure you've seen plenty of use-after-frees/use-after-move/dangling pointer type things or null pointer derefs, or data races, etc etc. These are largely impossible to do in safe rust.
Generally I have the easiest time when I declare my state in the outermost scope possible, and then pass it into functions that need to operate on it. If I'm using an actual pointer, rather than a mutable reference that came in as an argument, something weird is happening! Usually that's the interface with some external library.
Rust in particular is *really* obnoxiously bad at OOP patterns, and I think my lesson at this point is that this is because it is hard to do OOP safely, at least in a way that jives with its borrow checker. Something like functional core, imperative shell seems to be a much nicer flow for the thing in general.
Anyway, I've just got the one major Rust project (an NES emulator) so I'd say I'm pretty early in my Rust journey. For me personally, the good points (delightful match, powerful enum) outweigh the bad (occasional borrow checker weirdness, frustrating lifetimes) but I think it depends a lot on what you're trying to do with it.
You can achieve some level of OO design by using traits (the generic kind, not the dyn kind), but I think the functional style and inline testing gives you a ton of nice properties for free!
Rust also pushes you to refactor in a way that really pulls out the core of your problem; the refactoring is just you understanding the problem at a deeper level (in my experience)
Rust, like ocaml, is best when used purely functionally until you run into something that isn't performant unless its imperative. But unlike ocaml or haskell there is a safe imperative middle ground before going all the way to unsafe. People who write modern C++ with value semantics etc. seem to have a lot less trouble than people coming from Java.
I mean, I don’t write it that way, but if it works for you. I wouldn’t say you have to write it that way so I wouldn’t want to put anyone off.
Thinking about your answer a bit more, one of the paradigms of Rust is “there shall be many immutable references or just one mutable reference” and so I can see that functional programming would naturally lead to that. But it’s a paradigm that works with the underlying principles rather than the true nature of the language, IMHO.
I do it by thinking about different domains of object graphs, and how data moves between them, for example.
https://doc.rust-lang.org/book/ is great. I’d been writing Rust for months before I started reading it and still began learning new things from the start. Oh, that’s why it does this!
Edit: Oh! And use “cargo clippy” regularly. It makes excellent recommendations for how to make your code more idiomatic, with links to docs explaining why it’s nicer that way.
The borrow checker exists to force you to learn, rather than to let you skip learning.
To make an analogy, I think it would be weird if I complained that I had to "memorize the rules" of the type checker rather than learning how to use types as intended.
Fair enough, but the problem in this analogy is that this learning isn't always useful or productive in any way. This is more like doing arithmetic in a sort of maths notation where every result must be in base 12 and everything else must be in base 16. Sure, you can memorise the rules and the conversions but you aren't doing much useful with your life at that point.
Obviously, the borrow checker has uses in preventing a certain class of bugs, but it also destroys development velocity. Sometimes it's a good tradeoff (safety-critical systems, embedded, backends, etc.) and sometimes it's a square peg in a round hole (startups and gamedev where fast iteration is essential)
I think it destroys productivity if and only if you don’t roll with it and do things the Rusty way. If you write code with its idioms, it can be a huge productivity boost. Specifically, I can concentrate on fixing logic errors in my code instead of resource bookkeeping. When I refactor something, I know I didn’t accidentally forget to move alloc/free to the appropriate places for the new code: if my changes broke something, it’ll tell me.
Rust shouldn’t “destroy development velocity” once you’ve grasped the core concepts. There is some overhead to being explicit about how things are shared and kept, but that overhead diminishes with time as you internalize the rules.
Not if you're iterating and have to make fundamental changes. Just like certain advanced type systems, encoding too much at compile time means you have to change a lot of code in unnecessary mechanical ways when the design constraints change, or when you discover them.
This is not a bad thing by the way, it's an extremely plausible design chocie, and is one that Rust made very clearly: rejecting not-entirely-correct programs is more important than running the parts that do work. Languages that want to optimize for prototyping will make the opposite choice, and that's fine too.
Besides, if you still want to skip learning there are escape hatches like Rc<RefCell> but these hint pretty strongly (e.g. clones everywhere) that something might be wrong somewhere.
If the borrow checker only errd on code with bugs you could call it learning. Or if it was only possible to express correct programs in the Rust language. But such a thing isn't possible in general so we accept the weaker condition, accepting a subset of all valid code that can be proven correct. The usability of the language goes with how big that subset is, and the OP is expressing frustration at the size of Rust's.
Rust isn't alone in this, languages with type hints are currently going through the same thing where the type-checker can't express certain types of valid programs and have to be expanded.
Rust's reference topology is too restrictive. You can't have back references. This is what drives many C++ programmers nuts. It's common in C++ to have A point to B, and for B to have a pointer back to A. This happens implicitly with class inheritance, too. As a result, common C++ idioms don't translate to Rust at all.
This is fixable. Because you can have back references. You just have to use Rc, Rc::Weak, .upgrade(), RefCell, .borrow, and .borrow_mut(). This works, but only if the upgrades and borrows never fail. A failed .borrow() is a panic. The implication is that if you use .borrow() or .borrow_mut(), there's some good reason to think it will never fail.
For Rc::Weak, the key constraint is that all weak pointers must drop before all strong pointers have dropped. If you can prove that, .upgrade() doesn't need a run-time check.
For RefCell, the key constraint is that no .borrow() or .borrow_mut() may be enclosed by the scope of a conflicting .borrow() or .borrow_mut(). This requires a transitive closure check on who borrows what. For many simple cases, this is statically checkable. It does require inter-function checking.
Can those checks be moved to compile time? Probably. There's already a compile-time static Rc.[1]
Compile-time RefCell checking looks possible.[2] It's non-trivial to do this, but worth thinking about.
DARPA's TRACTOR project (Translating All C To Rust) is likely to generate vast amounts of Rc-heavy code,
if it works. So that provides some motivation for doing something to check at compile time.
I'm a relative beginner at Rust, but this matches my experience fairly well. Especially the part about the brittleness, where adding just one little thing can require propagating changes throughout a project. It might be adding lifetimes, or switching between values and references, or wrapping things in Rc or Arc or RefCell or Box or something. It seems hard to do Rust in a fully bottom-up fashion; you'll end up having to adjusting all the pieces repeatedly as you fit them together.
Maybe there's a style I haven't learned yet where you start out with Arc everywhere, or Rc, or Arc<Mutex<T>>, or whatever, and get everything working first then strip out as many wrappers as possible? It just feels wrong to go around hiding everything that's going on from the borrow checker and moving the errors to runtime. But maybe that's what you need to do to prototype things in Rust without a lot of pain? I don't know enough to know.
I have already noticed that building up the mindset of figuring out your ownership story, where your values are being moved to all the time, is addictive and contagious -- I'm sneaking more and more Rusty ways of doing things into my C++ code, and so far it feels beneficial.
> Maybe there's a style I haven't learned yet where you start out with Arc everywhere, or Rc, or Arc<Mutex<T>>, or whatever, and get everything working first then strip out as many wrappers as possible?
I wouldn't recommend that. It's easy to end up with a fundamentally flawed architecture impossible to refactor out of.
In general as long as you stick to keeping data ownership as high up in the call stack as possible everything should slowly fall into place.
Think functional core imperative shell.
Your main has services, dependencies, data, and just makes calls that operate on data without trying to make deeper owned objects that are inherently hard to keep references to.
Agreed. IMO, anywhere you butt heads with the borrow checker is a place where you’d have to by hyper nitpicky about user after free or memory leaks in C or C++, just without the compiler shouting at you to fix it.
This tallies with my experience with Rust. Four years ago I wrote an implementation of the TCL language in Rust (see https://github.com/wduquette/molt). It uses no unsafe code, and includes enough of the language to be useful. But it isn’t terribly efficient, and it’s a bit of a memory hog, and so I started looking at ways to improve it.
I usually like to evolve a code base towards a new architecture a little at a time, keeping it running and passing tests at every step of the way. What I found was that even seemingly small changes required an awful lot of work, as the OP says; if I could make them work at all. Eventually I decided that I’d learned what I’d needed to, and walked away from it. (To be fair, this was late spring or early summer of 2020, everything was peculiar, and I didn’t have the spare mental capacity for the project.)
I should add: I understand the need to use a language the way it wants to be used, and that you need to assimilate and internalize that to be truly fluent. I concluded that I didn’t need Rust’s extreme performance for the kind of work I do, and that there are less intrusive ways of getting memory safety.
> This means, to be a highly productive Rust programmer, you basically have to memorize the borrow checker rules, so you get it right the first time. This is stupid, because the whole point of having a type system or a borrow checker is to tell you when you get it wrong, so you don’t have to memorize how the borrow rules work.
This is completely back to front. Of course you have to internalise the rules of a borrow checker or type system to be highly productive. How can you hope to do a good job without that?
> Of course you have to internalise the rules of a borrow checker
This is generally a good thing: the more you internalise the logic of borrow checking, the earlier you start thinking about "who owns what" instead of deferring the choice to later, which often ends up in a tangled mess of "incidental data structures" as it is sometimes called in the c++ world [1].
Of course in c++ this means you have to internalise this discipline the hard way, i.e. without the borrow checker helping you.
Is it just me or is everyone in this comment talking past each other, with half of them not really understanding what the article is complaining about?
My take-away was that the article concludes that Rust is NOT a good tool for working with the borrow checker. I don’t think it said that there’s anything wrong with a borrow checker, and that you shouldn’t learn the basics of how it works or how to write idiomatic Rust.
A good tool shouldn’t require you to have a perfect memory of all the rules for you to be highly productive with it. If you make mistakes it should quickly tell you so with a message that quickly lets you figure out what to change.
I think this stands in contrast with Zig where these goals is the highest priority of the language. It’s also very strict with little to no undefined behaviour. But there’s also a lot of discipline in not introducing syntax or semantics that makes it hard for the compiler/checker to give a quick pass/fail with a clear message about went wrong. You can see from the issues in GitHub that improving error messages for failures in the type system is consistently prioritised. That puts a hard constraint on Zig where they’re held back from putting too much power into the type system.
That’s not to say that Rust doesn’t prioritise being a good tool. But the semantics of borrow checking makes their job an order of magnitude more difficult here. It’s an inherent trade-off. They’ve made a huge jump in the complexity and power of the language, and it’s probably much harder to then make a tool that makes it comfortable to work with this kind of power.
Some here may find it easy to deeply understand all the rules and to write code the first time that doesn’t trip up Rust too much. But in the real world code is written by many different kinds of people with different kinds and levels of intelligence.
I’ve found this to be an important consideration when choosing languages, libraries, tools and methodologies for large teams.
One way to be a 10x programmer is to write 10x as much code as an average programmer. Another way is to make 10 other average programmers twice as efficient, and that’s clearly more scalable.
Rust may still be a good choice to make the team more productive in the long run. My point is that adopting it in a team should perhaps not be considered a trivial decision.
> A good tool shouldn’t require you to have a perfect memory of all the rules for you to be highly productive with it. If you make mistakes it should quickly tell you so with a message that quickly lets you figure out what to change.
That's exactly what the Rust borrow checker does for me.
The borrow checker rules are quite simple conceptually.
If I own a book, I can read it, write in the margins or even destroy it. `let book: Book = Book{...}`.
I can lend this book to you exclusively `&mut Book`, you can read and write it, but not destroy it. And nobody else; including me; can even read it until you are done.
I can lend this book to you and others for reading `&Book`. We can all read it concurrently. And I must wait for everybody to be done before I can regain full ownership.
I can give you the book (passing by value). And it's now yours to do what you please. Including destroying it `drop(Book)`.
Sometimes you do want to share to many; and maybe even gate exclusive write access; at runtime. This is where Rc, Arc, Cell, RefCell and Mutex come in.
Rc and Arc destroy the book when a reference counter drops to zero. Another way to look at it, is that when the counter is 1, you have sole ownership of the book. And you can do with it what you please.
As for the runtime check for mutability, Mutex should be obvious. Cell and RefCell are similar but within a single threaded context.
And finally when you know better than the compiler, you use pointers (instead of references) and triple check your work within `unsafe` blocks.
> This is painful because I am an experienced C++ programmer, and C++ has this exact problem except worse: undefined behavior. In the worst case, C++ simply doesn’t check anything, compiles your code wrong, and then does inexplicable and impossible things at runtime for no discernable reason (or it just deletes your entire function).
This is completely wrong, even in the "not even wrong" territory. It reads like an attempt to parrot a cliche without having any idea what it means. "Undefined behavior" just means the standard does not define what is the expected behavior, and purposely leaves implementations free to implement it how they see fit. This means crashing the app or sending an email to the pope.
In practical terms this means developers should not write code that triggers undefined behavior, and treat the code that does as errors requiring a fix. Advanced users can lean on implementation-defined behavior from compilers to add some expectation to the behavior, but that's discouraged.
It's so strange how someone calling themselves a seasoned C++ developer fails to understand such a basic aspect of the language.
The important tidbit is that a) it's completely wrong to parrot "undefined behavior" on "C++ doesn't check anything", and b) if you code triggers undefined behavior without your knowledge then you just broke the code and wrote a bug out of your own ignorance.
To make matters worse, there are a myriad of code checkers for C++ that catch undefined behavior and even some classes of safety errors. Take for instance cppcheck. Why is the blogger whining about undefined behavior and "c++ not checking" when adding cppcheck to any project is enough to detect most if not all cases?
TFA argues developers add it left and right, unless someone memorised all the rules. Since reportedly it's not possible to memorise all the rules by a single human, then you either "simply add undefined behaviour", or limit yourself to a subset of C++ that programmers do actually understand. Which is a solution I see in many codebases: to limit a set of permissible constructs by a style manual.
I agree with this. However memorizing the borrow checker rules has led me to architect code “more correctly” upfront and consider things like ownership that I was otherwise pretty lax about ahead of time before. I think this has made me more thoughtful even in other languages which I think has been a win for me.
That said, the tedious refactors are a real pain. I think we all hoped that rustc would be smarter by now. It has gotten better but it isn’t there yet.
I don't really agree with this. I'd phrase it more as, you have to learn to really understand what the borrow checker is trying to do and how it makes you architect your programs and consider that ahead of time. Once you understand that, you'll rarely have problems with the borrow checker. It does preclude significant chunks of styles and data structures often used in other languages though.
> it makes you architect your programs and consider that ahead of time
This only works for projects which do not involve any R&D, but have a complete and well written functional specification written in advance. Also for projects which do a complete re-implementation of some pre-existing software.
For greenfield projects which require substantial amount of R&D, it’s impossible to architect programs ahead of time. At the start of the development, people only have a wishlist. Architecture comes later, after several prototypes implemented and evaluated, and people have some general understanding what does and doesn’t work, and what specifically needs to be done.
Rust implies that upfront architecture costs even for prototypes.
I don't really agree with that interpretation. In my opinion and experience, it does not restrict architectural choices to the extent that it makes it difficult to develop greenfield projects. It's more that it rules out a relatively small subset of architectural choices which are arguably a bad idea anyways, as they do infact have flaws that may not be obvious at first but will lead to a lot of pain if the project grows above a certain size.
Can't say I agree, or that this matches my experience of writing Rust.
I don't memorise how it works, I've just learnt what it rejects and why, and this in turn becomes clear as to why it's rejected that. Very rarely do I find myself going "oh bother, now I suddenly need to `Rc` or `Arc` this, I suspect because I've just gotten into the habit of suspecting when I anticipate things will run afoul and structuring things from the get-go to avoid that. Admittedly, I'm not writing absurdly low-level code.
I wonder if the authors grounding C++ is making life harder for them? Often when I've had to teach people Rust, getting them to stop writing {C/C#/Java}-but-in-Rust is the first stop on the trail to "stop fighting and actually enjoy the language". Every language has its idioms, just because you can, doesn't mean you should.
Great type system, great performance, great packages, great tooling, nice high-level API’s that produce low overhead code, I’m most familiar in Rust now.
I’ve had more than enough unpleasant experiences with write-runs-breaks-at-runtime style languages. I don’t like writing them, I hate having to support them in prod as they give me constant-persistent-stress. I hate what idiomatic C# is, and how much ceremony there is to read and write it. Java is worse. Go’s type system is too anaemic for my tastes. Haskell is nice, but gets a bit academic and lacks some day-to-day niceties. Kotlin is supposed to be nice, but again, we’re getting maybe 50% of Rust type system features, and you’re basically just piggybacking on Java/JVM which I hated dealing with previously. IDK what else that leaves in the mainstream. I used to play around with Nim, and that was quite nice though.
Expression-oriented, HM type inference with gradual typing, faster than other FP languages, can even reach for low-level bits, or write extra glue code in C# which is more pleasant at low-level imperative code.
Not sure what exactly you refer to as idiomatic C#. If it has too much ceremony chances are it’s anything but!
Yeah I’ve used F# before! It was pretty good, some solid features and nice experience.
It just falls into a bit of weird place IMO? You have to rely on writing/using C# to fill any holes, and I really dislike that language/ecosystem, and why split between 2 lands when I can just get the same HM type system, similar-enough principles, better perf and no MS taint.
Edit: I do love the ML style syntax though, Haskell, F#, Dhall are awesome, I wish it were more readily accepted.
The hatred of .NET (and C#) is unfortunate, irrational and unwarranted. I ended up unfortunately resorting to just thinking less of engineers that have it, because they can’t update their priors (“it was slightly inconvenient 8 years ago so it must be bad today surely”) and distinguish between Microsoft’s other products and policies and .NET itself.
This makes me further appreciate how golang's features tend to work entirely at compile time, which is also fast.
One of the other things that makes me worry about Rust is how similar it's depends look to npm projects, where there's a kitchen sink of third party (not the language's included library of code, and not the project's code) libraries pulled in for seemingly small utilities.
Dependencies are optional, and having a huge standard library also has its tradeoffs. If the standard library has a less than ideal API, it's stuck with that until a major version bump and you either:
1. End up with a third party package filling in the gaps, or
2. Another standard library API that users slowly migrate to
It's also a lot easier to release a new version of a package to fix a bug than do a bugfix release for the entire language toolchain, which is what would be needed in order to update the standard library. With Rust releasing a new minor version every six weeks, I think minimizing the chances of additional releases needed in between them is probably a good thing.
Unfortunately, in a world with increasingly more sophisticated attackers looking at supply chain attacks, having a lot of dependencies, especially ones that update regularly, is a huge security risk. For a language like Rust, which aims to be both low level and used in secure environments, I would argue that the risks far outweigh the benefits.
We'll see how this works, Rust is still young and not yet used in any hugely important projects (or at least not in hugely important parts of those projects - e.g. some Linux drivers, not the core kernel; some bits of Firefox'S rendering, not the JS engine). As it becomes more central, it's value as an attack target will increase, and people will start taking infiltrating malicious code in small but widely used dependencies.
I think it's the natural state of affairs for a "folk standard library" to emerge. I don't think pydantic or serde should be part of their standard libraries. But I will use them in most projects. In ten years, the "folk stdlib" will probably be a different set of packages (perhaps a superset, perhaps not). Don't push the river; if it's natural, manage it rather than fighting it.
Trying to anticipate all or even most use cases in the standard library is a fool's errand (unless we're talking about a DSL, of course). There are too many and they are too dynamic to be captured in the necessarily conservative release process of a language implementation. Languages should focus on being powerful and flexible enough to be adapted to a wide variety of use cases, and let the community of package maintainers handle the implementation. Think of this as a special case of the Unix philosophy; languages should do one thing very well, not a million things unevenly.
I bet most people here don't believe a command economy could ever work in a market for goods and services. Why should it work in a marketplace of ideas?
And new cars tailored for each consumer’s use case will emerge in ten years, too—that doesn’t make it any less awful to live in areas that lack good public transportation.
I'm not sure I understand the metaphor? Let me know if I'm off base.
If the suggestion is that putting things in the standard library makes them better, I disagree. My experience with Python for instance is that a "batteries included" strategy results in some phenomenal packages and some borderline abandoned packages that are actively dangerous to use.
To riff on your metaphor, the federal government designs the arterial highways, but the state, country, and city/town officials design the minutia of the traffic system. If the federal government had to approve spending on replacing some street signs or plowing snow, we would have a terribly impoverished transportation system.
Go’s features work only at compile time, but are far more limited. I experience more crashes in Go than in any other compiled language because of how limited it is as a language.
Two examples of things I want, built-in on day one, in some future language:
Structured concurrency. Don't provide "legacy" mechanisms and "opt in" for structure, just bite the bullet. Like the first language which told people no, we don't "go-to" other functions, that's not happening in my language, that was structured program flow I want structured concurrency, it's a thing but it's not yet popular enough to do that as the only provided concurrency, it should be.
Smart arithmetic. Your computer has Floating Point math. FP math is fast, but, it's hard for humans to think about exactly where they lose precision and performance while using it. I should be able to write the real mathematics I want, specify the precision I need and possibly the performance trades I'm comfortable with, and the compiler not me the programmer, figures out how to use FP math to calculate my mathematics with acceptable precision or tells me that I made demands it couldn't meet or which are nonsensical.
On the other hand, two things I really liked to see when I learned Rust:
&[T]: The slice reference type, a fat pointer which specifies where zero or more contiguous Ts are, and, how many of them. This is the Right Thing™ and it's right there in the core language design, which means you don't need to go back and retro-fit it.
String: The simplest possible way to build the growable text type, as a growable array of bytes but with the strict requirement that the array's content is always UTF-8 text. Is the "Small String Optimization" a cool trick? Yeah, but it need not live in this core vocabulary type. How about Copy-on-write ? Ditto. What about other text encodings? Transcode at the edges if you need that.
I sincerely believe that it is nearly impossible to have an objective and constructive conversation about the merits of programming languages, because the language of choice becomes part of people's worldview.
So it's like discussing politics or religion. People think that they have objective views, but they can't overcome their beliefs. That's just how beliefs work. They almost never change.
Also, beliefs are tied to groups. Humans automatically adopt the beliefs of their group, at least to some degree. Or they learn to shut up about their disagreements.
This is a thread for Rust critics and Rust advocates. Try to seriously sell F# or some other ML-like language in here and you are going to end up annoying both the C++ people and the Rust people.
The world will be a better place when the AIs finally take over. If we survive.
In my opinion, it seems like you may be taking random internet discussions a bit too seriously; I don't actually expect too much meaningful programming language discourse to occur in Hacker News comments. I think the reason why I keep coming here is in part because it's one of the rare public forums where occasionally some truly interesting discussions really do happen, but don't forget Sturgeon's Law. For better or worse, public and open forums are rarely productive places to have discussions, and a lot of the real innovations certainly seem to happen behind closed doors. (Personally I greatly prefer public forums for discussion, and even would prefer anonymity if it were feasible, but I take what I can get.)
What does that say about participating here? Well, for me, sometimes when I write a comment that I feel is constructive, reasonable, and honest, it goes gray anyways, and it's easy to chalk it up to people just irrationally downvoting it because they don't like my opinion. It's also pretty easy to do this, I just need to be cynical about Apple or optimistic about the Go programming language, or something similar, and there's some percentage chance it will go negative depending on presumably who sees it first. It's not going to stop me from doing so, and ultimately it's pretty inconsequential, as I'm just some guy and my opinions are not really that important anyways.
Somehow, even though I have all of this internalized, I can't help but go 30 nested replies deep into threads debating about something senseless and unimportant, but it almost feels like it wouldn't be the Internet without debates like that. XKCD 386.
I am currently learning Rust and found the post interesting. As I learn the language, I keep reading how Rust is great and you don't have to manage memory (unlike C or C++).
However, managing ownership and lifetime _is_ managing memory. The borrow checker is there, all the time, reminding you of memory management.
Now, in C and C++ the same problem exists but you don't have a borrow checker to remind you. I think this is the same conclusion the blog post came to, but I'm not entirely sure.
I've written a ton of software, both backend and embedded-like software in C++.
What are people writing that requires such fancy/extensive usage of the borrow checker?
I can't even remember the last time I had to use a shared_ptr... unique_ptr and other general RAII practices have been more than enough for our codebase.
Good algorithms (which don’t bottleneck on memory bandwidth) need multiple CPU cores to concurrently store different elements of the same output matrix. Moreover, the elements computed by different cores are not continuous slices, they are rectangular blocks. Such algorithm is not representable in safe rust.
In some ways, people who aren’t heavily invested in other languages to the point that they think in them. Neophytes know less, but they also have fewer things to unlearn.
Rust can be hard to get right because of the borrow checker.
I had a similar situation happen to me where I went about refactoring the code to make borrow checker happy ... until the last bit when things stopped compiling and I realized my approach was completely wrong (in the rust world, I had a self-reference in the structs)
Having said this, the benefits of borrow checker out weight the shortcomings. I can feel myself writing better code in other languages (I tend to think about the layout and the mutability and lifetimes upfront more now)
My rust code now is very functional, which seems to work best with lifetimes.
I would love to know more about the authors pain, I do hope rustc gets better at lifetime compilation errors cause some of them can be very very gnarly.
> I do hope rustc gets better at lifetime compilation errors cause some of them can be very very gnarly.
When this happens, file tickets! We do our best to improve diagnostics over time, but the best improvements have been reactive, by fixing a case that we never encountered but our users did.
will keep that in mind going forward! The most recent ones which I have been hitting are around "higher-ranked lifetime error"
I know my way around this now, which is to literally binary search over the timeline of my edits (commenting out code and then reintroducing it) to see what causes the compiler to trip over (there might be better ways to debug this, and I am all ears)
Most of the times this error is several layers deep in my application so even tho I want to ticket it up, not being able to create a minimal repo for anyone to iterate against feels like a bit of wasted energy on all sides, do let me know if I should change this way of thinking and I can promise myself to start being more proactive.
If it's public code, a link to a branch with the issue can still be useful. Looking at the compiler internals you can get a better sense on how to minimize the issue. That being said, not having a minimised repro lowers the chance of it getting addressed quickly.
Even if you have already figured out how to deal with it, your future colleagues might not, and by improving the diagnostic you would also be getting that time manually commenting code back.
People seriously need to stop obsessing about RC/ARC. Just use it, it will be fine. The perf difference wont matter in 99.99% of the cases. Whole languages (Swift) are based on that.
I habe been writing rust at work since 2016 or so and I can't say that the virtue checker ever had been much of a problem.
Like it's not that it never caused issues but most times fixing them also produces much better code.
In my experience the most common place to run into issues is if you write C/C++ style code in rust.
Or if you write certain kinds of functional style code in rust, rust has many functional features but isn't strictly speaking a functional language and while some functional pattern work well many other fall apart especially if combined with async (which will get better once async closure are stabilize).
in the end it often boils down to trying to use patterns and styles for other languages in a language which doesn't support them well, which always causes issues, but most times (in other languages) in more subtle ways then compiler errors, e.g. UB, perf, etc.
Through there is one field (game programming) where as long as your project doesn't become quite big you can get away with a lot of suboptimal approaches of state handling but not in rust. So if it comes to hobby from scratch state game programming I wouldn't be surprised for people to get annoyed (but if it's game programming using existing frameworks and e.g. stuff like a entire component library like bavy it's a different topic altogether)
Like many of these sorts of critique articles, I can see the author's pain point and empathize, but don't fully agree. Yes, it is true that if you aren't careful you can end up with a design that doesn't work and has to be redesigned. And yes, I do agree when this happens it can be very frustrating (and a time suck). However, in my experience, and it probably depends what type of code one writes, it doesn't happen enough to fully mar the experience of an otherwise very productive language. If I had to guess, I would say this happens to me maybe 1 day in 30. Not great, but not catastrophic either.
If I'm working on a section of code the relies heavily on borrowing and lifetimes, I will typically work up a prototype without all the functionality just to ensure I have a workable design before going back to fill in the rest of the code. This is probably why I don't tend to hit it all that often. It would be ideal if this wasn't necessary, but Rust has all sorts of other awesome features that make this something worth enduring.
It's interesting how Mojo solves some of Rust's lifetime UX issues. Because Mojo values uses ASAP destruction rather scope-based destructor, what Mojo's lifetime has to do is correctly track the last place a value was used, it doesn't track the validity of a scope.
What this means in practice is that Mojo's lifetime checker extends the life of values. Just point it at an origin and it'll ensure the origin is still alive wherever you use the value attached to it.
It completely defines away "value does not live long enough" compiler problems.
It is interesting that the borrow checker doesn't run until after typechecking succeeds. As far as I'm aware, rust-analyzer has its own builtin logic for doing typechecking, but it delegates to rustc for borrow checking. I wonder whether this is just a temporary situation due to lack of engineering resources to implement borrow checking in rust-analyzer; personally I doubt that, especially since gccrs is incorporating components of rustc wholesale and so I'd be a bit surprised if rust-analyzer moves in the opposite direction. In theory it seems possible to support borrow checking in the IDE for ill-typed programs, but having borrow checking as a separate analysis pass depending on successful typechecking is just such a nice abstraction boundary to have for maintaining the toolchain.
This is true but misleading: analogously, typechecking depends on parsing, but IDEs typically make a best effort to typecheck syntactically ill-formed programs.
rustc does its best to recover, continue and provide diagnostics from later stages. But at the same time it is better to provide a single early error and mark the entire node as recovered to avoid further errors at the cost of requiring more cycles of back and forth, over the alternative of tons of useless knock down errors that drown the underlying cause of the problem.
We are always on the lookout for improving in this area. Having examples of cases where we conceivably should have done better but didn't is useful. As mentioned already, the complexity here is that doing the right thing for the user requires architecting multiple separate stages of the compiler to talk to each other in way more complex ways than originally intended.
An often overlooked solution to this problem is to avoid using Rust, or to only use it for performance critical code. Writing a large application in Rust sounds hellish, it seems like it would be much nicer to only use it in sections of the hotpath where it is absolutely necessary.
That applies only if you're struggling with Rust. It's as good as any other general purpose programming language once you're out of the fight-the-borrow-checker stage. Structuring or refactoring large applications in Rust is nowhere as tedious as many project it. There are many zero-cost abstractions and other features that even makes it very pleasant.
My first preference for making simple utilities is as a shell script. The immediately next one is actually Rust.
Manually managing memory complicates the program, and makes it harder to change. Every interface written is contaminated with the implementation details of how it manages memory. This has a large cost in terms of development time. As much as you might want to imagine Rust has made every other programming language obsolete, that just isn't true.
Your assertion doesn't match my experience. Besides if that were true, nobody would be using C or C++ for writing general purpose (non-system) applications. C and C++ require the same system knowledge that Rust developers use to satisfy the borrow checker. Even worse, Rust borrow checker will remind you of those rules. C and C++ will just allow you to proceed and crash. C and C++ memory management is even more manually involved than Rust's. Yet people do write normal applications in C and C++.
The only explanation I can think of for the dislike towards Rust's compile time checks is that some people don't entirely understand these rules when they use C and C++. It's possible to resolve simple memory safety issues in C/C++ without in-depth knowledge of hardware semantics. But a complicate bug will easily stump you at runtime (personal experience).
This is exactly what I thought about rust when trying to learn it a few years ago. I'm also an experienced C++ programmer. After trying for 3-4 months and constantly fighting the borrow checker, I lost all motivation and gave up.
Rust borrow checker rules are a bit weird and unintuitive. But if you are a systems programmer (C or C++) and think a bit about the borrow checker complaints, you'll find that they almost always correspond to memory safety bugs like use-after-free or invalidated references. All you need to think is about what might happen if the code was accepted (this is for C/C++ programmers, since GC-based language programmers don't face those often). The same mistakes can happen in C and C++ too - but without the BC to back you up. In essence, there is no escape from those exact same rules.
There are a few genuine cases which the BC won't accept, though they may be valid. The first case is of data structures containing cycles (like dequeues, ring buffers, closed graphs, etc). The other is cross-FFI calls. This is due to the fact that the BC simply doesn't have the intelligence to reason about them (at compile-time). Even then, Rust gives you 2 types of escape hatches - runtime safety checks (using Rc, Cell, etc) with a slight performance impact, and manual safety checks (unsafe) when performance is paramount. All that's expected of you (the programmer) is to recognize such cases and use the appropriate solution.
I'm not too surprised when non-C/C++ programmers struggle with BC rules. They may be unfamiliar with the low-level execution semantics. But I'm surprised when C/C++ programmers fail to make the connection. I was a C++ programmer too and this is the first thing I noticed. Memorizing the BC rules is the absolute worst way to learn it. You should be looking for memory safety problems and correlating them with BC error messages instead. I know this works because I trained non-systems (non-C/C++, primarily JS and Python) developers in Rust. They picked up the execution and memory semantics quickly and easily made sense of the borrow checker idiosyncrasies.
This persons's problem is pretty clear - Rust is frankly miserable to write code in if you are trying to optimize everything as much as possible. Since this is the default mindspace of C/C++ programmers, the frustration is understandable.
Rust becomes a lot simpler when you borrow less and clone more. Sprinkle in smart pointers when appropriate. And the resulting program is probably still going to have fantastic performance - many developers err by spending weeks of developer time trying to shave off a few microseconds of runtime.
But, if you're a developer for whom those microseconds really do matter a lot, well then you just have to bite the bullet.
I think I come down somewhere close to this. Idiomatic safe Rust is noticeably slower and less scalable than the usual high-performance systems code many C++ developers write. This puts them in the position of either abandoning performance they know is possible or abandoning Rust code that is safe and sane, neither of which feels good.
Anecdotally, all of the happy productive Rust programmers I know do not come from a hardcore systems background. They were mostly Java and Python developers that wanted to get a bit closer to the metal. For them it is probably a great experience, and the performance is an improvement relative to what they are accustomed to.
There are quite a few shops that use Rust and C++ together, often wrapping a C++ core (for performance) with a Rust API layer (for safety).
That doesn't match my experience. C++ programmers typically use C++, because using libraries written in C++ from any other language is a miserable experience. It's rare to encounter C++ code with extensive low-level optimizations.
If you are used to C++11 or newer, you should be able to continue writing very similar code in Rust. The only major issue I encountered was the lack of the idea that "because objects A and B have effectively the same lifetime, they can safely store references to each other, as long as..." But if you are used to older versions of C++, trying to write similar code in Rust is going to be painful.
> Unfortunately, it can only catch undefined behavior that actually happens, so if your test suite doesn’t cover all your code branches you might have undefined behavior lurking in the code somewhere.
Covering every branch is not enough to say you have full coverage.
if( condition1 ) {
/* complicated things */
}
if( condition2 ) {
/* complicated things */
}
if( condition3 ) {
/* complicated things */
}
There are only three branches and you can "cover" them all with six tests. (Heck - you can cover them all with just two tests!) But there are eight paths through the code, and six tests can cover at most six of them.
If anyone's just starting out with Rust and struggling with the borrow checker, here are some tips. I have been using Rust since 2013 and have tried these ideas myself and while training others. (C/C++ devs may be familiar with some of these and may skip them.)
1. Give priority to the fundamental semantics of code execution - things like C memory layout [1], function calls, stack, stack frames, frame invalidation, heap, static data, multi-threading/programming, locks, synchronization, etc. Also learn associated problems like double-free, use-after-free, invalidated references, data races, etc. These are easiest to understand if you learn an assembly language. However, if you don't have the time or patience for that, at least focus hard on the hardware and memory layout topics in Rust books. If you prefer instead to learn those by making mistakes without the borrow checker intervening, start with C. (Aside: This knowledge is needed for C and C++ as well, especially C. You can't write large code without it.)
2. DON'T try to memorize the borrow checker. Borrow checker is based on a few simple principles (which you should know), but they can manifest in very complex and surprising ways (same as memory safety bugs). It's not practical to learn all such cases. Instead, check the borrow checker error message and see if you can correlate it to any memory safety problems I mentioned above. While the borrow checker can seem very complicated and arbitrary, it's designed solely to prevent those memory safety bugs. Pretty soon, you'll be comfortable with correlating BC errors to such bugs, without having to worry about how the BC found them. Knowing the real problem will also make it easy to satisfy the BC and avoid fighting with it.
3. Understand the memory layout of data structure that you use. Ref: [2]. Borrow checker errors often require this knowledge to make sense. The same becomes crucial during debugging if you make such mistakes in C/C++. The BC wont even allow you to compile it if you make similar mistakes in Rust. You need it just to get the program to run.
4. Borrow checker wont solve every problem for you. It doesn't have the intelligence to reason it all. There are a few notable cases:
- Data structures with cycles (like closed graphs, dequeues, etc) and algorithms that deal with them. (BC prefers data structures in a tree hierarchy)
- Function calls across an FFI boundary (since the BC can't check that code)
- Valid cases according to borrow checker rules, but rejected anyway due to complicated lifetime analysis. These may eventually get resolved in a later Rust version. But such cases exist.
5. Most of the BC errors can be solved by simple code refactors. But in cases like above, you need to identify them (as a limitation of the BC) and look for an alternative solution. BC is a compile-time safety checker. The alternatives are:
- Runtime safety checks using concepts like Rc, Arc, Cell, RefCell, etc. They will pass the BC checks at compile time. But if memory safety is violated at runtime, it will simply panic and crash. It may also have a slight performance hit due to runtime checks. But this is most often very negligible, given the fact that most other languages are based entirely on such checks (GC, ARC, etc). You don't need to be too shy in resorting to these to get around BC complaints. Many Rust programmers do.
- Manual safety checks using unsafe. If the performance is absolutely important for you, you can use unsafe functions and blocks. Unsafe keyword activates some potentially memory-unsafe language features (like raw pointer de-referencing) that the BC doesn't vet (Note: BC is not deactivated). This is often what you need when you're trying to implement an unavailable data structure or algorithm. This is also the only choice for FFI calls. Rust will not check them at any time. But this usually isn't a problem. Unsafe blocks are often used only for very fundamental ideas (eg: self-referential structs) and consist of no more than 5% of a codebase. If a memory safety bug does occur, it will be in one of those blocks and will be easier to locate and correct. Moreover people convert them to libraries and publish them on crates.io. This improves the chance of finding any hidden bugs. If you need unsafe code, there's probably a library for it already.
To get a better intro, check out 'Learn Rust With Entirely Too Many Linked Lists' [3]
People conflate difficulty of Rust borrow checker with the inherent difficulty in systems programming. However, this is not true. Both C and C++ are primarily systems programming languages. However, that never dissuaded anyone from using them as general purpose programming languages. The same should apply to Rust as well.
Meanwhile, Rust is my programming language and I choose it unless there is a good reason to choose another language. I never really struggled with the borrow checker. I think a lot of beginners approach the BC wrongly. Trying to memorize the rule is definitely the wrong way.
Sure. But pick based on concrete knowledge of the tool rather than broad and abstract categories. Tools are routinely discovered to be more broadly applicable than their initially intended use case, or even to be poor fit for that use case. The map is not the terrain.
And in any case, ecosystem usually trumps other concerns. Deep learning is a task you'd think a system language would be ideal for. Yet Python is a probably a better choice than Rust. The best reason to avoid Rust for a new project is probably that the market for Rust developers is insufficiently liquid (though presumably that won't be true for much longer, if it's still true at all).
As a long time Rust user, I more or less agree that it's a pain. And yeah, when working on a big project refactoring code, it's difficult to know ahead of time if the borrowing pattern you _think_ will work will actually work.
Of course, that's always the trade off with Rust. You're trading a _lot_ of time spent up-front for time saved in increments down the road.
As a concrete example, I'm in the process of building up a Rust database to replace the Postgres solution one of my applications is using. Partly because I'm a psycho, and partly because I've gotten query times down from 20 seconds on Postgres to 50ms with Rust (despite my best efforts to optimize Postgres).
Being a mostly ACID, async database, this involves some rather unpleasant interactions with the Rust borrow checker. I've had to refactor a significant portion of the code probably five times by now. The lack of feedback during the process is a huge pain point, as the article points out (though I'm not sure what the solution to that would be). Even if you _think_ you know the rules, you probably don't, and you're not going to find out until 2 hours later.
The second most painful point has to be the 'static lifetime, which comes up a lot when dealing with threading and async. For me it's when I need to use spawn_blocking inside an async function. Of course, the compiler has no way of knowing _when_ or _if_ spawn_blocking will finish, so it needs any borrows to be 'static. But in practice that means having to write all kinds of awkward workarounds in what should otherwise be simple code. I certainly understand the _why_, and I'm sure in X years it'll be fixed, but right now ... g'damn.
That said, the borrow checker _has_ improved. I think my last major Rust project was before the upgraded borrow checker, which wasn't able to infer lifetimes for local variables. So you had to throw a lot of stuff inside separate blocks. We also have a lot more elided lifetimes now. Just empirically from this project I'd say me and the borrow checker only had about 30% of the fisticuffs we did in the past.
Personally, I think the tradeoff is worth it. It won't be for everyone, or every project. But 20s to 50ms query time, with a ton of safety guarantees to ensure the valuable data running through the database is well cared for? Worth every line of refactored code.
* I also replaced some of my large JSON responses with FlatBuffers. FlatBuffers is a bit of a PITA, but when you're trying to shuffle 4 million integers over to the webapp, being able to do almost 0 decoding on the browser side, and get them as a Uint32Array directly is gold.
* It's a miracle I got away with the search parser in the project. I use Pest, and both the tree it spits out and the AST I build from it hold references. Yet sprinkling a little 'a on the impl's and struct's did the trick.
* Dynamic dispatch has also improved, as far as I can tell, which used to always involve some weird lifetimes if the return values needed to borrow stuff.
* ChatGPT o1 is a lot better at Rust then 4 or 4o. I've gotten a lot more useful stuff out of o1 this time around, including less hallucinations. Still weaker than Python/TypeScript/etc, with maybe 2-3 compile errors that need to be fixed each time. But still better. Sonnet completely failed every time I tried it :/ (Both through Copilot and the web). o1 in Copilot _could_ be amazing, since I can directly attach all my code. But the o1 in Copilot _feels_ weaker. I'm fairly sure the 4o Copilot uses is finetuned, and possibly smaller, so it too always felt weaker. Seems like o1 is the same deal. Still really useful for the Typescript side of things, but for Rust I had to farm out to the web and just copy paste the files each time.
Sounds familiar. I'm a relative beginner at Rust, but I've started feeling like I get the hang of the borrow checker rules and can get something close to right on my first writing. Finally. Ok, maybe 2nd or 3rd, but definitely before the dozenth!
Or at least I thought I did, until I launched into a project that mixes async and threading. That's where I hit a wall. What it's complaining about makes sense to me, but how to fix it does not -- partly because the async and threading come from libraries that I'm trying to stitch together. They necessarily have their own idiosyncratic ways of dealing with the issues, and as a beginner I don't even fully understand the problem they're solving let alone their solutions.
(For the record, I'm basically trying to force an unholy matrimony of matrix-sdk + tokio + pyo3 + pyo3-asyncio. You can take Python's GIL to grab out values to work with, but then if you want to do an async function then you'll have to release the GIL to stuff things into a future, and... well, if I fully understood the problem then I wouldn't be here whining about it, would I?)
> This is stupid, because the whole point of having a type system or a borrow checker is to tell you when you get it wrong, so you don’t have to memorize how the borrow rules work.
Sweet summer child. Use Haskell for a while and get back to me.
> This means that in order to write C++, you effectively have to memorize the undefined behavior rules, which sucks.
> This means, to be a highly productive Rust programmer, you basically have to memorize the borrow checker rules, so you get it right the first time. This is stupid, because the whole point of having a type system or a borrow checker is to tell you when you get it wrong
I'm not sure how you want to square this circle, you don't want to memorize the rules of UB, but you also don't want for compiler to correct you when you make UB behavior according to Borrow Checker?
The best way in both C++ and Rust is to structure your tree of lifetimes and use other means to achieve your desired goal.
They just don't want the borrow checker errors to only come after type checking. You have to know the rules well because otherwise you're going to finish a big refactor only to find out afterwards it was never going to work.
My opinion so far is that people want to write Java using Rust, or they want to write C++ using Rust, and they have a hard time. You can’t really just go write a program architected the way you do in those other languages. It’s not “C++ with a borrow checker”.
So I’d say it’s “worse” than “you have to memorize the borrow checker”. Its “you have to learn how to write programs in Rust”.
i’d consider myself a day-to-day c++ engineer. well, because i am. i like lots of things from rust. there’s a few things i don’t. c++ has a lot to learn from rust, if it is to continue to exist.
but really.. isn’t this the point of the language? you need to
understand the borrow checker because.. that’s why it’s here?
The overall point is it’s impossible to make a perfect spec. Rust doesn’t even have a spec, so your behavior is whatever the compiler gives you, which is C++ UB too.
Undefined behavior means something specific in the language specification world.
The language specification, or standard, guarantees certain things about the behavior of programs written to the specification. "Undefined behavior" means "you did something such that the guarantees of this specification no longer apply to your program." It's pretty much a worst case scenario in terms of writing programs. The program might do... anything. Fortunately, in reality it happens all the time and programs often keep behaving close enough to what we expect.
Turing completeness is unrelated to that sense of "undefined behavior".
> I understand and my point is rust has no “ub” only because there is no spec, not because it avoids inherent computing problems.
Well, your point is wrong because UB is not an inherent computing problem. That's what the post above tried to explain.
Many forms of UB are inherent to C-like languages, but languages don't have to be C-like.
> For example infinite template recursion is undefined. Specifying any other behavior is impossible due to halting problem.
A language can avoid this by not having infinite template recursion.
C++ currently allows infinite recursion at the language level, while acknowledging that compilers might abort early and recommending that 'early' is a depth of 1024 or higher. But a future version could bake that limit into the language itself, removing the problem.
> Another example: a system might be able to detect out of bounds pointer deref, or maybe not. Same with signed integer overflow.
A language can avoid out of bounds deref in many ways, one of which is not allowing pointer arithmetic.
Signed integer overflow is trivial to handle. I'm not sure what problem you're even suggesting here that the person in charge of the language spec can't overcome. C++'s lack of desire to remove that form of UB is not because it would be difficult.
A theme I’m noticing more frequently as Rust gains popularity is people trying to use Rust even though it doesn’t fit their preferred way of coding.
There is a learning curve for everyone when they pick up a new language: You have to learn how to structure your code in ways that work with the language, not in the ways you learned from previous languages. Some transitions are easier than others.
There should come a point where the new language clicks and you don’t feel like you’re always running into unexpected issues (like the author of this article is). I might have empathized more with this article in my first months with Rust, but it didn’t resonate much with me now. If you’re still relying on writing code and waiting for the borrow checker to identify problems every time, you’re not yet at the point where everything clicks.
The tougher conversation is that for some people, some languages may never fully agree with their preferred style of writing code. Rust is especially unforgiving for people who have a style that relies on writing something and seeing if it complies, rather than integrating a mental model of the language so you can work with it instead of against it.
In this case, if someone reaches a point where they’re so frustrated that they have to remember the basic rules of the language, why even force yourself to use that language? There are numerous other languages that have lower mental overhead, garbage collection, relaxed type rules, and so on in ways that match different personalities better. Forcing yourself to use a language you hate isn’t a good way to be productive.
> A theme I’m noticing more frequently as Rust gains popularity is people trying to use Rust even though it doesn’t fit their preferred way of coding.
I could've expressed the sentiment in this blog post back when I started playing with Rust ~2016. Instead, I ended up learning why I couldn't pass a mutable reference to a hashmap to a function I'd looked up via that hashmap (iterator invalidation lesson incoming!).
The kind of bug I was trying to add exists in many languages. We can only speak in general terms about the code the blog post is talking about, since we don't have it, but couching it in terms of "doesn’t fit their preferred way of coding" misses that the "preferred way of coding" for many people (me included) involved bugs I didn't even realise could exist.
> The kind of bug I was trying to add exists in many languages
Any example except C++? BTW, the closest thing possible in C# is modifying a collection from inside foreach loop which iterates over the collection. However, standard library collections throw “the collection was modified” exception when trying to continue enumerating after the change, i.e. easy to discover and fix.
> modifying a collection from inside foreach loop
This is exactly what the mutable map pointer was for, for the function to be able to modify the collection; C++ would result in potentially iterating garbage, C# it sounds like would throw an exception (and so show the design wouldn't work when I tried to test it), Python definitely didn't do a graceful thing when I tried it just now. And if I had a collection struct in C, I'm sure I could've done some horrible things with it when trying.
The best of those outcomes is C#, which would've shown me my design was bad when I ran it; that could be as early as the code was written if tests are written at every step. But it could be quite a bit later, after several other interacting parts get written, and you rely on what turns out to be an invalid assumption. For Rust, the compiler alerted me to the problem the moment I added the code.
FTR I ended up accumulating changes to the map during the loop, and only applying them after the loop had finished.
EDIT: Python did do something sensible: I didn't expect pop'ing an element from the list to echo the element popped in the REPL, and got a printed interleaved from front to back, which does makes sense.
Python defined the semantics of modifying lists while iterating over them, which is better than c/c++ just calling it undefined behavior, but I've basically never seen it not be a bug. Either I end up creating a copy of the list to iterate over, or figure out a way to defer the modifications, or write a new list. Imo the c# /Java behavior of detecting and throwing is probably the best option for non borrow checked languages.
> Any example except C++?
Java for sure.
> However, standard library collections throw “the collection was modified”
Java is similar, but the exception is done on a "best effort" basis, and is not guaranteed.
Python will raise a runtime exception if you modify the dict you are iterating over. You can work around that by copying a snapshot of the whole thing or just the keys and iterating over that.
C++ will laugh in invalidated iterators.
Of course you can erase from the container you are iterating over, but you have to make sure to continue the iteration using the iterator returned from the erase function and avoid storing copies of .end()
I'm pretty sure most languages just make the reasonable assumption that you want to map to the object so it uses the pointer for hashing and not actually hash its value.
And yet, such mutation is perfectly safe as long as you only change the value of existing keys. But you can't do that in Rust.
You would use interior mutability, putting things in Cells in the map.
> you’re not yet at the point where everything clicks
This just seems to be the standard excuse I read any time someone has a critique of Rust.
> This just seems to be the standard excuse I read any time someone has a critique of Rust.
The guys who parrot this blend of comments don't seem to be aware that cognitive load is a major problem, not a badge of honor.
Learning the language fundamentals is not "cognitive load" more so for rust than c.
With c you also need to understand pointers, manual allocation, volatility, is that bad?
What did the Rust community expect to happen if they so strongly and often claimed that Rust is easy to learn and user-friendly?
And how did we get from that to “for some people, some languages may never fully agree with their preferred style of writing code”?
If a C++ programmer (ideal Rust learner) says four years in that the ergonomics of Rust are bad, then based on my own experience I will believe them.
I don’t write something to see if it compiles either, I design every single line of code. But with Rust (and a lesser extent C++) a lot of that design is for memory safety and not necessary for the algorithms.
> C++ programmer (ideal Rust learner)
Well there's your problem. Rust does look like a semi-colon language, that's intentional, but if that's all you understand you're probably going to struggle.
The "ideal Rust learner" would have an ML, such as Ocaml or F#, maybe some Lisp or a Scheme something like that, as well as a semi-colon language like C or Java not just the weird forced perspective from C++
One experiment I probably won't get to try is to teach Rust as First Language for computer science students. Some top universities (including Oxbridge) teach an ML as first language, but neither teaches Rust because of course Rust is (relatively) new. The idea is, if you teach Rust as first language your students don't have to un-learn things from other languages. You can teach the move assignment semantic not as a weird special case but as it it really is in Rust - the obvious default assignment semantic. I pass a Goose to a function, obviously the Goose is gone, I gave it to the function. Nobody is surprised that when they pass a joint they don't have the joint any more, so why are we surprised when we pass a String to a function and the String is gone now? And once we've seen this, the motivation for borrows (reference types) is obvious, often we don't want to give you the string, we just want to tell you about a string that's still ours. And so on.
> Well there's your problem. Rust does look like a semi-colon language, that's intentional, but if that's all you understand you're probably going to struggle.
This is a silly point to make. What makes a C++ programmer a C++ programmer is not an uncanny ability to find a semicolon on the keyboard. It's stuff like using low-level constructs, having a working mental model of how to manage resources at a low level down and how to pass and track ownership of resources across boundaries. This is not a syntax issue.
It's absurd. You have people claiming that Rust is the natural progression for C++ programmers because their skillsets, mental models, and application domain overlap, but here are you negating all that and try to portray it as a semicolon issue?
> What did the Rust community expect to happen if they so strongly and often claimed that Rust is easy to learn and user-friendly?
No one really claimed it was easy to learn. Find me one Rustacean that claims Rust is as easy as Python/Ruby/Go.
But it is higher level language with decent ergonomic and in such way it can be interpreted as user-friendly.
> If a C++ programmer (ideal Rust learner)
If Java programmer (me) can learn Rust in a year enough to contribute to Servo development, you (ideal Rust learner) should have no problem either.
Plus Google saw about 6-month to get people up to speed with Rust.
> No one really claimed it was easy to learn.
It's a great example of unintended comedy the fact that the comment right below yours is literally "Rust is easy to learn and user friendly."
https://news.ycombinator.com/item?id=42162676
Rust is easy to learn and user friendly. But if you are stuck in your ways and insist on writing code exactly like you did in another language you will have a hard time. This is true for every language on earth. Rust will refuse to compile, whereas other languages give you much more freedom to not use them as they are meant to.
> If a C++ programmer (ideal Rust learner)
There is no ideal learner.
> Rust is easy to learn and user friendly. But if you are stuck in your ways (...)
It's high time that people in the Rust community such as you just stop with this act.
The Rust community itself already answered that they find "Rust too difficult to learn or learning will take too much time" as a concern to not use Rust. The community also flagged Rust being too difficult as one of the main reasons they stopped using Rust.
https://blog.rust-lang.org/2024/02/19/2023-Rust-Annual-Surve...
Anyone who has managed to become proficient in C++ is smart enough to be proficient in Rust. The only too difficult part is adapting to the rust way of doing things and some refuse to.
There is a saying among Haskellers that "Haskell has the steepest unlearning curve". Sounds like Rust is similar.
> Anyone who has managed to become proficient in C++ is smart enough to be proficient in Rust.
I'm not sure you're paying attention. The people who are saying Rust is too hard are the Rust community itself. They said so in Rusty's annual survey. The ones who participate in it are Rust users who feel strongly about the topic to participate in their governance.
It's Rust users who say Rust is too hard. There is no way around this fact.
Everybody who has managed to become proficient building a school is smart enough to be proficient in building a library.
So building libraries must be easy, that's totally why you can become an architect over night ... What a stupid argument you delivered
You just seem to have missed the point the author was making. I will try to clarify it: when you find out that a decision you made early on didn't pan out and you're forced to change the lifetime of some data, this will incur major refactoring in Rust and that will cause you to lose a lot of time. It's nearly impossible to avoid mistakes like this, not because you don't know Rust enough, that's almost completely irrelevant... it's because you just can't predict the direction your design will go to after many iterations and changes in requirements, which are unavoidable in the real world.
Replying to that with "Rust is easy to learn" just makes it sound like you didn't even understand what you're trying to reply to.
Rust is not easy to learn. Stop saying that. It is hard. It is harder than C#, harder than Python, harder than Java, harder than PHP, harder than JS, Harder than TS.
Saying it is easy to learn is just delusional and does a disservice to the language. Rust has many advantages, but trying to get people learning the language by lying about why they should learn it is just dumb.
I have memorised the UB rules for C. Or rather, more accurately, I have memorised the subset of UB rules I need to memorise to be productive in the language and am very strict in sticking to only writing code which I know is well defined (and know my way around the C standard at a level where any obscure code I sometimes need to write can be verified to be well defined without too much hassle). I think Rust may be difficult But, if I forget something, or make a mistake, I'm screwed. Yes there's ubsan, there's tests, but ubsan and tests aren't guaranteed to work when ub is involved.
This is why I call C a minefield.
On that note, C++ has such an explosion of UB that I don't generally believe anyone who claims to know C++ because it seems to me to be almost infeasible to both learn all the rules, or at least the subset required to be productive, and then to write/modify code without getting lost.
With rust, the amount of rules I need to learn to understand rust's borrow checker is about the same or even less. And if I forget the rules, the borrow checker is there to back me up.
I still think that unless you need the performance, you should use a higher level language which hides this from you. It's genuinely easier to think about.
That being said, writing correct rust which is going to a: work as I intended and b: not have UB is much less mentally taxing, even when I have to reach for unsafe.
If you find it more taxing than writing C or C++ it's probably either because you haven't internalised the rules of the borrow checker, or because your C or C++ are riddled with various kinds of serious issues.
The ISO document for C has an appendix which lists all the known categories of Undefined Behaviour. It's not exactly a small list, but it's something you could memorize if you wanted to, like the list of all US interstates, where they start and where they end.
There has been a proposal to attempt this for C++ but IMO the progress on making such an appendix is slower than the rate of the change for the language, making it a never ending task. It was also expanded by the fact that on top of Undefined Behaviour C++ also explicitly has IFNDR, programs which it declares to be Ill-formed (ie they are not C++) but No Diagnostic is required (ie your compiler doesn't know that it's not C++). This is much worse than UB.
That's the appendix containing documented UB. The standard also explicitly states that any behaviour not explicitly defined by the standard is undefined meaning that there are things which aren't in that list. And I can confirm, there are things which you can do in C which are UB but which are not on that list.
This only makes sense if one wants to write a Phd on C++ UB and needs the exhaustive list.
For the rest of us, there’s cppreference, UBsan and quite a few books on writing correct C++ code. Of course, these will still not suffice to write 100% memory safe code, which is a pretty arbitrary goal that just happens to match what Rust offers and is pushed a lot by Rust advocates.
It’s a nice goal, but not everybody works on software that’s attacked all day every day.
Also, memory safety isn't the only "bug" - I'd even argue that the majority of "memory" issues in unsafe languages like C are actually the result of a logic error or mismatch of interface expectations, and a memory error is often the "first noticed failure". In the trivial example strcpy() examples people love to use, unexpectedly truncating a string often means the program has "failed" in it's intended task just as much as a segfault or other memory corruption.
I'm extremely positive on highlighting as many of these problems before it gets to the user's hands, even more so if it's as early as a compile step as in the borrow checker, but lets not delude ourselves that they are the only possible issue software has. Or that in many languages it's a tooling issue (or culture issue accepting that tooling...) rather than a fundamental language difference.
On a side node, with the prevalence of things WASM I feel some people are just redefining what "memory safety" is. Defining a block of memory and using offsets within that is just reinventing pointers, the runtime ensuring that any offsets are within that block just mirroring the MMU and process isolation. We should really be looking at why that isn't well used rather than just reimplementing a new version on top for "security", as if those reasons aren't really "technical" (IE poor isolation between "Trusted" and "Untrusted" data processing in separate processes due to it being "Easier") we need to ensure we don't just do the same things again, and if they are technical we can fix them.
> I still think that unless you need the performance, you should use a higher level language which hides this from you.
Exporting and consuming the full c abi with very little effort is also another huge thing in rust's favor. Languages have opted heavily for supporting calling into the c abi and being hosted by the c abi, so naturally support for rust on the same terms comes for free. There's even rust in linux now.
After reading the article, it’s clear the author approves of the fact Rust has these rules (and prefers it over C++). They’re highlighting the natural challenges that brings so future iterations or competitors can see what needs to be improved.
> This is why I call C a minefield.
Computing is a series of "minefields." At least you get a map of this particular one.
I'm far more confronted by public facing APIs that involve user authentication than I am of any particular documented set of language facts.
> I have memorised the UB rules for C.
Why? What's wrong with using one of the many static code analysis tool to tell you about them if/when they appear?
Those tools can't reliably identify undefined behaviour.
> Those tools can't reliably identify undefined behaviour.
I'm sorry, can you explain what leads you to believe your hypothetical scenario is an argument rejecting the use of static code analysis tools?
I mean, I'm stating the fact that there are many many tools out there that can pick up these problems. This is a known fact. You're saying that hypothetically perhaps they might not catch each and every single hypothetical case. So what?
They're a good idea, but not a substitute for knowing the rules. And they don't just miss theoretical cases, they miss problems in practice even when used rigourously.
> They're a good idea, but not a substitute for knowing the rules.
It's a good thing no one made that claim, then.
The whole point is that were seeing people in this thread making all sort of wild claims on how it's virtually impossible to catch these errors in C++ even though back in reality there are a myriad of static analysis and memory checker tools that do just that.
Your average developer also knows how to type in a space character but still it's a good idea to onboard linters and automatic code formatters.
You made the claim
> Why? What's wrong with using one of the many static code analysis tool to tell you about them if/when they appear?
You clearly pose static analysers as an alternative to understanding UB. You still need to understand how things work.
Embedded. Your UB is my opportunity.
Really? So far it seems like most of the UBs in C are caused either by:
1. Masochism
2. Underspecification, in a vain attempt to make a language that can theoretically be used on PDP computers.
You’re missing #3, which accounts for an absolutely enormous amount of loss:
3. The fact that an inappropriate write through a pointer results in behavior that is so undefined that it can lead to remote code execution and hence do literally anything.
No amount of additional specification can fix #3, and masochism cannot explain it.
One could mitigate #3 to some extent with techniques like control flow integrity or running in a strongly sandboxed environment.
There's nothing really you can do with out-of-bounds write in C except say that it can do "anything". This UB is unavoidable.
I'm talking more about the nonsense like "c++ + ++c". There's no reason but masochism to keep it undefined. Just pick one unambiguous option and codify it.
An example of #2 is stuff like signed overflow. There are only so many ways to handle it: wraparound, saturate, error out. So C should just document them and provide a way to detect which behavior is active (like it does with endianness).
It's someone disingenuous to purposefully ignore what is the most common kind of UB in C. It's also ultimately not a very useful dichotomy, especially because it misunderstands why behavior ends up being undefined. For example:
> I'm talking more about the nonsense like "c++ + ++c". There's no reason but masochism to keep it undefined. Just pick one unambiguous option and codify it.
It's because there's an underlying variance in what the compilers (and the hardware [1]) translated for expressions like that, and codifying any option would have broken several of them, which was anathema in the days of ANSI C standardization. (It's still pretty frowned upon, but "get one person to change behavior so that everybody gets a consistent standard" is something the committees are more willing to countenance nowadays).
> An example of #2 is stuff like signed overflow. There are only so many ways to handle it: wraparound, saturate, error out.
Funnily enough, none of the ways you mention turn out to be the way it's actually implemented in the compiler nowadays.
As for why UB actually exists, there are several reasons. Sometimes, it's essential because the underlying behavior is impossible to rationally specify (e.g., errant pointer dereferences, traps). Sometimes, it's because you have optimization hints where you don't want to constrain violation of those hints (e.g., restrict, noreturn). Sometimes, it's erroneous behavior that's hard to consistently diagnose (e.g., signed overflow). Sometimes, it's for explicit implementation-defined behavior, but for various reasons, the standard authors didn't think it could be implemented as unspecified or implementation-defined behavior.
[1] Remember, this is the days of CISC, and not the x86 only-very-barely-not-RISC kind of CISC, the heady days of CISC where things like "*p++ = --q" is a single instruction.
That's not missing, I think they left it out of the "most" criticism on purpose. A dangling pointer is one of the few really good cases for UB. (Though good arguments can be made to give the compiler less leeway in that situation.)
> The fact that an inappropriate write through a pointer results in behavior that is so undefined that it can lead to remote code execution
This is a strange way to look at it. You'd get remote code execution only if the result of writing through the pointer was exactly what you'd expect: that the value you tried to write was copied into the memory indexed by the pointer.
I think you’re missing the author’s point, but OTOH he undermined it himself by stating that learning the rules helps: because Rust requires that the ownership and relationships are encoded in the type system, it requires significant design changes when those relationships change.
Learning the rules only partly mitigates this, because sometimes one does exploratory programming and isn’t sure what the final types are or they just want to change something.
Rust thrives on over-specification which calcifies the APIs.
Anyway, just as the author’s allegedly holding Rust wrong, one could say that you’re holding C++ wrong - the right approach is to learn how to write correct code and then the exceptions. Also accept and be at peace with the fact that your code will have some bugs. I don’t know why the average Rust developer is so obsessed with getting things perfect and no less with memory safety when the overall software quality is the way it is. I mean if someone’s researching the topic or works on Rust, sure, be the Stallman of memory correctness.
I think unless your code is guaranteed to never interact with any untrusted input it is nowadays an increasingly unacceptable compromise to just accept that your program might have serious flaws which can lead to remote code execution or worse.
Moreover, it becomes increasingly unpleasant and unworkable to deal with code which progressively gets more and more unreliable.
It's expected that if the complexity of a program grows, the state space that the program can occupy grows with it. But with UB you can run into by accident that state space seems to grow exponentially in comparison to a language like Rust.
If you are required to write code at that low level, I would not use anything other than something like rust.
If you are not required to write code at that level. There are many languages with much less uncertainty than C++ which are much more productive than either C++ or rust.
I think it's telling that whenever someone raises concerns about any element of Rust, no matter how constructively, they're always met with a wall of "you must not truly get the borrow checker," or "you're using Rust wrong," or "stop trying to write <C/C++/Java/etc> in Rust!", usually with zero evidence that that is in fact what is happening. There's never anything to improve on Rust, it's always user error / a skill issue. If there ever surfaces any audio of Linus Torvalds and Ken Thompson discussing the pros and cons of the borrow checker, I expect a sea of patronising anime avatars to show up, seeking to explain Rust's invention of the concept of ownership to them.
Rust is really nifty, but there are still (many) things that could be improved in Rust, and we'd all benefit from more competition in this space, including Rust! This is not a zero sum game.
Honestly, I also think many people just want a nice ML-like with a good packaging story, and just put up with the borrow checker to get friendly C-like syntax for the Option monad, sum types with exhaustive matching, etc. This is a use case that could very much benefit from a competitor with a more conventional memory model.
> I think it's telling that whenever someone raises concerns about any element of Rust, no matter how constructively (...)
I'm far from a Rust expert, but to me if someone is whining about how it is hard to track lifecycle rules of an object because they are passing it through long chains of function calls across all sorts of boundaries, what this tells me is that you're creating your own problems that you could avoid if you simply passed the object by value instead of by reference. I mean, if tracking life cycles is a problem then why not prevent it from being a problem? Not all code lives in the hot path. I'm sure your performance benchmarks can spare a copy somewhere.
> I mean, if tracking life cycles is a problem then why not prevent it from being a problem?
So you're suggesting that people should just wrap everything in Arc or make copies everywhere to avoid lifetimes? At that point why not just use Java/OCaml/Swift/your-favourite-GC-lang?
> So you're suggesting that people should just wrap everything in Arc (...)
You're the only one who managed to come up with this nonsense. No one else did, and clearly you did not pick that from what I wrote because I definitely did no wrote that.
Please refrain from slippery slope fallacies.
> zero evidence
you mean except the original poster often implying exactly that in their articles or the personal experience of the commenter that nearly always whenever they ran into borrow checker problems it was due to exactly that reason
> There's never anything to improve on Rust, it's always user error / a skill issue.
RFCs get accepted and implemented nearly every week, like in any language there is always a lot to improve
the problem is that the complains of such articles are often less about aspects where you can improve things but more about "the borrow checker is bad" on a level of detail which if you consider the borrow checker a fundamental component basically is "rust is fundamentally bad"
Yes, and that's why the comments usually are in some sense "you don't use the BC right", because they don't want a BC. And that's fine, but you can't blame Rust for this
No one thinks there's nothing to improve in Rust, there are lots of features it is missing, some of which are in nightly or on the roadmap. But the borrow checker and the concepts that underpin it are pretty fundamental to Rust and what separates it from other languages. If you like Rust except for the borrow checker, then I would think you don't really like Rust.
> If you like Rust except for the borrow checker, then I would think you don't really like Rust.
The sentence in my original post that this addresses is supportive of the emergence of an alternative language to Rust for people with this use case, so I think we're just agreeing. (Although I wouldn't go so far as to tell others what they do or do not like based on my own ideas of what is essential and what isn't.)
I am A-OK with someone not liking Rust. I do, but it’s still only my 3rd-most used language behind the Python and TypeScript I write at work.
It’s just that time after time I’ve heard people criticize Rust because they were, in fact, trying to write their pet language in Rust. It’s similar to how many complaints I’ve heard about Python because “it’s weakly typed”. What? Feel free not to like either of them, but make it for the right reasons, not because of a misunderstanding of how they work.
Now, the author of this post may be doing everything right and Rust just isn’t good at the things they want to use it for. The complaint about constantly bumping against the borrow checker leads me to wonder.
> It’s similar to how many complaints I’ve heard about Python because “it’s weakly typed”. What? Feel free not to like either of them, but make it for the right reasons, not because of a misunderstanding of how they work.
Are you sure you're not just being harsh to people whose grasp of CS vocab is weaker than yours? If someone tells me that Python is 'weakly typed', I translate it in my head to 'dynamically typed', and the rest of their complaint generally makes sense to me, in that the speaker presumably prefers static typing. Which is a valid opinion to hold, not necessarily the result of any misunderstanding.
Reasonably sure. If it’s clear they actually mean dynamically typed, fine. That’s down to preference, and I won’t say they’re wrong any more than I’ll argue that chocolate is better than strawberry.
However, I’ve heard lots of utterly wrong criticisms of Python (and Rust and…) that were based on factual misunderstandings and not just a vocabulary mistake.
> whenever someone raises concerns about any element of Rust, no matter how constructively,
I don't think that it is a very constructive article. The author's critique of Rust raises questions like "how to do it better" but there are not answers.
> they're always met with a wall of "you must not truly get the borrow checker,"
Yeah, it is frustrating in this case particularly. The author openly states that he doesn't want to learn all the quirks of the borrow checker, and people respond to it with "you just don't get the borrow checker". I can see how this answer could be helpful, but if it was expanded constructively, if there was an explanation how it can become easy to deal with problems the author faces if you understood the borrow checker. OTOH I cannot see how such an argument can be constructed without a real example with the real code and the history of failed changes to it.
I personally feel, that the borrow checker is simple, if you got it. And the author's struggles just go away, if you got it. You can easily predict what will happen if you try this or that changes to the code, and you know how to do something so the borrow checker will be happy. But I cannot elaborate and to make it clear how it works.
I can't help but feel this is a somewhat veiled complaint about the Rust community instead of anything substantive with the language :/
Veiled? :P
I'd characterise it as a gentle criticism of the way the Rust community tends to react to anything other than effusive praise.
Rust is a nifty language, albeit with room for improvement, that falls into the (sadly overpopulated) category of 'neat thing, somewhat obnoxious fan club'.
There are improvements to the borrow checker that is in the roadmap for Rust 2024. So it's not like the "community" is claiming there are no issues
[flagged]
Rust was a pain in the ass until I stopped trying to write C code in it and started writing idiomatic Rust. I don’t know the author of this blog, but he mentions extensive C++ experience which makes me wonder if he’s trying to write C++ in Rust.
Maybe not! Maybe it’s truly just Rust being stubborn and difficult. However, it’s such an easy trap to fall into that I’ve gotta think it’s at least possible.
> Rust was a pain in the ass until I stopped trying to write C code in it and started writing idiomatic Rust.
This is the #1 problem I see with people trying to learn a new language (not just Rust).
I’ve watched enough people try to adopt different languages at companies over the years that I now lean pessimistic by default about people adopting new languages. Many times it’s not that they can’t learn the new language, it’s that they really like doing things the way they learned in another language and they don’t want to give up those patterns.
Some people like working in a certain language and there’s nothing wrong with that. The problems come when they try to learn a new language without giving up the old ways.
Like you, I’m getting similar vibes from the article. The author wants to write Rust, but the entire premise of the article is about not wanting to learn the rules of Rust.
> The problems come when they try to learn a new language without giving up the old ways
In Python, I frequently see the same problem from the other side. Instead of C/C++ programmers learning Rust and "not wanting to learn the rules of Rust", it's Java/C# programmers learning Python and not wanting to unlearn the rules of Java/C#. They write three times as much code as they need to - introducing full class hierarchies where a few duck-typed functions would do.
> This is the #1 problem I see with people trying to learn a new language (not just Rust).
Definitely! I've also noticed people will learn a group of similar languages, like Java, C#/.Net, then Kotlin as the most distant relation. Now, they think they know many languages, but they mainly have the same core idea. So when they try something new like Haskell or Swift or Rust, they think it's doing something different from the "norm" in a really irritating way.
Trying to convince developers from "classical" OO that not everything needs to be a class in JavaScript has been a major thorn in my side for years. Your little procedure with no state? That can just be an exported function.
I learned Rust before learning C properly.
Oh boy. I see bugs everywhere in C and why the borrow checker exists. It really forces you to understand what happens under the hood.
The most issues in Rust are indeed related the expressions - you don't know how to describe some scenario for compiler well-enough, in order to prove that this is actually possible - and then your program won't compile.
In C, you talk more to the computer with the language syntax, whereas in Rust you talk to the compiler.
> In C, you talk more to the computer with the language syntax, whereas in Rust you talk to the compiler.
The C compiler pretends to be the computer. But UB is still there, as a compiler-only thing that has no representation at all on the computer.
> Oh boy. I see bugs everywhere in C and why the borrow checker exists.
Any examples that you could provide? I have been dealing with C/C++ for close to 30 years. Number of times I have shot myself with undefined/unspecified behavior is less than 5.
In 30+ years of experience in C, you haven't used a free()d variable or written past the end of a buffer more than 5 times? If that's true, then you have more care and attention than 99.99% of all C experts.
I should have been clear.
Of course, I have done such mistakes, but they were caught early in the dev. process. I am talking about bugs that were caught in production due to misunderstanding of C compilers on 16/32 bit processors.
I also avoid idioms like `*p` instead write `p[i]` whereever possible.
The number of times you shot yourself in the foot that you know about. Some of those bullets just haven't landed yet. C and C++ give you very interesting foot-guns: sometimes they go off even when you don't touch them (compiler upgrade, dependencies changing, building on a new architecture, ...)
The borrow checker isn't just about UB, it is mostly about memory safety.
I'm sure you've seen plenty of use-after-frees/use-after-move/dangling pointer type things or null pointer derefs, or data races, etc etc. These are largely impossible to do in safe rust.
Borrow checker checks memory safety. Undefined/unspecified behavior still present in Rust[1].
[1]: https://doc.rust-lang.org/reference/behavior-considered-unde...
Only from code annotated unsafe. In other words, if you do not use the keyword unsafe, you have no undefined behaviors.
Clearly you must be superhuman then, something as simple as forgetting a null pointer check is bound to hit you every now and then.
Of course, I do, but they are caught early in the dev. process. Not in production though.
I would contend that’s an unusually sophisticated dev process not used by most.
> In C, you talk more to the computer with the language syntax, whereas in Rust you talk to the compiler.
C is like a fast motorcycle, rust is a car with driver-assistance system.
Are there examples one can learn from about idiomatic rust? I would appreciate either books or projects to learn from.
Generally I have the easiest time when I declare my state in the outermost scope possible, and then pass it into functions that need to operate on it. If I'm using an actual pointer, rather than a mutable reference that came in as an argument, something weird is happening! Usually that's the interface with some external library.
Rust in particular is *really* obnoxiously bad at OOP patterns, and I think my lesson at this point is that this is because it is hard to do OOP safely, at least in a way that jives with its borrow checker. Something like functional core, imperative shell seems to be a much nicer flow for the thing in general.
Anyway, I've just got the one major Rust project (an NES emulator) so I'd say I'm pretty early in my Rust journey. For me personally, the good points (delightful match, powerful enum) outweigh the bad (occasional borrow checker weirdness, frustrating lifetimes) but I think it depends a lot on what you're trying to do with it.
You can achieve some level of OO design by using traits (the generic kind, not the dyn kind), but I think the functional style and inline testing gives you a ton of nice properties for free!
Rust also pushes you to refactor in a way that really pulls out the core of your problem; the refactoring is just you understanding the problem at a deeper level (in my experience)
Rust, like ocaml, is best when used purely functionally until you run into something that isn't performant unless its imperative. But unlike ocaml or haskell there is a safe imperative middle ground before going all the way to unsafe. People who write modern C++ with value semantics etc. seem to have a lot less trouble than people coming from Java.
It's difficult to really use Rust purely functionally given that it removed pure functions from its type system, and that has a limited stack size.
I mean, I don’t write it that way, but if it works for you. I wouldn’t say you have to write it that way so I wouldn’t want to put anyone off.
Thinking about your answer a bit more, one of the paradigms of Rust is “there shall be many immutable references or just one mutable reference” and so I can see that functional programming would naturally lead to that. But it’s a paradigm that works with the underlying principles rather than the true nature of the language, IMHO.
I do it by thinking about different domains of object graphs, and how data moves between them, for example.
https://doc.rust-lang.org/book/ is great. I’d been writing Rust for months before I started reading it and still began learning new things from the start. Oh, that’s why it does this!
Edit: Oh! And use “cargo clippy” regularly. It makes excellent recommendations for how to make your code more idiomatic, with links to docs explaining why it’s nicer that way.
The borrow checker exists to force you to learn, rather than to let you skip learning. To make an analogy, I think it would be weird if I complained that I had to "memorize the rules" of the type checker rather than learning how to use types as intended.
Fair enough, but the problem in this analogy is that this learning isn't always useful or productive in any way. This is more like doing arithmetic in a sort of maths notation where every result must be in base 12 and everything else must be in base 16. Sure, you can memorise the rules and the conversions but you aren't doing much useful with your life at that point.
Obviously, the borrow checker has uses in preventing a certain class of bugs, but it also destroys development velocity. Sometimes it's a good tradeoff (safety-critical systems, embedded, backends, etc.) and sometimes it's a square peg in a round hole (startups and gamedev where fast iteration is essential)
I think it destroys productivity if and only if you don’t roll with it and do things the Rusty way. If you write code with its idioms, it can be a huge productivity boost. Specifically, I can concentrate on fixing logic errors in my code instead of resource bookkeeping. When I refactor something, I know I didn’t accidentally forget to move alloc/free to the appropriate places for the new code: if my changes broke something, it’ll tell me.
Rust shouldn’t “destroy development velocity” once you’ve grasped the core concepts. There is some overhead to being explicit about how things are shared and kept, but that overhead diminishes with time as you internalize the rules.
Not if you're iterating and have to make fundamental changes. Just like certain advanced type systems, encoding too much at compile time means you have to change a lot of code in unnecessary mechanical ways when the design constraints change, or when you discover them.
This is not a bad thing by the way, it's an extremely plausible design chocie, and is one that Rust made very clearly: rejecting not-entirely-correct programs is more important than running the parts that do work. Languages that want to optimize for prototyping will make the opposite choice, and that's fine too.
Besides, if you still want to skip learning there are escape hatches like Rc<RefCell> but these hint pretty strongly (e.g. clones everywhere) that something might be wrong somewhere.
If the borrow checker only errd on code with bugs you could call it learning. Or if it was only possible to express correct programs in the Rust language. But such a thing isn't possible in general so we accept the weaker condition, accepting a subset of all valid code that can be proven correct. The usability of the language goes with how big that subset is, and the OP is expressing frustration at the size of Rust's.
Rust isn't alone in this, languages with type hints are currently going through the same thing where the type-checker can't express certain types of valid programs and have to be expanded.
Rust's reference topology is too restrictive. You can't have back references. This is what drives many C++ programmers nuts. It's common in C++ to have A point to B, and for B to have a pointer back to A. This happens implicitly with class inheritance, too. As a result, common C++ idioms don't translate to Rust at all.
This is fixable. Because you can have back references. You just have to use Rc, Rc::Weak, .upgrade(), RefCell, .borrow, and .borrow_mut(). This works, but only if the upgrades and borrows never fail. A failed .borrow() is a panic. The implication is that if you use .borrow() or .borrow_mut(), there's some good reason to think it will never fail.
For Rc::Weak, the key constraint is that all weak pointers must drop before all strong pointers have dropped. If you can prove that, .upgrade() doesn't need a run-time check.
For RefCell, the key constraint is that no .borrow() or .borrow_mut() may be enclosed by the scope of a conflicting .borrow() or .borrow_mut(). This requires a transitive closure check on who borrows what. For many simple cases, this is statically checkable. It does require inter-function checking.
Can those checks be moved to compile time? Probably. There's already a compile-time static Rc.[1] Compile-time RefCell checking looks possible.[2] It's non-trivial to do this, but worth thinking about.
DARPA's TRACTOR project (Translating All C To Rust) is likely to generate vast amounts of Rc-heavy code, if it works. So that provides some motivation for doing something to check at compile time.
[1] https://github.com/matthieu-m/static-rc
[2] https://internals.rust-lang.org/t/zero-cost-interior-mutabil...
[3] https://www.darpa.mil/program/translating-all-c-to-rust
I'm a relative beginner at Rust, but this matches my experience fairly well. Especially the part about the brittleness, where adding just one little thing can require propagating changes throughout a project. It might be adding lifetimes, or switching between values and references, or wrapping things in Rc or Arc or RefCell or Box or something. It seems hard to do Rust in a fully bottom-up fashion; you'll end up having to adjusting all the pieces repeatedly as you fit them together.
Maybe there's a style I haven't learned yet where you start out with Arc everywhere, or Rc, or Arc<Mutex<T>>, or whatever, and get everything working first then strip out as many wrappers as possible? It just feels wrong to go around hiding everything that's going on from the borrow checker and moving the errors to runtime. But maybe that's what you need to do to prototype things in Rust without a lot of pain? I don't know enough to know.
I have already noticed that building up the mindset of figuring out your ownership story, where your values are being moved to all the time, is addictive and contagious -- I'm sneaking more and more Rusty ways of doing things into my C++ code, and so far it feels beneficial.
> Maybe there's a style I haven't learned yet where you start out with Arc everywhere, or Rc, or Arc<Mutex<T>>, or whatever, and get everything working first then strip out as many wrappers as possible?
I wouldn't recommend that. It's easy to end up with a fundamentally flawed architecture impossible to refactor out of.
In general as long as you stick to keeping data ownership as high up in the call stack as possible everything should slowly fall into place.
Think functional core imperative shell.
Your main has services, dependencies, data, and just makes calls that operate on data without trying to make deeper owned objects that are inherently hard to keep references to.
Agreed. IMO, anywhere you butt heads with the borrow checker is a place where you’d have to by hyper nitpicky about user after free or memory leaks in C or C++, just without the compiler shouting at you to fix it.
This tallies with my experience with Rust. Four years ago I wrote an implementation of the TCL language in Rust (see https://github.com/wduquette/molt). It uses no unsafe code, and includes enough of the language to be useful. But it isn’t terribly efficient, and it’s a bit of a memory hog, and so I started looking at ways to improve it.
I usually like to evolve a code base towards a new architecture a little at a time, keeping it running and passing tests at every step of the way. What I found was that even seemingly small changes required an awful lot of work, as the OP says; if I could make them work at all. Eventually I decided that I’d learned what I’d needed to, and walked away from it. (To be fair, this was late spring or early summer of 2020, everything was peculiar, and I didn’t have the spare mental capacity for the project.)
I should add: I understand the need to use a language the way it wants to be used, and that you need to assimilate and internalize that to be truly fluent. I concluded that I didn’t need Rust’s extreme performance for the kind of work I do, and that there are less intrusive ways of getting memory safety.
> This means, to be a highly productive Rust programmer, you basically have to memorize the borrow checker rules, so you get it right the first time. This is stupid, because the whole point of having a type system or a borrow checker is to tell you when you get it wrong, so you don’t have to memorize how the borrow rules work.
This is completely back to front. Of course you have to internalise the rules of a borrow checker or type system to be highly productive. How can you hope to do a good job without that?
> Of course you have to internalise the rules of a borrow checker
This is generally a good thing: the more you internalise the logic of borrow checking, the earlier you start thinking about "who owns what" instead of deferring the choice to later, which often ends up in a tangled mess of "incidental data structures" as it is sometimes called in the c++ world [1].
Of course in c++ this means you have to internalise this discipline the hard way, i.e. without the borrow checker helping you.
[1] https://isocpp.org/blog/2016/05/cppcon-2015-better-code-data...
You can do a good job by offloading that cognitive overhead to better tooling
What if Rust itself is the better tooling?
Is it just me or is everyone in this comment talking past each other, with half of them not really understanding what the article is complaining about?
My take-away was that the article concludes that Rust is NOT a good tool for working with the borrow checker. I don’t think it said that there’s anything wrong with a borrow checker, and that you shouldn’t learn the basics of how it works or how to write idiomatic Rust.
A good tool shouldn’t require you to have a perfect memory of all the rules for you to be highly productive with it. If you make mistakes it should quickly tell you so with a message that quickly lets you figure out what to change.
I think this stands in contrast with Zig where these goals is the highest priority of the language. It’s also very strict with little to no undefined behaviour. But there’s also a lot of discipline in not introducing syntax or semantics that makes it hard for the compiler/checker to give a quick pass/fail with a clear message about went wrong. You can see from the issues in GitHub that improving error messages for failures in the type system is consistently prioritised. That puts a hard constraint on Zig where they’re held back from putting too much power into the type system.
That’s not to say that Rust doesn’t prioritise being a good tool. But the semantics of borrow checking makes their job an order of magnitude more difficult here. It’s an inherent trade-off. They’ve made a huge jump in the complexity and power of the language, and it’s probably much harder to then make a tool that makes it comfortable to work with this kind of power.
Some here may find it easy to deeply understand all the rules and to write code the first time that doesn’t trip up Rust too much. But in the real world code is written by many different kinds of people with different kinds and levels of intelligence.
I’ve found this to be an important consideration when choosing languages, libraries, tools and methodologies for large teams.
One way to be a 10x programmer is to write 10x as much code as an average programmer. Another way is to make 10 other average programmers twice as efficient, and that’s clearly more scalable.
Rust may still be a good choice to make the team more productive in the long run. My point is that adopting it in a team should perhaps not be considered a trivial decision.
My short comment was a bit tongue in cheek.
> A good tool shouldn’t require you to have a perfect memory of all the rules for you to be highly productive with it. If you make mistakes it should quickly tell you so with a message that quickly lets you figure out what to change.
That's exactly what the Rust borrow checker does for me.
The borrow checker rules are quite simple conceptually.
If I own a book, I can read it, write in the margins or even destroy it. `let book: Book = Book{...}`.
I can lend this book to you exclusively `&mut Book`, you can read and write it, but not destroy it. And nobody else; including me; can even read it until you are done.
I can lend this book to you and others for reading `&Book`. We can all read it concurrently. And I must wait for everybody to be done before I can regain full ownership.
I can give you the book (passing by value). And it's now yours to do what you please. Including destroying it `drop(Book)`.
Sometimes you do want to share to many; and maybe even gate exclusive write access; at runtime. This is where Rc, Arc, Cell, RefCell and Mutex come in.
Rc and Arc destroy the book when a reference counter drops to zero. Another way to look at it, is that when the counter is 1, you have sole ownership of the book. And you can do with it what you please.
As for the runtime check for mutability, Mutex should be obvious. Cell and RefCell are similar but within a single threaded context.
And finally when you know better than the compiler, you use pointers (instead of references) and triple check your work within `unsafe` blocks.
From the article:
> This is painful because I am an experienced C++ programmer, and C++ has this exact problem except worse: undefined behavior. In the worst case, C++ simply doesn’t check anything, compiles your code wrong, and then does inexplicable and impossible things at runtime for no discernable reason (or it just deletes your entire function).
This is completely wrong, even in the "not even wrong" territory. It reads like an attempt to parrot a cliche without having any idea what it means. "Undefined behavior" just means the standard does not define what is the expected behavior, and purposely leaves implementations free to implement it how they see fit. This means crashing the app or sending an email to the pope.
In practical terms this means developers should not write code that triggers undefined behavior, and treat the code that does as errors requiring a fix. Advanced users can lean on implementation-defined behavior from compilers to add some expectation to the behavior, but that's discouraged.
It's so strange how someone calling themselves a seasoned C++ developer fails to understand such a basic aspect of the language.
The important tidbit is that a) it's completely wrong to parrot "undefined behavior" on "C++ doesn't check anything", and b) if you code triggers undefined behavior without your knowledge then you just broke the code and wrote a bug out of your own ignorance.
To make matters worse, there are a myriad of code checkers for C++ that catch undefined behavior and even some classes of safety errors. Take for instance cppcheck. Why is the blogger whining about undefined behavior and "c++ not checking" when adding cppcheck to any project is enough to detect most if not all cases?
The quote is incomplete without its continuation:
> This means that in order to write C++, you effectively have to memorize the undefined behavior rules, which sucks. Sound familiar?
Which is the point TFA is making. I believe you expended the attention span a bit too early.
> Which is the point TFA is making.
Again, the point is wrong in more than one way.
You don't simply add undefined behavior. It's wrong, and buggy code. Onboard a static code analysis tool like cppcheck to tell you when you messed up.
It takes far less work to onboard any of these tools than it takes to write a sentence in a blog post.
TFA argues developers add it left and right, unless someone memorised all the rules. Since reportedly it's not possible to memorise all the rules by a single human, then you either "simply add undefined behaviour", or limit yourself to a subset of C++ that programmers do actually understand. Which is a solution I see in many codebases: to limit a set of permissible constructs by a style manual.
I agree with this. However memorizing the borrow checker rules has led me to architect code “more correctly” upfront and consider things like ownership that I was otherwise pretty lax about ahead of time before. I think this has made me more thoughtful even in other languages which I think has been a win for me.
That said, the tedious refactors are a real pain. I think we all hoped that rustc would be smarter by now. It has gotten better but it isn’t there yet.
Same for me. I think I write better code in other languages now.
I don't really agree with this. I'd phrase it more as, you have to learn to really understand what the borrow checker is trying to do and how it makes you architect your programs and consider that ahead of time. Once you understand that, you'll rarely have problems with the borrow checker. It does preclude significant chunks of styles and data structures often used in other languages though.
> it makes you architect your programs and consider that ahead of time
This only works for projects which do not involve any R&D, but have a complete and well written functional specification written in advance. Also for projects which do a complete re-implementation of some pre-existing software.
For greenfield projects which require substantial amount of R&D, it’s impossible to architect programs ahead of time. At the start of the development, people only have a wishlist. Architecture comes later, after several prototypes implemented and evaluated, and people have some general understanding what does and doesn’t work, and what specifically needs to be done.
Rust implies that upfront architecture costs even for prototypes.
I don't really agree with that interpretation. In my opinion and experience, it does not restrict architectural choices to the extent that it makes it difficult to develop greenfield projects. It's more that it rules out a relatively small subset of architectural choices which are arguably a bad idea anyways, as they do infact have flaws that may not be obvious at first but will lead to a lot of pain if the project grows above a certain size.
Agreed. This is probably the reason why rewriting in Rust "works" -- because the architecture has been previously worked out.
Can't say I agree, or that this matches my experience of writing Rust.
I don't memorise how it works, I've just learnt what it rejects and why, and this in turn becomes clear as to why it's rejected that. Very rarely do I find myself going "oh bother, now I suddenly need to `Rc` or `Arc` this, I suspect because I've just gotten into the habit of suspecting when I anticipate things will run afoul and structuring things from the get-go to avoid that. Admittedly, I'm not writing absurdly low-level code.
I wonder if the authors grounding C++ is making life harder for them? Often when I've had to teach people Rust, getting them to stop writing {C/C#/Java}-but-in-Rust is the first stop on the trail to "stop fighting and actually enjoy the language". Every language has its idioms, just because you can, doesn't mean you should.
If you aren't writing low level code, why not use a GC language?
Great type system, great performance, great packages, great tooling, nice high-level API’s that produce low overhead code, I’m most familiar in Rust now.
I’ve had more than enough unpleasant experiences with write-runs-breaks-at-runtime style languages. I don’t like writing them, I hate having to support them in prod as they give me constant-persistent-stress. I hate what idiomatic C# is, and how much ceremony there is to read and write it. Java is worse. Go’s type system is too anaemic for my tastes. Haskell is nice, but gets a bit academic and lacks some day-to-day niceties. Kotlin is supposed to be nice, but again, we’re getting maybe 50% of Rust type system features, and you’re basically just piggybacking on Java/JVM which I hated dealing with previously. IDK what else that leaves in the mainstream. I used to play around with Nim, and that was quite nice though.
F# :)
Expression-oriented, HM type inference with gradual typing, faster than other FP languages, can even reach for low-level bits, or write extra glue code in C# which is more pleasant at low-level imperative code.
Not sure what exactly you refer to as idiomatic C#. If it has too much ceremony chances are it’s anything but!
Yeah I’ve used F# before! It was pretty good, some solid features and nice experience. It just falls into a bit of weird place IMO? You have to rely on writing/using C# to fill any holes, and I really dislike that language/ecosystem, and why split between 2 lands when I can just get the same HM type system, similar-enough principles, better perf and no MS taint.
Edit: I do love the ML style syntax though, Haskell, F#, Dhall are awesome, I wish it were more readily accepted.
The hatred of .NET (and C#) is unfortunate, irrational and unwarranted. I ended up unfortunately resorting to just thinking less of engineers that have it, because they can’t update their priors (“it was slightly inconvenient 8 years ago so it must be bad today surely”) and distinguish between Microsoft’s other products and policies and .NET itself.
I do have to ask, I have worked in codebases which used lifetimes and didn't lean into Rc/Arc and vice-versa.
I used to think Arc/Rc was a shortcut to avoiding the borrow checker shenanigans, but have evolved that thinking over time.
You do mention it in your comment so wondering if you have anything to share about it
This makes me further appreciate how golang's features tend to work entirely at compile time, which is also fast.
One of the other things that makes me worry about Rust is how similar it's depends look to npm projects, where there's a kitchen sink of third party (not the language's included library of code, and not the project's code) libraries pulled in for seemingly small utilities.
Dependencies are optional, and having a huge standard library also has its tradeoffs. If the standard library has a less than ideal API, it's stuck with that until a major version bump and you either:
1. End up with a third party package filling in the gaps, or
2. Another standard library API that users slowly migrate to
It's also a lot easier to release a new version of a package to fix a bug than do a bugfix release for the entire language toolchain, which is what would be needed in order to update the standard library. With Rust releasing a new minor version every six weeks, I think minimizing the chances of additional releases needed in between them is probably a good thing.
Unfortunately, in a world with increasingly more sophisticated attackers looking at supply chain attacks, having a lot of dependencies, especially ones that update regularly, is a huge security risk. For a language like Rust, which aims to be both low level and used in secure environments, I would argue that the risks far outweigh the benefits.
We'll see how this works, Rust is still young and not yet used in any hugely important projects (or at least not in hugely important parts of those projects - e.g. some Linux drivers, not the core kernel; some bits of Firefox'S rendering, not the JS engine). As it becomes more central, it's value as an attack target will increase, and people will start taking infiltrating malicious code in small but widely used dependencies.
I think it's the natural state of affairs for a "folk standard library" to emerge. I don't think pydantic or serde should be part of their standard libraries. But I will use them in most projects. In ten years, the "folk stdlib" will probably be a different set of packages (perhaps a superset, perhaps not). Don't push the river; if it's natural, manage it rather than fighting it.
Trying to anticipate all or even most use cases in the standard library is a fool's errand (unless we're talking about a DSL, of course). There are too many and they are too dynamic to be captured in the necessarily conservative release process of a language implementation. Languages should focus on being powerful and flexible enough to be adapted to a wide variety of use cases, and let the community of package maintainers handle the implementation. Think of this as a special case of the Unix philosophy; languages should do one thing very well, not a million things unevenly.
I bet most people here don't believe a command economy could ever work in a market for goods and services. Why should it work in a marketplace of ideas?
And new cars tailored for each consumer’s use case will emerge in ten years, too—that doesn’t make it any less awful to live in areas that lack good public transportation.
I'm not sure I understand the metaphor? Let me know if I'm off base.
If the suggestion is that putting things in the standard library makes them better, I disagree. My experience with Python for instance is that a "batteries included" strategy results in some phenomenal packages and some borderline abandoned packages that are actively dangerous to use.
To riff on your metaphor, the federal government designs the arterial highways, but the state, country, and city/town officials design the minutia of the traffic system. If the federal government had to approve spending on replacing some street signs or plowing snow, we would have a terribly impoverished transportation system.
Rust’s checks are also evaluated and enforced at compile time.
Go’s features work only at compile time, but are far more limited. I experience more crashes in Go than in any other compiled language because of how limited it is as a language.
> Rust is not the answer, it is simply a step towards the answer.
True that.
On one hand, it's amazing. On the other hand, the nagging feeling that we still have work to go in programming language design has not gone away.
Two examples of things I want, built-in on day one, in some future language:
Structured concurrency. Don't provide "legacy" mechanisms and "opt in" for structure, just bite the bullet. Like the first language which told people no, we don't "go-to" other functions, that's not happening in my language, that was structured program flow I want structured concurrency, it's a thing but it's not yet popular enough to do that as the only provided concurrency, it should be.
Smart arithmetic. Your computer has Floating Point math. FP math is fast, but, it's hard for humans to think about exactly where they lose precision and performance while using it. I should be able to write the real mathematics I want, specify the precision I need and possibly the performance trades I'm comfortable with, and the compiler not me the programmer, figures out how to use FP math to calculate my mathematics with acceptable precision or tells me that I made demands it couldn't meet or which are nonsensical.
On the other hand, two things I really liked to see when I learned Rust:
&[T]: The slice reference type, a fat pointer which specifies where zero or more contiguous Ts are, and, how many of them. This is the Right Thing™ and it's right there in the core language design, which means you don't need to go back and retro-fit it.
String: The simplest possible way to build the growable text type, as a growable array of bytes but with the strict requirement that the array's content is always UTF-8 text. Is the "Small String Optimization" a cool trick? Yeah, but it need not live in this core vocabulary type. How about Copy-on-write ? Ditto. What about other text encodings? Transcode at the edges if you need that.
I sincerely believe that it is nearly impossible to have an objective and constructive conversation about the merits of programming languages, because the language of choice becomes part of people's worldview.
So it's like discussing politics or religion. People think that they have objective views, but they can't overcome their beliefs. That's just how beliefs work. They almost never change.
Also, beliefs are tied to groups. Humans automatically adopt the beliefs of their group, at least to some degree. Or they learn to shut up about their disagreements.
This is a thread for Rust critics and Rust advocates. Try to seriously sell F# or some other ML-like language in here and you are going to end up annoying both the C++ people and the Rust people.
The world will be a better place when the AIs finally take over. If we survive.
In my opinion, it seems like you may be taking random internet discussions a bit too seriously; I don't actually expect too much meaningful programming language discourse to occur in Hacker News comments. I think the reason why I keep coming here is in part because it's one of the rare public forums where occasionally some truly interesting discussions really do happen, but don't forget Sturgeon's Law. For better or worse, public and open forums are rarely productive places to have discussions, and a lot of the real innovations certainly seem to happen behind closed doors. (Personally I greatly prefer public forums for discussion, and even would prefer anonymity if it were feasible, but I take what I can get.)
What does that say about participating here? Well, for me, sometimes when I write a comment that I feel is constructive, reasonable, and honest, it goes gray anyways, and it's easy to chalk it up to people just irrationally downvoting it because they don't like my opinion. It's also pretty easy to do this, I just need to be cynical about Apple or optimistic about the Go programming language, or something similar, and there's some percentage chance it will go negative depending on presumably who sees it first. It's not going to stop me from doing so, and ultimately it's pretty inconsequential, as I'm just some guy and my opinions are not really that important anyways.
Somehow, even though I have all of this internalized, I can't help but go 30 nested replies deep into threads debating about something senseless and unimportant, but it almost feels like it wouldn't be the Internet without debates like that. XKCD 386.
There is no answer. There are only improvements.
I am currently learning Rust and found the post interesting. As I learn the language, I keep reading how Rust is great and you don't have to manage memory (unlike C or C++).
However, managing ownership and lifetime _is_ managing memory. The borrow checker is there, all the time, reminding you of memory management.
Now, in C and C++ the same problem exists but you don't have a borrow checker to remind you. I think this is the same conclusion the blog post came to, but I'm not entirely sure.
I've written a ton of software, both backend and embedded-like software in C++.
What are people writing that requires such fancy/extensive usage of the borrow checker?
I can't even remember the last time I had to use a shared_ptr... unique_ptr and other general RAII practices have been more than enough for our codebase.
> I've written a ton of software, both backend and embedded-like software in C++
Me too.
> What are people writing that requires such fancy/extensive usage of the borrow checker?
The simplest example I can imagine is this: https://en.wikipedia.org/wiki/Matrix_multiplication When your matrices are large and you want it to run fast, you want to parallelize.
Good algorithms (which don’t bottleneck on memory bandwidth) need multiple CPU cores to concurrently store different elements of the same output matrix. Moreover, the elements computed by different cores are not continuous slices, they are rectangular blocks. Such algorithm is not representable in safe rust.
I think OP is likely overusing references. The vast majority of code doesn't need to deal with explicit lifetimes.
OP sounds fairly smart and my first thought is "if OP is struggling then Rust probably isn't for me".
Who is Rust for?
I don't think it's useful to think there is some single spectrum of intelligence that makes one more or less suited to using Rust.
Rust is for anyone who finds it useful.
In some ways, people who aren’t heavily invested in other languages to the point that they think in them. Neophytes know less, but they also have fewer things to unlearn.
Rust can be hard to get right because of the borrow checker. I had a similar situation happen to me where I went about refactoring the code to make borrow checker happy ... until the last bit when things stopped compiling and I realized my approach was completely wrong (in the rust world, I had a self-reference in the structs)
Having said this, the benefits of borrow checker out weight the shortcomings. I can feel myself writing better code in other languages (I tend to think about the layout and the mutability and lifetimes upfront more now)
My rust code now is very functional, which seems to work best with lifetimes.
I would love to know more about the authors pain, I do hope rustc gets better at lifetime compilation errors cause some of them can be very very gnarly.
> I do hope rustc gets better at lifetime compilation errors cause some of them can be very very gnarly.
When this happens, file tickets! We do our best to improve diagnostics over time, but the best improvements have been reactive, by fixing a case that we never encountered but our users did.
will keep that in mind going forward! The most recent ones which I have been hitting are around "higher-ranked lifetime error"
I know my way around this now, which is to literally binary search over the timeline of my edits (commenting out code and then reintroducing it) to see what causes the compiler to trip over (there might be better ways to debug this, and I am all ears)
Most of the times this error is several layers deep in my application so even tho I want to ticket it up, not being able to create a minimal repo for anyone to iterate against feels like a bit of wasted energy on all sides, do let me know if I should change this way of thinking and I can promise myself to start being more proactive.
If it's public code, a link to a branch with the issue can still be useful. Looking at the compiler internals you can get a better sense on how to minimize the issue. That being said, not having a minimised repro lowers the chance of it getting addressed quickly.
Even if you have already figured out how to deal with it, your future colleagues might not, and by improving the diagnostic you would also be getting that time manually commenting code back.
> Rust can be hard to get right because of the borrow checker.
In the same vein: «C/C++ can be hard to get right because of the valgrind.» ;-)
People seriously need to stop obsessing about RC/ARC. Just use it, it will be fine. The perf difference wont matter in 99.99% of the cases. Whole languages (Swift) are based on that.
I habe been writing rust at work since 2016 or so and I can't say that the virtue checker ever had been much of a problem.
Like it's not that it never caused issues but most times fixing them also produces much better code.
In my experience the most common place to run into issues is if you write C/C++ style code in rust.
Or if you write certain kinds of functional style code in rust, rust has many functional features but isn't strictly speaking a functional language and while some functional pattern work well many other fall apart especially if combined with async (which will get better once async closure are stabilize).
in the end it often boils down to trying to use patterns and styles for other languages in a language which doesn't support them well, which always causes issues, but most times (in other languages) in more subtle ways then compiler errors, e.g. UB, perf, etc.
Through there is one field (game programming) where as long as your project doesn't become quite big you can get away with a lot of suboptimal approaches of state handling but not in rust. So if it comes to hobby from scratch state game programming I wouldn't be surprised for people to get annoyed (but if it's game programming using existing frameworks and e.g. stuff like a entire component library like bavy it's a different topic altogether)
Like many of these sorts of critique articles, I can see the author's pain point and empathize, but don't fully agree. Yes, it is true that if you aren't careful you can end up with a design that doesn't work and has to be redesigned. And yes, I do agree when this happens it can be very frustrating (and a time suck). However, in my experience, and it probably depends what type of code one writes, it doesn't happen enough to fully mar the experience of an otherwise very productive language. If I had to guess, I would say this happens to me maybe 1 day in 30. Not great, but not catastrophic either.
If I'm working on a section of code the relies heavily on borrowing and lifetimes, I will typically work up a prototype without all the functionality just to ensure I have a workable design before going back to fill in the rest of the code. This is probably why I don't tend to hit it all that often. It would be ideal if this wasn't necessary, but Rust has all sorts of other awesome features that make this something worth enduring.
It's interesting how Mojo solves some of Rust's lifetime UX issues. Because Mojo values uses ASAP destruction rather scope-based destructor, what Mojo's lifetime has to do is correctly track the last place a value was used, it doesn't track the validity of a scope.
What this means in practice is that Mojo's lifetime checker extends the life of values. Just point it at an origin and it'll ensure the origin is still alive wherever you use the value attached to it.
It completely defines away "value does not live long enough" compiler problems.
It is interesting that the borrow checker doesn't run until after typechecking succeeds. As far as I'm aware, rust-analyzer has its own builtin logic for doing typechecking, but it delegates to rustc for borrow checking. I wonder whether this is just a temporary situation due to lack of engineering resources to implement borrow checking in rust-analyzer; personally I doubt that, especially since gccrs is incorporating components of rustc wholesale and so I'd be a bit surprised if rust-analyzer moves in the opposite direction. In theory it seems possible to support borrow checking in the IDE for ill-typed programs, but having borrow checking as a separate analysis pass depending on successful typechecking is just such a nice abstraction boundary to have for maintaining the toolchain.
borrow checking relies on types to be able to check; types are what carry borrows after all.
This is true but misleading: analogously, typechecking depends on parsing, but IDEs typically make a best effort to typecheck syntactically ill-formed programs.
rustc does its best to recover, continue and provide diagnostics from later stages. But at the same time it is better to provide a single early error and mark the entire node as recovered to avoid further errors at the cost of requiring more cycles of back and forth, over the alternative of tons of useless knock down errors that drown the underlying cause of the problem.
We are always on the lookout for improving in this area. Having examples of cases where we conceivably should have done better but didn't is useful. As mentioned already, the complexity here is that doing the right thing for the user requires architecting multiple separate stages of the compiler to talk to each other in way more complex ways than originally intended.
An often overlooked solution to this problem is to avoid using Rust, or to only use it for performance critical code. Writing a large application in Rust sounds hellish, it seems like it would be much nicer to only use it in sections of the hotpath where it is absolutely necessary.
That applies only if you're struggling with Rust. It's as good as any other general purpose programming language once you're out of the fight-the-borrow-checker stage. Structuring or refactoring large applications in Rust is nowhere as tedious as many project it. There are many zero-cost abstractions and other features that even makes it very pleasant.
My first preference for making simple utilities is as a shell script. The immediately next one is actually Rust.
Manually managing memory complicates the program, and makes it harder to change. Every interface written is contaminated with the implementation details of how it manages memory. This has a large cost in terms of development time. As much as you might want to imagine Rust has made every other programming language obsolete, that just isn't true.
Your assertion doesn't match my experience. Besides if that were true, nobody would be using C or C++ for writing general purpose (non-system) applications. C and C++ require the same system knowledge that Rust developers use to satisfy the borrow checker. Even worse, Rust borrow checker will remind you of those rules. C and C++ will just allow you to proceed and crash. C and C++ memory management is even more manually involved than Rust's. Yet people do write normal applications in C and C++.
The only explanation I can think of for the dislike towards Rust's compile time checks is that some people don't entirely understand these rules when they use C and C++. It's possible to resolve simple memory safety issues in C/C++ without in-depth knowledge of hardware semantics. But a complicate bug will easily stump you at runtime (personal experience).
You seem to have forgotten that garbage collected languages exist, and are much preferable to Rust in many circumstances.
True, you need to know what is and isn't possible in the borrow checking model to avoid learning it the hard way after writing the code.
There are some gotchas that you need to learn (e.g. self-referential structs won't work, or & returned from a &mut method won't be shareable).
But besides a few exceptions, it's mostly shared XOR mutable data in the shape of a tree. It's possible to build intuition around it.
This is exactly what I thought about rust when trying to learn it a few years ago. I'm also an experienced C++ programmer. After trying for 3-4 months and constantly fighting the borrow checker, I lost all motivation and gave up.
Rust borrow checker rules are a bit weird and unintuitive. But if you are a systems programmer (C or C++) and think a bit about the borrow checker complaints, you'll find that they almost always correspond to memory safety bugs like use-after-free or invalidated references. All you need to think is about what might happen if the code was accepted (this is for C/C++ programmers, since GC-based language programmers don't face those often). The same mistakes can happen in C and C++ too - but without the BC to back you up. In essence, there is no escape from those exact same rules.
There are a few genuine cases which the BC won't accept, though they may be valid. The first case is of data structures containing cycles (like dequeues, ring buffers, closed graphs, etc). The other is cross-FFI calls. This is due to the fact that the BC simply doesn't have the intelligence to reason about them (at compile-time). Even then, Rust gives you 2 types of escape hatches - runtime safety checks (using Rc, Cell, etc) with a slight performance impact, and manual safety checks (unsafe) when performance is paramount. All that's expected of you (the programmer) is to recognize such cases and use the appropriate solution.
I'm not too surprised when non-C/C++ programmers struggle with BC rules. They may be unfamiliar with the low-level execution semantics. But I'm surprised when C/C++ programmers fail to make the connection. I was a C++ programmer too and this is the first thing I noticed. Memorizing the BC rules is the absolute worst way to learn it. You should be looking for memory safety problems and correlating them with BC error messages instead. I know this works because I trained non-systems (non-C/C++, primarily JS and Python) developers in Rust. They picked up the execution and memory semantics quickly and easily made sense of the borrow checker idiosyncrasies.
This persons's problem is pretty clear - Rust is frankly miserable to write code in if you are trying to optimize everything as much as possible. Since this is the default mindspace of C/C++ programmers, the frustration is understandable.
Rust becomes a lot simpler when you borrow less and clone more. Sprinkle in smart pointers when appropriate. And the resulting program is probably still going to have fantastic performance - many developers err by spending weeks of developer time trying to shave off a few microseconds of runtime.
But, if you're a developer for whom those microseconds really do matter a lot, well then you just have to bite the bullet.
I think I come down somewhere close to this. Idiomatic safe Rust is noticeably slower and less scalable than the usual high-performance systems code many C++ developers write. This puts them in the position of either abandoning performance they know is possible or abandoning Rust code that is safe and sane, neither of which feels good.
Anecdotally, all of the happy productive Rust programmers I know do not come from a hardcore systems background. They were mostly Java and Python developers that wanted to get a bit closer to the metal. For them it is probably a great experience, and the performance is an improvement relative to what they are accustomed to.
There are quite a few shops that use Rust and C++ together, often wrapping a C++ core (for performance) with a Rust API layer (for safety).
That doesn't match my experience. C++ programmers typically use C++, because using libraries written in C++ from any other language is a miserable experience. It's rare to encounter C++ code with extensive low-level optimizations.
If you are used to C++11 or newer, you should be able to continue writing very similar code in Rust. The only major issue I encountered was the lack of the idea that "because objects A and B have effectively the same lifetime, they can safely store references to each other, as long as..." But if you are used to older versions of C++, trying to write similar code in Rust is going to be painful.
This sounds like a reasonable complaint, that refactorings are too complex in large programs.
But I have a hard time to envision any other solution than "better tooling for refactoring".
I’ve been wrestling with Swift’s region isolation checker recently and had a similar experience.
A tangential issue:
> Unfortunately, it can only catch undefined behavior that actually happens, so if your test suite doesn’t cover all your code branches you might have undefined behavior lurking in the code somewhere.
Covering every branch is not enough to say you have full coverage.
There are only three branches and you can "cover" them all with six tests. (Heck - you can cover them all with just two tests!) But there are eight paths through the code, and six tests can cover at most six of them.If anyone's just starting out with Rust and struggling with the borrow checker, here are some tips. I have been using Rust since 2013 and have tried these ideas myself and while training others. (C/C++ devs may be familiar with some of these and may skip them.)
1. Give priority to the fundamental semantics of code execution - things like C memory layout [1], function calls, stack, stack frames, frame invalidation, heap, static data, multi-threading/programming, locks, synchronization, etc. Also learn associated problems like double-free, use-after-free, invalidated references, data races, etc. These are easiest to understand if you learn an assembly language. However, if you don't have the time or patience for that, at least focus hard on the hardware and memory layout topics in Rust books. If you prefer instead to learn those by making mistakes without the borrow checker intervening, start with C. (Aside: This knowledge is needed for C and C++ as well, especially C. You can't write large code without it.)
2. DON'T try to memorize the borrow checker. Borrow checker is based on a few simple principles (which you should know), but they can manifest in very complex and surprising ways (same as memory safety bugs). It's not practical to learn all such cases. Instead, check the borrow checker error message and see if you can correlate it to any memory safety problems I mentioned above. While the borrow checker can seem very complicated and arbitrary, it's designed solely to prevent those memory safety bugs. Pretty soon, you'll be comfortable with correlating BC errors to such bugs, without having to worry about how the BC found them. Knowing the real problem will also make it easy to satisfy the BC and avoid fighting with it.
3. Understand the memory layout of data structure that you use. Ref: [2]. Borrow checker errors often require this knowledge to make sense. The same becomes crucial during debugging if you make such mistakes in C/C++. The BC wont even allow you to compile it if you make similar mistakes in Rust. You need it just to get the program to run.
4. Borrow checker wont solve every problem for you. It doesn't have the intelligence to reason it all. There are a few notable cases:
- Data structures with cycles (like closed graphs, dequeues, etc) and algorithms that deal with them. (BC prefers data structures in a tree hierarchy)
- Function calls across an FFI boundary (since the BC can't check that code)
- Valid cases according to borrow checker rules, but rejected anyway due to complicated lifetime analysis. These may eventually get resolved in a later Rust version. But such cases exist.
5. Most of the BC errors can be solved by simple code refactors. But in cases like above, you need to identify them (as a limitation of the BC) and look for an alternative solution. BC is a compile-time safety checker. The alternatives are:
- Runtime safety checks using concepts like Rc, Arc, Cell, RefCell, etc. They will pass the BC checks at compile time. But if memory safety is violated at runtime, it will simply panic and crash. It may also have a slight performance hit due to runtime checks. But this is most often very negligible, given the fact that most other languages are based entirely on such checks (GC, ARC, etc). You don't need to be too shy in resorting to these to get around BC complaints. Many Rust programmers do.
- Manual safety checks using unsafe. If the performance is absolutely important for you, you can use unsafe functions and blocks. Unsafe keyword activates some potentially memory-unsafe language features (like raw pointer de-referencing) that the BC doesn't vet (Note: BC is not deactivated). This is often what you need when you're trying to implement an unavailable data structure or algorithm. This is also the only choice for FFI calls. Rust will not check them at any time. But this usually isn't a problem. Unsafe blocks are often used only for very fundamental ideas (eg: self-referential structs) and consist of no more than 5% of a codebase. If a memory safety bug does occur, it will be in one of those blocks and will be easier to locate and correct. Moreover people convert them to libraries and publish them on crates.io. This improves the chance of finding any hidden bugs. If you need unsafe code, there's probably a library for it already.
To get a better intro, check out 'Learn Rust With Entirely Too Many Linked Lists' [3]
[1] https://www.scaler.com/topics/c/memory-layout-in-c/
[2] https://cheats.rs/#basic-types
[3] https://rust-unofficial.github.io/too-many-lists/
Yes, and people should stop using Rust for projects that do not require a systems programming language.
Know what tool to pick.
People conflate difficulty of Rust borrow checker with the inherent difficulty in systems programming. However, this is not true. Both C and C++ are primarily systems programming languages. However, that never dissuaded anyone from using them as general purpose programming languages. The same should apply to Rust as well.
Meanwhile, Rust is my programming language and I choose it unless there is a good reason to choose another language. I never really struggled with the borrow checker. I think a lot of beginners approach the BC wrongly. Trying to memorize the rule is definitely the wrong way.
Sure. But pick based on concrete knowledge of the tool rather than broad and abstract categories. Tools are routinely discovered to be more broadly applicable than their initially intended use case, or even to be poor fit for that use case. The map is not the terrain.
And in any case, ecosystem usually trumps other concerns. Deep learning is a task you'd think a system language would be ideal for. Yet Python is a probably a better choice than Rust. The best reason to avoid Rust for a new project is probably that the market for Rust developers is insufficiently liquid (though presumably that won't be true for much longer, if it's still true at all).
I used it for an API server and it was pleasantly fun. “Systems” is a broad brush.
It's a high level language with algebraic data types and fantastic type checking. I'll use it where ever I darn well please.
Sometimes I write Rust with Arc and Rc sprinkled everywhere because I just need a dialect of OCaml.
The only thing that's really missing is deref patterns so that you can pattern match on an Arc<T> with a T pattern.
As a long time Rust user, I more or less agree that it's a pain. And yeah, when working on a big project refactoring code, it's difficult to know ahead of time if the borrowing pattern you _think_ will work will actually work.
Of course, that's always the trade off with Rust. You're trading a _lot_ of time spent up-front for time saved in increments down the road.
As a concrete example, I'm in the process of building up a Rust database to replace the Postgres solution one of my applications is using. Partly because I'm a psycho, and partly because I've gotten query times down from 20 seconds on Postgres to 50ms with Rust (despite my best efforts to optimize Postgres).
Being a mostly ACID, async database, this involves some rather unpleasant interactions with the Rust borrow checker. I've had to refactor a significant portion of the code probably five times by now. The lack of feedback during the process is a huge pain point, as the article points out (though I'm not sure what the solution to that would be). Even if you _think_ you know the rules, you probably don't, and you're not going to find out until 2 hours later.
The second most painful point has to be the 'static lifetime, which comes up a lot when dealing with threading and async. For me it's when I need to use spawn_blocking inside an async function. Of course, the compiler has no way of knowing _when_ or _if_ spawn_blocking will finish, so it needs any borrows to be 'static. But in practice that means having to write all kinds of awkward workarounds in what should otherwise be simple code. I certainly understand the _why_, and I'm sure in X years it'll be fixed, but right now ... g'damn.
That said, the borrow checker _has_ improved. I think my last major Rust project was before the upgraded borrow checker, which wasn't able to infer lifetimes for local variables. So you had to throw a lot of stuff inside separate blocks. We also have a lot more elided lifetimes now. Just empirically from this project I'd say me and the borrow checker only had about 30% of the fisticuffs we did in the past.
Personally, I think the tradeoff is worth it. It won't be for everyone, or every project. But 20s to 50ms query time, with a ton of safety guarantees to ensure the valuable data running through the database is well cared for? Worth every line of refactored code.
Asides:
* The project in question: https://github.com/fpgaminer/tagstormdb
* I also replaced some of my large JSON responses with FlatBuffers. FlatBuffers is a bit of a PITA, but when you're trying to shuffle 4 million integers over to the webapp, being able to do almost 0 decoding on the browser side, and get them as a Uint32Array directly is gold.
* It's a miracle I got away with the search parser in the project. I use Pest, and both the tree it spits out and the AST I build from it hold references. Yet sprinkling a little 'a on the impl's and struct's did the trick.
* Dynamic dispatch has also improved, as far as I can tell, which used to always involve some weird lifetimes if the return values needed to borrow stuff.
* ChatGPT o1 is a lot better at Rust then 4 or 4o. I've gotten a lot more useful stuff out of o1 this time around, including less hallucinations. Still weaker than Python/TypeScript/etc, with maybe 2-3 compile errors that need to be fixed each time. But still better. Sonnet completely failed every time I tried it :/ (Both through Copilot and the web). o1 in Copilot _could_ be amazing, since I can directly attach all my code. But the o1 in Copilot _feels_ weaker. I'm fairly sure the 4o Copilot uses is finetuned, and possibly smaller, so it too always felt weaker. Seems like o1 is the same deal. Still really useful for the Typescript side of things, but for Rust I had to farm out to the web and just copy paste the files each time.
Sounds familiar. I'm a relative beginner at Rust, but I've started feeling like I get the hang of the borrow checker rules and can get something close to right on my first writing. Finally. Ok, maybe 2nd or 3rd, but definitely before the dozenth!
Or at least I thought I did, until I launched into a project that mixes async and threading. That's where I hit a wall. What it's complaining about makes sense to me, but how to fix it does not -- partly because the async and threading come from libraries that I'm trying to stitch together. They necessarily have their own idiosyncratic ways of dealing with the issues, and as a beginner I don't even fully understand the problem they're solving let alone their solutions.
(For the record, I'm basically trying to force an unholy matrimony of matrix-sdk + tokio + pyo3 + pyo3-asyncio. You can take Python's GIL to grab out values to work with, but then if you want to do an async function then you'll have to release the GIL to stuff things into a future, and... well, if I fully understood the problem then I wouldn't be here whining about it, would I?)
> This is stupid, because the whole point of having a type system or a borrow checker is to tell you when you get it wrong, so you don’t have to memorize how the borrow rules work.
Sweet summer child. Use Haskell for a while and get back to me.
> This means that in order to write C++, you effectively have to memorize the undefined behavior rules, which sucks.
> This means, to be a highly productive Rust programmer, you basically have to memorize the borrow checker rules, so you get it right the first time. This is stupid, because the whole point of having a type system or a borrow checker is to tell you when you get it wrong
I'm not sure how you want to square this circle, you don't want to memorize the rules of UB, but you also don't want for compiler to correct you when you make UB behavior according to Borrow Checker?
The best way in both C++ and Rust is to structure your tree of lifetimes and use other means to achieve your desired goal.
They just don't want the borrow checker errors to only come after type checking. You have to know the rules well because otherwise you're going to finish a big refactor only to find out afterwards it was never going to work.
That's one of their issues. Other issues are borrow virality and stuff for next Rust.
My opinion so far is that people want to write Java using Rust, or they want to write C++ using Rust, and they have a hard time. You can’t really just go write a program architected the way you do in those other languages. It’s not “C++ with a borrow checker”.
So I’d say it’s “worse” than “you have to memorize the borrow checker”. Its “you have to learn how to write programs in Rust”.
what a miss.
i’d consider myself a day-to-day c++ engineer. well, because i am. i like lots of things from rust. there’s a few things i don’t. c++ has a lot to learn from rust, if it is to continue to exist.
but really.. isn’t this the point of the language? you need to understand the borrow checker because.. that’s why it’s here?
maybe i’m missing something.
Daily reminder that every Turing complete language has undefined behavior.
What? That’s not true. Turing machines themselves do not have undefined behavior.
Halting problem?
The overall point is it’s impossible to make a perfect spec. Rust doesn’t even have a spec, so your behavior is whatever the compiler gives you, which is C++ UB too.
Undefined behavior means something specific in the language specification world.
The language specification, or standard, guarantees certain things about the behavior of programs written to the specification. "Undefined behavior" means "you did something such that the guarantees of this specification no longer apply to your program." It's pretty much a worst case scenario in terms of writing programs. The program might do... anything. Fortunately, in reality it happens all the time and programs often keep behaving close enough to what we expect.
Turing completeness is unrelated to that sense of "undefined behavior".
I understand and my point is rust has no “ub” only because there is no spec, not because it avoids inherent computing problems.
> halting
They are not entirely unrelated. C++ UB is often things that would be very difficult to detect.
For example infinite template recursion is undefined. Specifying any other behavior is impossible due to halting problem.
Another example: a system might be able to detect out of bounds pointer deref, or maybe not. Same with signed integer overflow.
> I understand and my point is rust has no “ub” only because there is no spec, not because it avoids inherent computing problems.
Well, your point is wrong because UB is not an inherent computing problem. That's what the post above tried to explain.
Many forms of UB are inherent to C-like languages, but languages don't have to be C-like.
> For example infinite template recursion is undefined. Specifying any other behavior is impossible due to halting problem.
A language can avoid this by not having infinite template recursion.
C++ currently allows infinite recursion at the language level, while acknowledging that compilers might abort early and recommending that 'early' is a depth of 1024 or higher. But a future version could bake that limit into the language itself, removing the problem.
> Another example: a system might be able to detect out of bounds pointer deref, or maybe not. Same with signed integer overflow.
A language can avoid out of bounds deref in many ways, one of which is not allowing pointer arithmetic.
Signed integer overflow is trivial to handle. I'm not sure what problem you're even suggesting here that the person in charge of the language spec can't overcome. C++'s lack of desire to remove that form of UB is not because it would be difficult.