Seasoned Rust coders don’t spend time fighting the borrow checker - their code is already written in a way that just works. Once you’ve been using Rust for a while, you don’t have to “restructure” your code to please the borrow checker, because you’ve already thought about “oh, these two variables need to be mutated concurrently, so I’ll store them separately”.
The “object soup” is a particular approach that won’t work well in Rust, but it’s not a fundamentally easier approach than the alternatives, outside of familiarity.
"This chair is guaranteed not to collapse out from under you. It might be a little less comfortable and a little heavier, but most athletic people get used to that and don't even notice!"
Let's quote the article:
> I’d say as it currently stands Rust has poor developer ergonomics but produces memory safe software, whereas Zig has good developer ergonomics and allows me to produce memory safe software with a bit of discipline.
The Rust community should be upfront about this tradeoff - it's a universal tradeoff, that is: Safety is less ergonomic. It's true when you ride a skateboard with a helmet on, it's true when you program, it's true for sex.
Instead you see a lot of arguments with anecdotal or indeterminate language. "Most people [that I talk to] don't seem to have much trouble unless they're less experienced."
It's an amazing piece of rhetoric. In one sentence the ergonomic argument has been dismissed by denying subjectivity exists or matters and then implying that those who disagree are stupid.
"a bit of discipline" is doing a lot of work here.
"Just don't write (memory) bugs!" hasn't produced (memory) safe C, and they've been trying for 50yrs. The best practices have been to bolt on analyzers and strict "best practice" standards to enforce what should be part of the language.
You're either writing in Rust, or you're writing in something else + using extra tools to try and achieve the same result as Rust.
You can argue that using C or C++ can get you to 80% of the way but most people don't actively think "okay, how do I REALLY mess up this program?" and fix all the various invariant that they forgot to handle. Even worse, this issue is endemic in higher level dynamic languages like Python too. Most people most of the time only think about the happy path.
That can be true for small programs. Not always, because Rust's type system makes for programs that can be every bit as compact as Python if the algorithm doesn't interact badly with the borrow checker. Or even if it does. For example this tutorial converts a fairly nary C program to Rust: https://cliffle.com/p/dangerust/ The C was 234 lines, the finished memory safe Rust 198 lines.
But when it comes to large programs, the ergonomics strangely tips into reverse. By "strangely tips into reverse", I mean yes it takes more tokens and thinking to produce a working Rust program, but overall it saves time. Here a "large program" means a programmer can't fit it all in his head at one time. I think Andrew Huang summed the effect up best, when he said if you start pulling on a thread in a Rust program, you always get to the end. In other languages, you often just make the knot tighter.
I'd agree with that if the comparison is JavaScript or Python. If the comparison is Zig (or C or C++) then I don't agree that it's universal. I personally find Rust more ergonomic than those languages (in addition to be being safer).
Rust is not the helmet. It is not a safety net that only gives you a benefit in rare catastrophic events.
Rust is your lane assist. It relieves you from the burden of constant vigilance.
A C or C++ programmer that doesn't feel relief when writing Rust has never acquired the mindset that is required to produce safe, secure and reliable code.
I interpreted the parent to be saying that ergonomics IS (at least partly) subjective. The subjective aspect is "what you are used to". And once you get used to Rust its ergonomics are fine, something I agree with having used Rust for a few years now.
> The Rust community should be upfront about this tradeoff
I think they are. But more to the point, I think that safety is not really something you can reasonably "trade-off", at least not for non-toy software. And I think that because I don't really see C/C++/Zig people saying "we're trading off safety for developer productivity/performance/etc". I see them saying "we can write safe code in an unsafe language by being really careful and having a good process". Maybe they're right, but I'm skeptical based on the never-ending proliferation of memory safety issues in C/C++ code.
I'm not sure that that tradeoff is quite so universal. GC'd languages (or even GC'd implementations like Fil-C) are equally or even more memory-safe than Rust but aren't necessarily any less ergonomic. If anything, it's not an uncommon position that GC'd languages are more ergonomic since they don't forbid some useful patterns that are difficult or impossible to express in safe Rust.
Turns out not wearing that helmet, and continously falling down for 40 years at the skate park has its price.
In fact, I find it more ergonomic than any other language I ever work with. I'm consistently more productive with it than even scripting languages.
Getting tired of this quip being asserted as fact. Ergonomics are subjective; memory safety is not.
Anyway, it’s all pretty easy, what’s the use arguing which of multiple easy things is easiest?
I feel unburdened while using Rust, which is not something I can say about a lot of other dev environments.
As for Zig... I tried to get into it, and I can't remember the specifics, but they felt like "poor" taste in language design (I have a similar opinion of Go). I say taste because I think some thighs weren't necessarily bad, but I just couldn't convince myself to like them. I realise this is a very minority opinion, and I know great engineers who love Zig.
Zig's just not my thing I guess. Same way Rust isn't someone else's thing.
It's not safety that makes it less ergonomic, it's correctness.
Well put! And this should not be contentious issue, it simply is annoying to deal with Rust's very strict compiler. It's not a matter of opinion it simply is more annoying than if you were to use any other language that does not put that much burden on you the developer.
Not all memory safety bugs are critical issues either. We like to pretend like they are but specifically in `coreutils` there were 2 memory safety bugs found recently.
However is it really a big concern? if someone has gotten access to your system where they can run `coreutil` commands you probably have bigger problems than them running a couple of commands that leak.
I honestly don't even know what to respond to that, but it's kind of weird to me to honestly think that you'd need essentially a "PhD" in order to use a tool...
I like the fact that "fighting the borrow checker" is an idea from the period when the borrowck only understood purely lexical lifetimes. So you have to fight to explain why the thing you wrote, which is obviously correct, is in fact correct.
That's already ancient history by the time I learned Rust in 2021. But, this idea that Rust will mean "fighting the borrow checker" took off anyway even though the actual thing it's about was solved.
Now for many people it really is a significant adjustment to learn Rust if your background is exclusively say, Python, or C, or Javascript. For me it came very naturally and most people will not have that experience. But even if you're a C programmer who has never had most of this [gestures expansively] before you likely are not often "fighting the borrow checker". That diagnostic saying you can't make a pointer via a spurious mutable reference? Not the borrow checker. The warning about failing to use the result of a function? Not the borrow checker.
Now, "In Rust I had to read all the diagnostics to make my software compile" does sound less heroic than "battling with the borrow checker" but if that's really the situation maybe we need to come up with a braver way to express this.
When I was learning rust (coming from python/java) it certainly felt like a battle because I "knew" the code was logically sound (at least in other languages) but it felt like I had to do all sorts of magic tricks to get it to compile. Since then I've adapted and understand better _why_ the compiler has those rules, but in the beginning it definitely felt like a fight and that the code _should_ work.
My experience is that what makes your statement true, is that _seasoned_ Rust developers just sprinkle `Arc` all over the place, thus effectively switching to automatic garbage collection. Because 1) statically checked memory management is too restrictive for most kinds of non trivial data structures, and 2) the hoops of lifetimes you have to go to to please the static checker whenever you start doing anything non trivial are just above human comprehension level.
- 151 instances of "Arc<" in Servo: https://github.com/search?q=repo%3Aservo%2Fservo+Arc%3C&type...
- 5 instances of "Arc<" in AWS SDK for Rust https://github.com/search?q=repo%3Arusoto%2Frusoto%20Arc%3C&...
- 0 instances for "Arc<" in LOC https://github.com/search?q=repo%3Acgag%2Floc%20Arc%3C&type=...
If you use Rust for web server backend code then yes, you see `Arc`s everywhere. Otherwise their use is pretty rare, even in large projects. Rust is somewhat unique in that regard, because most Rust code that is written is not really a web backend code.
Arc isn't really garbage collection. It's like a reference counted smart pointer like C++ has shared_ptr.
If you drop an Arc and it's the last reference to the underlying object, it gets dropped deterministically.
Garbage collection generally refers to more complex systems that periodically identify and free unused objects in a less deterministic manner.
That doesn’t mean there aren’t other legitimate use cases, but “all the time” is not representative of the code I read or write, personally.
No, this couldn't be further from the truth.
I do find myself running into lifetime and borrow-checker issues much less these days when writing larger programs in rust. And while your comment is a bit cheeky, I think it gets at something real.
One of the implicit design mentalities that develops once you write rust for a while is a good understanding of where to apply the `UnsafeCell`-related types, which includes `Arc` but also `Rc` and `RefCell` and `Cell`. These all relate to inner mutability, and there are many situations where plopping in the right one of these effectively resolves some design requirement.
The other idiomatic thing that happens is that you implicitly begin structuring your abstract data layouts in terms of thunks of raw structured data and connections between them. This usually involves an indirection - i.e. you index into an array of things instead of holding a pointer to the thing.
Lastly, where lifetimes do get involved, you tend to have a prior idea of what thing they annotate. The example in the article is a good case study of that. The author is parsing a `.notes` file and building some index of it. The text of the `.notes` file is the obvious lifetime anchor here.
You would write your indexing logic with one lifetime 'src: `fn build_index<'src>(src: &'src str)`
Internally to the indexing code, references to 'src-annotated things can generally pass around freely as their lifetime converges after it.
Externally to the indexing code you'd build a string of the notes text, and passing a reference to that to the `build_index` function.
For simple CLI programs, you tend not to really need anything more than this.
It gets more hairy if you're looking at constructing complex object graphs with complex intermediate state, partial construction of sub-states, etc. Keeping track of state that's valid at some level, while temporarily broken at another level, is where it gets really annoying with multiple nested lifetimes and careful annotation required.
But it was definitely a bit of a hair-pulling journey to get to my state of quasi-peace with Rust's borrow checker.
How else would you safely share data in multi-threaded code? Which is the only reason to use Atomic reference counts.
No true scotsman would ever be confused by the borrow checker.
i've seen plenty of rust projects open source and otherwise that utilise Arc heavily or use clone and/or copy all over the place.
Would you also say the same for a C++ project that uses shared_ptrs everywhere?
The clone quip doesn't work super well when comparing to C++ since that language "clones" data implicitly all the time
They are clearly just saying as you become more proficient with X, Y is less of a problem. Not that if the borrow checker is blocking you that you aren't a real Rust programmer.
Let's say you're trying to get into running. You express that you can't breathe well during the exercise and it's a miserable experience. One of your friends tells you that as an experienced runner they don't encounter that in the same way anymore, and running is thus more enjoyable. Do you start screeching No True Scotsman!! at them? I think not.
> No true scotsman would ever be confused by the borrow checker.
I'd take that No true scotsman over the "Real C programmers write code without CVE" for $5000.
Also you are strawmanning the argument. GP said, "As a seasoned veteran of Rust you learn to think like the borrow checkers." vs "Real Rust programmers were born with knowledge of borrow checker".
I have some issues with Zig's design, especially around the lack of explicit interface/trait, but I agree with the post that it is a more practical language, just because of how much simpler its adoption is.
That hasn't been my experience at all. At best, the first version of code pops out quickly and cleanly because the author knows the appropriate idiom to choose. Refactoring rust code to handle changes in that allocation idiom is extremely expensive, even for the most seasoned developers.
Case in point:
> Once you’ve been using Rust for a while, you don’t have to “restructure” your code to please the borrow checker, because you’ve already thought about “oh, these two variables need to be mutated concurrently, so I’ll store them separately”.
Which fails to handle "these two variables didn't need to be mutated concurrently, but now they do".
In the C/C++/Zig code, you would add the second concurrent access, and then start fixing things up and restructuring things - if you, the programmer, knew about the first access, and knew that the concurrent access is a problem.
In countless cases, that work would not be done, and I cannot blame any of the involved people, because managing that kind of detailed complexity over the lifespan of a project is not humanly possible. The result is another concurrency bug, meaning UB in production.
Having the compiler tell you about such problems up front, exactly when they happen, is a complete game changer.
My beef is sometimes with the ways traits are implemented or how AWS implemented Errors for the their library that is just pure madness.
I really hope it’s an Rc/Arc that you’re cloning. Just deep cloning the value to get ownership is dangerous when you’re doing it blindly.
Yes, they know when to give up.
Even though Rust can end up with some ugly/crazy code, I love it overall because I can feel pretty safe that I'm not going to create hard-to-find memory errors.
Sure, I can (and do) write code that causes my (rust) app to crash, but so far they've all been super trivial errors to debug and fix.
I haven't tried Zig yet though. Does it give me all the same compile time memory usage guarantees?
I really wish people would quit bleating on about the borrow checker. As someone who does systems programming, that's not the problem with Rust.
Which Trait do I need to do X? Where is Trait Y and who has my destructor? How am I supposed to refactor this closure into a function? Sigh, I have to wrap yet another object as a newtype because of the Orphan Rule. Ah yes, an eight deep chain initialization calls because Rust won't do named/optional function arguments. Oh, great, the bug is inside a macro--well, there goes at least one full day. Ah, an entity component systems that treats indices like pointers but without the help of the compiler so I can index into the void and scribble over everything--but, hey, it's memory safe and won't segfault (erm, there is a reason why C programmers groan when they get a stack/heap smasher bug).
The words of every C programmer who created a CVE.
The question is, then, what price in language complexity are you willing to pay to completely avoid the 8th most dangerous cause of vulnerabilities as opposed to reducing them but not eliminating them? Zig makes it easier to find UAF than in C, and not only that, but the danger of UAF exploitability can be reduced even further in the general case rather easily (https://www.cl.cam.ac.uk/~tmj32/papers/docs/ainsworth20-sp.p...). So it is certainly true that memory unsafety is a cause of dangerous vulnerabilities, but it is the spatial unsafety that's the dominant factor here, and Zig eliminates that. So if you believe (rightly, IMO) that a language should make sure to reduce common causes of dangerous vulnerabilities (as long as the price is right), then Zig does exactly that!
I don't think it's unreasonable to find the cost of Rust justified to eliminate the 8th most dangerous cause of vulnerabilities, but I think it's also not unreasonable to prefer not to pay it.
[1]: https://cwe.mitre.org/top25/archive/2024/2024_cwe_top25.html
Languages like Modula-3 or Oberon would have taken over the world of systems programming.
Unfortunately there are too many non-believers for systems programming languages with automatic resource management to take off as they should.
Despite everything, kudos to Apple for pushing Swift no matter what, as it seems to be only way for adoption.
All jokes aside, it doesn’t actually take much discipline to write a small utility that stays memory safe. If you keep allocations simple, check your returns, and clean up properly, you can avoid most pitfalls. The real challenge shows up when the code grows, when inputs are hostile, or when the software has to run for years under every possible edge case. That’s where “just be careful” stops working, and why tools, fuzzing, and safer languages exist.
Much of Zig's user base seems to be people new to systems programming. Coming from a managed code background, writing native code feels like being a powerful wizard casting fireball everywhere. After you write a few unsafe programs without anything going obviously wrong, you feel invincible. You start to think the people crowing about memory safety are doing it because they're stupid, or, cowards, or both. You find it easy to allocate and deallocate when needed: "just" use defer, right? Therefore, it someone screws up, that's a personal fault. You're just better, right?
You know who used to think that way?
Doctors.
Ignaz Semmelweis famously discovered that hand-washing before childbirth decreased morality by an order of magnitude. He died poor and locked in an asylum because doctors of the day were too proud to acknowledge the need to adopt safety measures. If mandatory pre-surgical hand-washing step prevented complication, that implied the surgeon had a deficiency in cleanliness and diligence, right?
So they demonized Semmelweis and patients continued for decades to die needlessly. I'm sure that if those doctors had been on the internet today, they would say, as the Zig people do say, "skill issue".
It takes a lot of maturity to accept that even the most skilled practitioners of an art need safety measures.
What happens in those cases is that you drop a whole lot of disorganized dynamic and stack allocations and just handle them in a batch. So in all cases where the problem is tracking temporary objects, there's no need to track ownership and such. It's a complete non-problem.
So if you're writing code in domains where the majority of effort to do manual memory management is tracking temporary allocations, then in those cases you can't really meaningfully say that because Rust is safer than a corresponding malloc/free program in C/C++ it's also safer than the C3/Jai/Odin/Zig solution using arenas.
And I think a lot of the disagreement comes from this. Rust devs often don't think that switching the use of the allocator matters, so they argue against what's essentially a strawman built from assumed malloc/free based memory patterns that are incorrect.
ON THE OTHER HAND, there are cases where this isn't true and you need to do things like safely passing data back and forth between threads. Arenas doesn't help with that at all. So in those cases I think everyone would agree that Rust or Java or Go is much safer.
So the difference between domains where the former or the latter dominates needs to be recognised, or there can't possibly be any mutual understanding.
Rust's model has a strict model that effectively prevents certain kinds of logic errors/bugs. So that's good (if you don't mind the price). But it doesn't address all kinds of other logic errors/bugs. It's like closing one door to the barn, but there are six more still wide open.
I see rust as an incremental improvement over C, which comes at quite a hefty price. Something like zig is also an incremental improvement over C, which also comes at a price, but it looks like a significantly smaller one.
(Anyway, I'm not sure zig is even the right comp for rust. There are various languages that provide memory safety, if that's your priority, which also generally allow dropping into "unsafe" -- typically C -- where performance is needed.)
Could you point at some language features that exist in other languages that Rust doesn't have that help with logic errors? Sum types + exhaustive pattern matching is one of the features that Rust does have that helps a lot to address logic errors. Immutability by default, syntactic salt on using globals, trait bounds, and explicit cloning of `Arc`s are things that also help address or highlight logic bugs. There are some high level bugs that the language doesn't protect you from, but I know of now language that would. Things like path traversal bugs, where passing in `../../secret` let's an attacker access file contents that weren't intended by the developer.
The only feature that immediately comes to mind that Rust doesn't have that could help with correctness is constraining existing types, like specifying that an u8 value is only valid between 1 and 100. People are working on that feature under the name "pattern in types".
There is a significant crowd of people who don't necessarily love borrow checker, but traits/proper generic types/enums win them over Go/Python. But yes, it takes significant maturity to recognize and know how to use types properly.
- Every C programmer I've talked to
No its not, if it was that easy C wouldn't have this many memory related issues...
avoiding all memory management mistakes is not easy, and the bigger the codebase becomes, the more exponential the chance for disaster gets
C and Zig aren't the same. I would wager that syntax differences between languages can help you see things in one language that are much harder to see in another. I'm not saying that Zig or C are good or bad for this, or that one is better than the other in terms of the ease of seeing memory problems with your eyes, I'm just saying that I would bet that there's some syntax that could be employed which make memory usage much more clear to the developer, instead of requiring that the developer keep track of these things in their mind.
Even if you must manually annotate each function so that some metaprogram that runs at compile time can check that nothing is out of place could help detect memory leaks, I would think. or something; that's just an idea. There's a whole metaprogramming world of possibilities here that Zig allows that C simply doesn't. I think there's a lot of room for tooling like this to detect problems without forcing you to contort yourself into strange shapes simply to make the compiler happy.
Probably both. They're words of hubris.
C and Zig give the appearance of practicality because they allow you to take shortcuts under the assumption that you know what you're doing, whereas Rust does not; it forces you to confront the edge cases in terms of ownership and provenance and lifetime and even some aspects of concurrency right away, and won't compile until you've handled them all.
And it's VERY frustrating when you're first starting because it can feel so needlessly bureaucratic.
But then after awhile it clicks: Ownership is HARD. Lifetimes are HARD. And suddenly when going back to C and friends, you find yourself thinking about these things at the design phase rather than at the debugging phase - and write better, safer code because of it.
And then when you go back to Rust again, you breathe a sigh of relief because you know that these insidious things are impossible to screw up.
On both your average days and your bad days.
Over the 40 to 50 years that your carer lasts.
I guess those kind of developers exist, but I know that I'm not one of them.
(If you go no GC "because it's fun" then there's no need for the post in the first place --- just use what's fun!)
Distribution can also be a lot easier if you don't need to care about the user having a specific version of Python or specific packages available.
Most mobile games are implemented in a system with GC (Unity with il2cpp), and it's not even a /good/ GC, it's Boehm.
- Interlisp => https://interlisp.org/
- Cedar => https://www.youtube.com/watch?v=z_dt7NG38V4
Imagine what we could have today with hardware that is mostly busy running Electron crap and CLI tools from the 1970s.
Not Go because of its anaemic type system.
"Cognitive overhead: You’re constantly thinking about lifetimes, ownership, and borrow scopes, even for simple tasks. A small CLI like my notes tool suddenly feels like juggling hot potatoes."
None of this goes away if you are using C or Zig, you just get less help from the compiler.
"Developers are not idiots"
Even intelligent people will make mistakes because they are tired or distracted. Not being an idiot is recognising your own fallibility and trying to guard against it.
What I will say, that the post fails to touch on, is: The Rust compiler's ability to reason about the subset of programs that are safe is currently not good enough, it too often rejects perfectly good programs. A good example of this it the inability to express that the following is actually fine:
struct Foo {
bar: String,
baz: String,
}
impl Foo {
fn barify(&mut self) -> &mut String {
self.bar.push_str("!");
&mut self.bar
}
fn bazify(&self) -> &str {
&self.baz
}
}
fn main() {
let mut foo = Foo {
bar: "hello".to_owned(),
baz: "wordl".to_owned(),
};
let s = foo.barify();
let a = foo.bazify();
s.push_str("!!");
}
which leads to awkward constructs like fn barify(bar: &mut String) -> &mut String {
bar.push_str("!");
bar
}
// in main
let s = barify(&mut foo.bar);I believe that explains why many game developers, who have a very complex job to do by default, usually see the Rust tradeoff as not worth it. Less optionality in system design compounds the difficulty of an already difficult task.
If The Rust Compiler never produced false positives it should in theory be (ignoring syntactic/semantic flaws) damn-near as ergonomic as anything. Much, much easier said than done.
In particular, if your comparison point is C and Zig and you don't care about safety you could use unsafe, knowing you are likely triggering UB, and be in mostly the same position as you would in C or Zig.
There has been discussion to solve this particular problem[0].
The thing I wish we would remember, as developers, is that not all programs need to be so "safe". They really, truly don't. We all grew up loving lots of unsafe software. Star Fox 64, MS Paint, FruityLoops... the sad truth is that developers are so job-pilled and have pager-trauma, so they don't even remember why they got in the game.
I remember reading somewhere that Andrew Kelley wrote zig because he didn't have a good language to write a DAW in, and I think its so well suited to stuff like that! Make cool creative software you like in zig, and people that get hella about memory bugs can stay mad.
Meanwhile, everyone knows that memory bugs made super mario world better, not worse.
The thing I wish we would remember, as developers, is that not all programs need to be so "safe".
"Safety" is just a shorthand for "my program means what I say". Unsafety is semantic gibberish.There's lots of reasons to write artistically gibberish code, just as there is with natural language (e.g. Lewis Carroll). Most programs aren't going for code as art though. They're trying to accomplish something definite through a computer and gibberish is directly counterproductive. If you don't mean what you write or care what you get, software seems like the wrong way to accomplish your goals. I'd still question whether you want software even in a probabilistic argument along these lines.
Even for those cases where gibberish is meaningful at a higher level (like IOCCC and poetry), it should be intentional and very carefully crafted. You can use escape hatches to accomplish this in Rust, though I make no comment on the artistic merits of doing so.
The argument you're making is that uncontrolled, unintentional gibberish is a positive attribute. I find that a difficult argument to accept. If we could wave a magic wand and make all code safe with no downsides, who among us wouldn't?
It doesn't change anything about Super Mario World speedruns because you can accomplish the same thing as arbitrary code execution inputs with binary patching. We just have this semi-irrational belief that one is cheating and one is not.
You could write rust code with logic errors. I could write C with a memory leak that doesn't matter because of the context it runs in. Neither program is gibberish but one of them causes real problems.
Either way, telling someone they have to pick between using a safe language like Rust and writing "semantically gibberish" is a false dichotomy. Please don't call programs written in memory-unsafe languages semantic gibberish until you prove that there are absolutely zero bugs in your program.
Anybody would, but Rust is not that wand, and there is no wand.
Code needs to _exist_ in order to matter. Time is finite, my free-time is even more limited. Most of my code is garbage collected, and runs great, but if I needed it to be really fast, I would use Zig.
I don't need to be told what software is for or how to accomplish my goals, and I'm sure you wouldn't understand my goals. I've been making code creatively for almost 30 years. You might as well tell an origami artist that folding paper is a bad way to accomplish their goals.
The attitude among _some_ Rust devs (or armchair coders) that there is no place for non-rust manual-memory languages is insanely disconnected with reality. Games exist, Rust ones hardly do. Synths exist, Rust ones hardly do. Not everything is a high-availability microservice or an OS kernel! Look around!
Edited to add:
> "Safety" is just a shorthand for "my program means what I say". Unsafety is semantic gibberish.
You know this isn't true, right? "Safety" in Rust specifically means memory safety and thread safety: no use-after-free, no data races, no null/dangling pointer dereferences, no buffer overflows. That's it. It doesn't guarantee your program is correct, and it doesn't even prevent memory leaks.
Things being manually managed doesn't make them gibberish, and something being implicit, rather than explicit, doesn't mean its gibberish.
The attitude of Rust being bug-free is _insaaaane_. A "bug" is just code that breaks expectations, I promise we can and will write those in every language forever.
I am fine with ignoring the problems that rust solves, but not because I'm smart and disciplined. It just fits my use-case of making fast _non-critical_ software. I don't think we should rewrite security and networking stacks in it.
> This means that basically the borrow checker can only catch issues at comptime but it will not fix the underlying issue that is developers misunderstanding memory lifetimes or overcomplicated ownership. The compiler can only enforce the rules you’re trying to follow; it can’t teach you good patterns, and it won’t save you from bad design choices.
In the short times that I wrote Rust, it never occurred to me that my lifetime annotations were incorrect. They felt like a bit of a chore but I thought said what I meant. I'm sure there's a lot of getting used to using it--like static types--and becomes second nature at some point. Regardless, code that doesn't use unsafe can't have two threads concurrently writing the same memory.
The full title is "Why Zig Feels More Practical Than Rust for Real-World CLI Tools". I don't see why CLI tools are special in any respect. The article does make some good points, but it doesn't invalidate the strength of Rust in preventing CVEs IMO. Rust or Zig may feel certain ways to use for certain people, time and data will tell.
Personally, there isn't much I do that needs the full speed of C/C++, Zig, Rust so there's plenty of GC languages. And when I do contribute to other projects, I don't get to choose the language and would be happy to use Rust, Zig, or C/C++.
Because they don't grow large or need a multi-person team. CLI tools tend to be one & done. In other words, it's saying "Zig, like C, doesn't scale well. Use something else for larger, longer lived codebases."
This really comes across in the article's push that Zig treats you like an adult while Rust is a babysitter. This is not unlike the sentiment for Java back in the day. But the reality is that most codebases don't need to be clever and they do need a babysitter.
Most of those are more memory safe than C. None of them have the borrow checker. This leaves me wondering why - other than proselytizing Zig - this article would make such a direct and narrow comparison between only Zig and Rust.
It's a bit messier than that. Basically the only concurrency-related bug I ever actually want help with from the compiler is memory ordering issues. Rust chose to make those particular racey memory writes safe instead of unsafe.
I feel like I am most interested about nim given how easy it was to pick up and how interoperable it is with C and it has a garbage collector and can change it which seems to be great for someone like me who doesn't want to worry about manual memory management right now but maybe if it becomes a bottleneck later, I can atleast fix it without worrying too much..
Out of all of them from what little I know and my very superficial knowledge Odin seems the most appealing to me, it's primary use case from what I know is game development I feel like that could easily pivot into native desktop application development was tempted to make a couple of those in odin in the past but never found the time.
Nim I like the concept and the idea of but the python-like syntax just irks me. haha I can't seem to get into languages where indentation replaces brackets.
But the GC part of it is pretty neat, have you checked Go yet?
But I like nim in the sense that I feel sometimes in golang that I can't change its GC and so although I do know that for most things it wouldn't be a breaker.
but still, I sometimes feel like I should've somewhat freedom to add memory management later without restarting from scratch or something y'know?
Golang is absolutely goated. This was why I also recommended V-lang, V-lang is really similar to golang except it can have memory management...
They themselves say that on the website that IIRC if you know golang, you know 70% V-lang
I genuinely prefer golang over everything but I still like nim/ V-lang too as fun languages as I feel like their ecosystem isn't that good even though I know that yes they can interop with C but still...
We don't need yet another language with manual memory management in the 21st century, and V doesn't look that would ever be that relevant.
V is also similar to golang in syntax, something that I definitely admire tbh.
I am interested about nim and V more tbh as compared to D-lang
In fact I was going to omit D-lang from my comment but I know that those folks are up to something great too and I will try to look into them more but nim defintely peaks my interests as a production ready language-ish imo as compared to V-lang or even D-lang
I think people prefer what's familiar to them, and Swift definitely looks closer to existing C++ to me, and I believe has multiple people from the C++ WG working on it now as well, supposedly after getting fed up with the lack of language progress on C++.
The most recent versions gained a lot in the way of cross-platform availability, but the lack of a native UI framework and its association with Apple seem to put off a lot of people from even trying it.
I wish it was a lot more popular outside of the Apple ecosystem.
https://docs.swift.org/swift-book/documentation/the-swift-pr...
He seems to know what he's doing, from the author's Twitter:
Post something slightly mentioning rust in r/cpp, Rust evangelists show up, post something slightly mentioning rust in r/zig, Rust evangelists show up. How is this not a cult?Plenty of such people out there.
This guy appears to just personally dislike Rust for reasons undisclosed and tries to rationalize it via posts like this one.
It's like with this former coworker of my former coworker who was really argumentative, seemingly for the sake of it. I did some digging and found that his ex left him and is now happily married.
Turns out that when he was criticizing the use of if-else in Angular templates what he was really thinking about was "if someone else".
If your program runs for a short time and then exits, arena editing is an option. That seems to be what the author means by "CLI tools". It's the lifetime, not the input format.
"Rust is amazing, if you’re building something massive, multithreaded, or long-lived, where compile-time guarantees actually save your life. The borrow checker, lifetimes, and ownership rules are a boon in large systems."
Yes. That's really what Rust is for. I've written a large metaverse client in Rust, and one of the regression tests I run is to put an avatar in a tour vehicle and let it ride around for 24 hours. About 20 threads. No memory leaks. No crashes. That would take a whole QA team and lots of external tools such as Valgrind in C++, and it would be way too slow in any of the interpreted languages.
Off-topic - that sounds amazing, is this commercial or hobby software? Any way I could learn more about it?
C also allows to produce memory safe software with a bit of discipline. This "bit of discipline" is the issue here which developers are lacking.
- out of bounds access (70% of CVEs)
- nullptr dereferences
- type safety issues
That’s massively better than C. Preventing use after free errors requires much less discipline than never missing a boundary or bungling a signed/unsigned conversion.
Zig also has a knock your socks off incredible cross platform build system, empowers some really nice optimizations/ergonomics with comptime, has orders of magnitude faster build times that C++/rust.
Zig is still < v1.0 so standard library could use some work and there are other warts, but I think it will be a great choice for performance oriented programs in the future.
I agree the borrow checker can be a pain though, I wish there were something like Rust with a great GC. Go has loads of other bad design decisions (err != nil, etc.) and Cargo is fantastic.
If I don't need absolute best performance, I can use GC-ed systems like Node, Python, Go, OCaml, or even Java (which starts fast now thanks to Graal AOT) and enjoy both the safety and expressive power of using a high-level language. When I use a GCed language, I don't have to worry about allocation, lifetimes, and so on, and the user gets a plenty good experience.
If I need the performance only manual memory management can provide (and this situation arises a lot less often than people think it does), I can justify spending the extra time expressing my thoughts in Rust, which will get me both performance and safety.
Why would I go to the trouble of using Zig instead of Rust? Zig, like Rust, incurs a complexity and ecosystem cost. It doesn't give me safety in exchange. I put in about as much effort as I would into a Rust program but don't get anything extra back. (Same goes if you substitute "C++" for "Rust".)
> All it took was some basic understanding of memory management and a bit of discipline.
Is the idea behind Zig just that it's perfectly safe if you know what you're doing --- therefore using Zig is some kind of costly signal of competence? That's like someone saying free-solo-ing a cliff face is perfectly safe if you know what you're doing. Someone falls to his death? Skill issue, right?
We have decades of experience showing that nobody, no matter how much "understanding" and "discipline" he has, can consistently write memory-safe code with manual memory management in a language that doesn't enforce memory safety rules.
So what's the value proposition for Zig?
Are you supposed to use it instead of something like Go or Python or AOT-Kotlin and spend more of your time dealing with memory than you would in one of these languages? Why?
Or are you supposed to use it instead of Rust and get, what, slightly faster compile times, maybe? And no memory safety?
There is a lot of research showing that a high percentage of security bugs are from memory safety issues across many different studies. I believe this is why people are pushing moving to Rust.
However, if you don’t get the memory safety in Zig. Why bother moving from your existing coding language? Why not just not learn a new language and code where you are?
Null pointers disallowed by default. Slices. Superb cross-compilation. Easy C interop. Comptime instead of C preprocessor.
There are lots more.
The problem Rust is up against is that the number of people who want Rust simply because of "strict typing" far, far, far outnumbers those who care about safety or speed. And that leads to the issue that most people really should be using a GC language like OCaml rather than Rust.
Unfortunately, the OCaml ecosystem ... :(
Because Zig is a better language across the board.
And that includes safety. You can have a language that doesn't do heavy static-analysis like Rust which still makes safety a lot easier than C/C++.
Memory safety is not even a flaw of C/C++, it's a tradeoff. That being said, even if memory-safety was a 'feature' (rather than a tradeoff, and yes, Rust did a better job minimizing the trade than GC or FP languages), it's not the only feature.
I once joined a company with a large C/C++ codebase. There I worked with some genuinely expert developers - people who were undeniably smart and deeply experienced. I'm not exaggerating and mean it.
But when I enabled the compiler warnings (which annoyed them) they had disabled and ran a static analyzer over the codebase for the first time, hundreds of classic C bugs popped up: memory leaks, potential heap corruptions, out-of-bounds array accesses, you name it.
And yet, these same people pushed back when I introduced things like libfmt to replace printf, or suggested unique_ptr and vector instead of new and malloc.
I kept hearing:
"People just need to be disciplined allocations. std::unique_ptr has bad performance" "My implementation is more optimized than some std algorithm." "This printf is more readable than that libfmt stuff." etc.
The fact is, developers, especially the smart ones probably, need to be prevented from making avoidable mistakes. You're building software that processes medical data. Or steers a car. Your promise to "pay attention" and "be careful" cannot be the safeguard against catastrophe.
printf("Error: File `%s` in batch %d failed.", file.c_str(), batch) vs fmt::print("Error: File `{}` in batch {} failed.", file, batch)
One of which is objectively safer and more portable than the other. They didn't care. "I like what I've been doing for the last 20 years already better because it looks better.". "No Its not because I'm just used to it." "If you are careful it is just as safe. But you gotta know what you are doing."
And best of all - classic elitism:
"If you are not smart enough to do it right with printf, maybe you shouldn't be a C++ programmer. Go write C# or something instead."
The same person was not smart enough to do it right in many places as I've proven with a static analyzer.
I stole someone else's benchmark to use, and at one point I ran into seriously buggy behavior on strings (but not integers) that wasn't caught at the point where it happened early even with -Odebug.
Turns out the benchmark was freeing the strings before it finished performing all of the operations on the data structure. That's the sort of thing that Rust makes nearly impossible, but Zig didn't catch at all.
That being said, you've missed the point if you can't understand that safety comes at a real cost, not an abstract or 'by any means necessary' cost, but a cost as real as the safety issues.
self.last.as_ref().unwrap().borrow().next.as_ref().unwrap().clone()
I know it can be improved but that's what I think of
I'd love to see the actual code here! When I imagine the Rust code for this, I don't really foresee complicated borrow-checker or reference issues. I imagine something like
struct Note {
filename: String,
// maybe: contents: String
}
// newtype for indices into `notes`
struct NoteIdx(usize);
struct Notes {
notes: Vec<Note>,
tag_refs: HashMap<String, Vec<NoteIdx>>
}
You store indices instead of pointers. This is very unlikely to be slower: both a usize index and a pointer are most likely 64 bits on your hardware; there's arguably one extra memory deref but because `notes` will probably be in cache I'd argue it's very unlikely you'll see a real-life performance difference.It's not magic: you can still mess up the indices as you add and remove notes.
But it's safer: if you mess up the indices, you'll get an out-of-bounds error instead of writing to an unintended location in your process's memory.
Anyway, even if you don't care about safety, it's clear and easy to think about and reason about, and arguably easier to do printf debugging with: "this tag is mentioned in notes 3, 10 and 190, oh, let's print out what those ones are". That's better than reading raw pointers.
Maybe I'm missing something? This sort of task comes up all day every while writing Rust code. It's just a pretty normal pattern in the language. You don't store raw references for ordinary logic like this. You do need it when writing allocators, async runtimes, etc. Famously, async needs self-referential structs to store stack local state between calls to `.await`, and that's why the whole business with `Pin` exists.
(I think Miri will shout at you if you use a invalidated pointer here)
> Developers are not Idiots
I'm often distracted and AIs are idiots, so a stricter language can keep both me and AIs from doing extra dumb stuff.
I really appreciate this in my role, where I have an office right next to the entrance to the building. I get walk-ins all of the time. When my door is closed, I get knocks on the door all of the time. Both AI and strict languages are great tools in my environment, where focus for me is as abundant as water in a desert.
Yes, safety isn't correctness but if you can't even get safety then how are you supposed to get correctness?
For small apps Zig probably is more practical than Rust. Just like hiring an architect and structural engineers for a fence in your back yard is less practical than winging it.
https://play.rust-lang.org/?version=stable&mode=debug&editio...
Also, I started programming on a TRS-80 color computer with cassette tape storage and it still feels crazy to just do stuff and it allocates somewher and you have little control over what happens and some other process runs in the background that tries to clean up after you. I bounce off of languages (for hobby projects) with such a large list of features and with executable sizes that are so huge. I can't help it.
Here is a bunch of more memory safe languages that are more popular than Rust: Python, Java, JS ... should I continue?
But wait, there's more. Can Rust verify that arbitrary in-data is valid, like having contracts statically checked at compile time? It can't, and so then by the same logic as some people calling non-Rust languages "gibberish" for not having a borrow checker, should we call Rust "gibberish" because it can't check those invariants at compile time like C3 can do?
No?
Then get off that high horse.
And by the way, my great fear is that we'll get more adoption by Rust while at the same time Rust is unable to improve the compile times. C++, Swift and Rust, the three horsemen of infamously long compile times.
The borrow checker isn't the main problem, just like C++'s problems isn't complex template messages – it's that both takes so long to compile. Time you could spend reading and testing the code. Long compile times inhibits refactoring and cause bugs to stay unfixed.
Rust's concept of "safety" really means "absence of Undefined Behavior". Lots of people don't seem to understand that C, C++, and Zig programs containing UB are gibberish - they are not C, C++, or Zig programs, but something else entirely. The key insight here is that in any language, UB invalidates the entire program. It no longer does what you think it does, your tests are all moot, and so on. But a lot of people seem to think that there is an acceptable amount of UB in any code base. There isn't.
UB is a concept that exists for every language, even languages like Python, Java, C#, JavaScript, but those languages make it very hard to encounter accidentally.
Before Rust, there was no way to guarantee the absence of UB without a significant runtime cost, so that's a very meaningful invention.
So this. We currently spent about a month carefully instrumenting and coming to understand a subtle bug in our distributed radio network. This all runs on bare metal C (samd21 chips). Because timing, and hundreds of little processors, and radios were all involved, it was a pita to surface what the issue was. It was algorithmic. Not a memory problem. Writing this in rust or zig (instead of straight C) would not have fixed this problem.
I’d like to consider doing next generations of this product in zig or rust. I’m not opposed. I like the extra tools to make the product better. But they’re a small part of the picture in writing good software. The borrow checker may improve your code, it doesn’t guarantee successful software.
As a professional Rust developer, I don’t find I do this. I occasionally think of those things. But I do remember a short adjustment period when I was learning Rust that I would get frustrated by the borrow checker. Of course, that’s it doing its job!
Weird that they don’t consider other options, in particular languages with reference counting or garbage collection. Those will not solve all ownership issues, but for immutable objects, they typically do. For short-running CLI tools, garbage collecting languages may even be faster than ones with manual memory management because they may be able to postpone all memory freeing until the program exits.
Edits mine.
I like to keep the spacetime topologies complete.
Constant = time atom of value.
Register = time sequence of values.
Stack = time hierarchy of values.
Heap = time graph of values.
Apparently it isn't programming with a straightjacket any longer, like on Usenet discussions.
You could argue with the reasoning that C feels more practical than Zig for real-world CLI tools.
The argument provided by the author feels a bit besides the point.
> Rust’s borrow checker is a a pretty powerful tool that helps ensure memory safety during compile time. It enforces a set of rules that govern how references to data can be used, preventing common programming memory safety errors such as null pointer dereferencing, dangling pointers and so on. However you may have notice the word compile time in the previous sentence. Now if you got any experience at systems programming you will know that compile time and runtime are two very different things. Basically compile time is when your code is being translated into machine code that the computer can understand, while runtime is when the program is actually running and executing its instructions. The borrow checker operates during compile time, which means that it can only catch memory safety issues that can be determined statically, before the program is actually run. > > This means that basically the borrow checker can only catch issues at comptime but it will not fix the underlying issue that is developers misunderstanding memory lifetimes or overcomplicated ownership. The compiler can only enforce the rules you’re trying to follow; it can’t teach you good patterns, and it won’t save you from bad design choices.
This appears to be claiming that Rust's borrow checker is only useful for preventing a subset of memory safety errors, those which can be statically analysed. Implying the existence of a non-trivial quantity of memory safety errors that slip through the net.
> The borrow checker blocks you the moment you try to add a new note while also holding references to the existing ones. Mutability and borrowing collide, lifetimes show up, and suddenly you’re restructuring your code around the compiler instead of the actual problem.
Whereas this is only A Thing because Rust enforces rules so that memory safety errors can be statically analysed and therefore the first problem isn't really a problem. (Of course you can still have memory safety problems if you try hard enough, especially if you start using `unsafe`, but it does go out of its way to "save you from bad design choices" within that context.)
If you don't want that feature, then it's not a benefit. But if you do, it is. The downside is that there will be a proportion of all possible solutions that are almost certainly safe, but will be rejected by the compiler because it can't be 100% sure that it is safe.
Experienced Rust coders aren't going to find themselves in various borrow checker (and lifetime) pitfalls that newbies do, sure.
That said, the borrow checker and lifetime do cause problems for even experienced Rust programmers. Not because they don't understand memory management, lifetimes, etc. But because they don't - yet - fully understand the problem being solved.
All programs are an evolutionary process of developing a solution to a problem (or many problems). You think one thing, code it up, realize you missed something or didn't fully grok the issue, pivot, etc.
Rust does a great job in the compiler of letting the user know if they've borked something. But often times a refactor/fix in C/D/Zig due to learned (or new) requirements is just a tweak, while in Rust it becomes a major overhaul because now something needs to be `mut` or have a lifetime added to it. I - personally - consider that "fighting" the borrow checker, regardless of how helpful (or correct) it also is.
Maybe we'll even get a tabs vs. spaces article next.
I would actually be interested in a honest comparison of a Rust CLI program using clap, duct and some other standard CLI handling libraries with their Zig equivalents.
Show the code side by side, how much boilerplate each requires. Then compare how quickly they compile in debug and release mode, how fast they start, how big the binaries are after stripping, stuff like that.
It's true, but devs are not infallible and that's the point of Rust. Not idiots, not infallible either.
IMO admitting that one can make mistakes even if they don't think they have is a sign of an experienced and trustworthy developer.
It's not that Rust compiler engineers think that devs are idiots, in fact you CAN have footguns in Rust, but one should never use a footgun easily, because that's how you get security vulnerabilities.
The catgirls have no problems producing lots of great software in Rust. It seems more such software comes out every day, nya :3
> Compile-time only: The borrow checker cannot fix logic bugs, prevent silent corruption, or make your CLI behave predictably. It only ensures memory rules are followed.
Also not really true from my experience. There have been plenty of times where the borrow checker is a MASSIVE help in multithreaded context.