NHacker Next
login
▲Thoughts on Go vs. Rust vs. Zigsinclairtarget.com
241 points by yurivish 5 hours ago | 255 comments
Loading comments...
kibwen 4 hours ago [-]
> In Rust, creating a mutable global variable is so hard that there are long forum discussions on how to do it. In Zig, you can just create one, no problem.

Well, no, creating a mutable global variable is trivial in Rust, it just requires either `unsafe` or using a smart pointer that provides synchronization. That's because Rust programs are re-entrant by default, because Rust provides compile-time thread-safety. If you don't care about statically-enforced thread-safety, then it's as easy in Rust as it is in Zig or C. The difference is that, unlike Zig or C, Rust gives you the tools to enforce more guarantees about your code's possible runtime behavior.

nixpulvis 43 seconds ago [-]
A language that makes making a global mutable variable feel like making any other binding is a anti-pattern and something I'm glad Rust doesn't try to pretend is the same thing.

If you treat shared state like owned state, you're in for a bad time.

ajross 2 hours ago [-]
> [...] is trivial in Rust [...] it just requires [...]

This is a tombstone-quality statement. It's the same framing people tossed around about C++ and Perl and Haskell (also Prolog back in the day). And it's true, insofar as it goes. But languages where "trivial" things "just require" rapidly become "not so trivial" in the aggregate. And Rust has jumped that particular shark. It will never be trivial, period.

JuniperMesos 5 minutes ago [-]
It just requires unsafe. One concept, and then you can make a globally mutable variable.

And it's a good concept, because it makes people feel a bit uncomfortable to type the word "unsafe", and they question whether a globally mutable variable is in fact what they want. Which is great! Because this is saving every future user of that software from concurrency bugs related to that globally mutable variable, including ones that aren't even preserved in the software now but that might get introduced by a later developer who isn't thinking about the implications of that global unsafe!

kibwen 1 hours ago [-]
> languages where "trivial" things "just require" rapidly become "not so trivial" in the aggregate

Sure. And in C and Zig, it's "trivial" to make a global mutable variable, it "just requires" you to flawlessly uphold memory access invariants manually across all possible concurrent states of your program.

Stop beating around the bush. Rust is just easier than nearly any other language for writing concurrent programs, and it's not even close (though obligatory shout out to Erlang).

sharifhsn 1 hours ago [-]
This is a miscommunication between the values of “shipping” which optimizes for fastest time to delivery and “correctness” which optimizes for the quality of the code.

Rust makes it easy to write correct software quickly, but it’s slower for writing incorrect software that still works for an MVP. You can get away with writing incorrect concurrent programs in other languages… for a while. And sometimes that’s what business requires.

I actually wish “rewrite in Rust” was a more significant target in the Rust space. Acknowledging that while Rust is not great for prototyping, the correctness/performance advantages it provides justifies a rewrite for the long-term maintenance of software—provided that the tools exist to ease that migration.

josephg 59 minutes ago [-]
Lately rust is my primary language, and I couldn't agree more with this.

I've taken to using typescript for prototyping - since its fast (enough), and its trivial to run both on the server (via bun) or in a browser. The type system is similar enough to rust that swapping back and forth is pretty easy. And there's a great package ecosystem.

I'll get something working, iterate on the design, maybe go through a few rewrites and when I'm happy enough with the network protocol / UI / data layout, pull out rust, port everything across and optimize.

Its easier than you think to port code like this. Our intuition is all messed up when it comes to moving code between languages because we look at a big project and think of how long it took to write that in the first place. But rewriting code from imperative language A to B is a relatively mechanical process. Its much faster than you think. I'm surprised it doesn't happen more often.

timschmidt 1 hours ago [-]
> Rust makes it easy to write correct software quickly, but it’s slower for writing incorrect software that still works for an MVP.

I don't find that to be the case. It may be slower for a month or two while you learn how to work with the borrow checker, but after the adjustment period, the ideas flow just as quickly as any other language.

Additionally, being able to tell at a glance what sort of data functions require and return saves a ton of reading and thinking about libraries and even code I wrote myself last week. And the benefits of Cargo in quickly building complex projects cannot be overstated.

All that considered, I find Rust to be quite a bit faster to write software in than C++, which is probably it's closest competitor in terms of capabilities. This can be seen at a macro scale in how quickly the Rust library ecosystem has grown.

nu11ptr 38 minutes ago [-]
I disagree. I've been writing heavy Rust for 5 years, and there are many tasks for which what you say is true. The problem is Rust is a low level language, so there is often ceremony you have to go through, even if it doesn't give you value. Simple lifetimes aren't too bad, but between that and trait bounds on some one else traits that have 6 or 7 associated types, it can get hairy FAST. Then consider a design that would normally have self referential structs, or uses heavy async with pinning, async cancellation, etc. etc.

I do agree that OFTEN you can get good velocity, but there IS a cost to any large scale program written in Rust. I think it is worth it (at least for me, on my personal time), but I can see where a business might find differently for many types of programs.

timschmidt 15 minutes ago [-]
> The problem is Rust is a low level language so there is often ceremony you have to go through, even if it doesn't give you value.

As is C++ which I compared it to, where there is even more boilerplate for similar tasks. I spent so much time working with C++ just integrating disparate build systems in languages like Make and CMake which just evaporates to nothing in Rust. And that's before I even get to writing my code.

> I do agree that OFTEN you can get good velocity, but there IS a cost to any large scale program written in Rust.

I'm not saying there's no cost. I'm saying that in my experience (about 4 years into writing decently sized Rust projects now, 20+ years with C/C++) the cost is lower than C++. C++ is one of the worst offenders in this regard, as just about any other language is easier and faster to write software in, but also less capable for odd situations like embedded, so that's not a very high bar. The magical part is that Rust seems just as capable as C++ with a somewhat lower cost than C++. I find that cost with Rust often approaches languages like Python when I can just import a library and go. But Python doesn't let me dip down to the lower level when I need to, whereas C++ and Rust do. Of the languages which let me do that, Rust is faster for me to work in, no contest.

So it seems like we agree. Rust often approaches the productivity of other languages (and I'd say surpasses some), but doesn't hide the complexity from you when you need to deal with it.

gjsjchd6 1 hours ago [-]
[dead]
adastra22 49 minutes ago [-]
He’s talking about adding a keyword. That is all. I’d call that trivial.
globalnode 4 hours ago [-]
so does the rust compiler check for race conditions between threads at compile time? if so then i can see the allure of rust over c, some of those sync issues are devilish. and what about situations where you might have two variables closely related that need to be locked as a pair whenever accessed.
andsoitis 3 hours ago [-]
> so does the rust compiler check for race conditions between threads at compile time?

My understanding is that Rust prevents data races, but not all race conditions. You can still get a logical race where operations interleave in unexpected ways. Rust can’t detect that, because it’s not a memory-safety issue.

So you can still get deadlocks, starvation, lost wakeups, ordering bugs, etc., but Rust gives you:

- No data races

- No unsynchronized aliasing of mutable data

- Thread safety enforced through type system (Send/Sync)

throwawaymaths 3 hours ago [-]
and you can have good races too (where the order doesnt matter)
tczMUFlmoNk 4 hours ago [-]
> what about situations where you might have two variables closely related that need to be locked as a pair whenever accessed.

This fits quite naturally in Rust. You can let your mutex own the pair: locking a `Mutex<(u32, u32)>` gives you a guard that lets you access both elements of the pair. Very often this will be a named `Mutex<MyStruct>` instead, but a tuple works just as well.

treyd 3 hours ago [-]
This was a primary design goal for Rust! To prevent data races (and UAF and other types of memory unsafety) by construction through the type system.
PartiallyTyped 4 hours ago [-]
In rust, there are two kinds of references, exclusive (&mut) and shared(&). Rustc guarantees you that if you provide an exclusive reference, no other thread will have that. If your thread has an exclusive reference, then it can mutate the contents of the memory. Rustc also guarantees that you won't end up with a dropped reference inside of your threads, so you will always have allocated memory.

Because rust guarantees you won't have multiple exclusive (and thus mutable refs), you won't have a specific class of race conditions.

Sometimes however, these programs are very strict, and you need to relax these guarantees. To handle those cases, there are structures that can give you the same shared/exclusive references and borrowing rules (ie single exclusive, many shared refs) but at runtime. Meaning that you have an object, which you can reference (borrow) in multiple locations, however, if you have an active shared reference, you can't get an exclusive reference as the program will (by design) panic, and if you have an active exclusive reference, you can't get any more references.

This however isn't sufficient for multithreaded applications. That is sufficient when you have lots of pieces of memory referencing the same object in a single thread. For multi-threaded programs, we have RwLocks.

https://doc.rust-lang.org/std/cell/index.html

ViewTrick1002 4 hours ago [-]
It entirely prevents race conditions due to the borrow checker and safe constructs like Mutexes.

Logical race conditions and deadlocks can still happen.

kibwen 4 hours ago [-]
Rust's specific claims are that safe Rust is free from data races, but not free from general race conditions, including deadlocks.
globalnode 4 hours ago [-]
ah i see, thanks. i have no idea what rust code looks like but from the article it sounds like a language where you have a lot of metadata about the intended usage of a variable so the compiler can safety check. thats its trick.
timschmidt 4 hours ago [-]
That's a fairly accurate idea of it. Some folks complain about Rust's syntax looking too complex, but I've found that the most significant differences between Rust and C/C++ syntax are all related to that metadata (variable types, return types, lifetimes) and that it's not only useful for the compiler, but helps me to understand what sort of data libraries and functions expect and return without having to read through the entire library or function to figure that out myself. Which obviously makes code reuse easier and faster. And similarly allows me to reason much more easily about my own code.
mh2266 3 hours ago [-]
The only thing I really found weird syntactically when learning it was the single quote for lifetimes because it looks like it’s an unmatched character literal. Other than that it’s a pretty normal curly-braces language, & comes from C++, generic constraints look like plenty of other languages.

Of course the borrow checker and when you use lifetimes can be complex to learn, especially if you’re coming from GC-land, just the language syntax isn’t really that weird.

timschmidt 3 hours ago [-]
Agreed. In practice Rust feels very much like a rationalized C++ in which 30 years of cruft have been shrugged off. The core concepts have been reduced to a minimum and reinforced. The compiler error messages are wildly better. And the tooling is helpful and starts with opinionated defaults. Which all leads to the knock-on effect of the library ecosystem feeling much more modular, interoperable, and useful.
pornel 1 hours ago [-]
Thread safety metadata in Rust is surprisingly condensed! POSIX has more fine-grained MT-unsafe concepts than Rust.

Rust data types can be "Send" (can be moved to another thread) and "Sync" (multiple threads can access them at the same time). Everything else is derived from these properties (structs are Send if their fields are Send. Wrapping non-Sync data in a Mutex makes it Sync, thread::spawn() requires Send args, etc.)

Rust doesn't even reason about thread-safety of functions themselves, only the data they access, and that is sufficient if globals are required to be "Sync".

etse 4 hours ago [-]
That seems unusual. I would assume trivial means the default approach works for most cases. Perhaps mutable global variables are not a common use case. Unsafe might make it easier, but it’s not obvious and probably undesired. I don’t know Rust, but I’ve heard pockets of unsafe code in a code base can make it hard to trust in Rust’s guarantees. The compromise feels like the language didn’t actually solve anything.
kibwen 3 hours ago [-]
Outside of single-initialization/lazy-initialization (which are provided via safe and trivial standard library APIs: https://doc.rust-lang.org/std/sync/struct.LazyLock.html ) almost no Rust code uses global mutable variables. It's exceedingly rare to see any sort of global mutable state, and it's one of the lovely things about reading Rust code in the wild when you've spent too much of your life staring at C code whose programmers seemed to have a phobia of function arguments.
dxxvi 2 hours ago [-]
> It's exceedingly rare to see any sort of global mutable state I know a bit of Rust, so you don't need to explain in details. How to use a local cache or db connection pool in Rust (both of them, IMO, are the right use case of global mutable state)?
adastra22 16 minutes ago [-]
You wrap it in a mutex and then it is allowed.

Global state is allowed. It just has to be thread safe.

therein 2 hours ago [-]
Why does that have to be global? You can still pass it around. If you don't want to clobber registers, you can still put it in a struct. I don't imagine you are trying to avoid the overhead of dereferencing a pointer.
stouset 3 hours ago [-]
The default approach is to use a container that enforces synchronization. If you need manual control, you are able to do that, you just need to explicitly opt into the responsibility that comes with it.

If you use unsafe to opt out of guarantees that the compiler provides against data races, it’s no different than doing the exact same thing in a language that doesn’t protect against data races.

bigstrat2003 2 hours ago [-]
> I would assume trivial means the default approach works for most cases.

I mean, it does. I'm not sure what you consider the default approach, but to me it would be to wrap the data in a Mutex struct so that any thread can access it safely. That works great for most cases.

> Perhaps mutable global variables are not a common use case.

I'm not sure how common they are in practice, though I would certainly argue that they shouldn't be common. Global mutable variables have been well known to be a common source of bugs for decades.

> Unsafe might make it easier, but it’s not obvious and probably undesired.

All rust is doing is forcing you to acknowledge the trade-offs involved. If you want safety, you need to use a synchronization mechanism to guard the data (and the language provides several). If you are ok with the risk, then use unsafe. Unsafe isn't some kind of poison that makes your program crash, and all rust programs use unsafe to some extent (because the stdlib is full of it, by necessity). The only difference between rust and C is that rust tells you right up front "hey this might bite you in the ass" and makes you acknowledge that. It doesn't make that global variable any more risky than it would've been in any other language.

nu11ptr 3 hours ago [-]
> I would assume trivial means the default approach works for most cases. Perhaps mutable global variables are not a common use case. Unsafe might make it easier, but it’s not obvious and probably undesired.

I'm a Rust fan, and I would generally agree with this. It isn't difficult, but trivial isn't quite right either. And no, global vars aren't terribly common in Rust, and when used, are typically done via LazyLock to prevent data races on intialization.

> I don’t know Rust, but I’ve heard pockets of unsafe code in a code base can make it hard to trust in Rust’s guarantees. The compromise feels like the language didn’t actually solve anything.

Not true at all. First, if you aren't writing device drivers/kernels or something very low level there is a high probability your program will have zero unsafe usages in it. Even if you do, you now have an effective comment that tells you where to look if you ever get suspicious behavior. The typical Rust paradigm is to let low level crates (libraries) do the unsafe stuff for you, test it thoroughly (Miri, fuzzing, etc.), and then the community builds on these crates with their safe programs. In contrast, C/C++ programs have every statement in an "unsafe block". In Rust, you know where UB can or cannot happen.

irishcoffee 3 hours ago [-]
> Even if you do, you now have an effective comment that tells you where to look if you ever get suspicious behavior.

By the time suspicious behavior happens, isn’t it kind of a critical inflection point?

For example, the news about react and next that came out. Once the code is deployed, re-deploying (especially with a systems language that quite possibly lives on an air-gapped system with a lot of rigor about updates) means you might as well have used C, the dollar cost is the same.

stouset 3 hours ago [-]
Are you with a straight face saying that occasionally having a safety bug in limited unsafe areas of Rust is functionally the same as having written the entire program in an unsafe language like C?

One, the dollar cost is not the same. The baseline floor of quality will be higher for a Rust program vs. a C program given equal development effort.

Second, the total possible footprint of entire classes of bugs is zero thanks to design features of Rust (the borrowck, sum types, data race prevention), except in a specifically delineated areas which often total zero in the vast majority of Rust programs.

irishcoffee 3 hours ago [-]
> The baseline floor of quality will be higher for a Rust program vs. a C program given equal development effort.

Hmm, according to whom, exactly?

> Second, the total possible footprint of entire classes of bugs is zero thanks to design features of Rust (the borrowck, sum types, data race prevention), except in a specifically delineated areas which often total zero in the vast majority of Rust programs.

And yet somehow the internet went down because of a program written in rust that didn’t validate input.

kibwen 3 hours ago [-]
> And yet somehow the internet went down because of a program written in rust that didn’t validate input.

What? The Cloudflare bug was from a broken system configuration that eventually cascaded into (among other things) a Rust program with hardcoded limits that crashed loudly. In no way did that Rust program bring down the internet; it was the canary, not the gas leak. Anybody trying to blame Rust for that event has no idea what they're talking about.

nu11ptr 2 hours ago [-]
> And yet somehow the internet went down because of a program written in rust that didn’t validate input.

Tell me which magic language creates programs free of errors? It would have been better had it crashed and compromised memory integrity instead of an orderly panic due to an invariant the coder didn't anticipate? Type systems and memory safety are nice and highly valuable, but we all know as computer scientists we have yet to solve for logic errors.

bigstrat2003 2 hours ago [-]
> Hmm, according to whom, exactly?

Well, Google for one. https://security.googleblog.com/2025/11/rust-in-android-move...

> And yet somehow the internet went down because of a program written in rust that didn’t validate input.

You're ignoring other factors (it wasn't just Cloudflare's rust code that led to the issue), but even setting that aside your framing is not accurate. The rust program went down because the programmer made a choice that, given invalid input, it should crash. This could happen in every language ever made. It has nothing to do with rust.

nu11ptr 3 hours ago [-]
> might as well have used C, the dollar cost is the same.

When your unsafe area is small, you put a LOT of thought/testing into those small blocks. You write SAFETY comments explaining WHY it is safe (as you start with the assumption there will be dragons there). You get lots of eyeballs on them, you use automated tools like miri to test them. So no, not even in the same stratosphere as "might as well have used C". Your probability of success vastly higher. A good Rust programmer uses unsafe judiciously, where as a C programmer barely blinks as they need ensure every single snippet of their code is safe, which in a large program, is an impossible task.

As an aside, having written a lot of C, the ecosystem and modern constructs available in Rust make writing large scale programs much easier, and that isn't even considering the memory safety aspect I discuss above.

mh2266 3 hours ago [-]
This just skips the:

> First, if you aren't writing device drivers/kernels or something very low level there is a high probability your program will have zero unsafe usages in it.

from the original comment. Meanwhile all C code is implicitly “unsafe”. Rust at least makes it explicit!

But even if you ignore memory safety issues bypassed by unsafe, Rust forces you to handle errors, it doesn’t let you blow up on null pointers with no compiler protection, it allows you to represent your data exhaustively with sum types, etc etc etc

irishcoffee 3 hours ago [-]
Isn’t rust proffered up as a systems language? One that begged to be accepted into the Linux kernel?

Don’t device drivers live in the Linux kernel tree?

So, unsafe code is generally approved in device driver code?

Why not just use C at that point?

3 hours ago [-]
lowbloodsugar 32 minutes ago [-]
So I've got a crate I built that has a type that uses unsafe. Couple of things I've learned. First, yes, my library uses unsafe, but anyone who uses it doesn't have to deal with that at all. It behaves like a normal implementation of its type, it just uses half the memory. Outside of developing this one crate, I've never used unsafe.

Second, unsafe means the author is responsible for making it safe. Safe in rust means that the same rules must apply as unsafe code. It does not mean that you don't have to follow the rules. If one instead used it to violate the rules, then the code will certainly cause crashes.

I can see that some programmers would just use unsafe to "get around a problem" caused by safe rust enforcing those rules, and doing so is almost guaranteed to cause crashes. If the compiler won't let you do something, and you use unsafe to do it anyway, there's going to be a crash.

If instead we use unsafe to follow the rules, then it won't crash. There are tools like Miri that allow us to test that we haven't broken the rules. The fact that Miri did find two issues in my crate shows that unsafe is difficult to get right. My crate does clever bit-tricks and has object graphs, so it has to use unsafe to do things like having back pointers. These are all internal, and you can use the crate in safe rust. If we use unsafe to implement things like doubly-linked lists, then things are fine. If we use unsafe to allow multiple threads to mutate the same pointers (Against The Rules), then things are going to crash.

The thing is, when you are programming in C or C++, it's the same as writing unsafe rust all the time. In C/C++, the "pocket of unsafe code" is the entire codebase. So sure, you can write safe C, like I can write safe "unsafe rust". But 99% of the code I write is safe rust. And there's no equivalent in C or C++.

10000truths 4 hours ago [-]
The reason I really like Zig is because there's finally a language that makes it easy to gracefully handle memory exhaustion at the application level. No more praying that your program isn't unceremoniously killed just for asking for more memory - all allocations are assumed fallible and failures must be handled explicitly. Stack space is not treated like magic - the compiler can reason about its maximum size by examining the call graph, so you can pre-allocate stack space to ensure that stack overflows are guaranteed never to happen.

This first-class representation of memory as a resource is a must for creating robust software in embedded environments, where it's vital to frontload all fallibility by allocating everything needed at start-up, and allow the application freedom to use whatever mechanism appropriate (backpressure, load shedding, etc) to handle excessive resource usage.

kibwen 4 hours ago [-]
> No more praying that your program isn't unceremoniously killed just for asking for more memory - all allocations are assumed fallible and failures must be handled explicitly.

But for operating systems with overcommit, including Linux, you won't ever see the act of allocation fail, which is the whole point. All the language-level ceremony in the world won't save you.

10000truths 3 hours ago [-]
Sure, but you can do the next best thing, which is to control precisely when and where those allocations occur. Even if the possibility of crashing is unavoidable, there is still huge operational benefit in making it predictable.

Simplest example is to allocate and pin all your resources on startup. If it crashes, it does so immediately and with a clear error message, so the solution is as straightforward as "pass bigger number to --memory flag" or "spec out larger machine".

kibwen 2 hours ago [-]
No, this is still misunderstanding.

Overcommit means that the act of memory allocation will not report failure, even when the system is out of memory.

Instead, failure will come at an arbitrary point later, when the program actually attempts to use the aforementioned memory that the system falsely claimed had been allocated.

Allocating all at once on startup doesn't help, because the program can still fail later when it tries to actually access that memory.

the_duke 2 hours ago [-]
To be fair, you can enforce this just by filling all the allocated memory with zero, so it's possible to fail at startup.

Or, even simpler, just turn off over-commit.

But if swap comes into the mix, or just if the OS decides it needs the memory later for something critical, you can still get killed.

bluGill 21 minutes ago [-]
I would be suprised if some os detects the page of zeros and removes that allocation until you need it. this seems like a common enough case as to make it worth it when memory is low. I'm not aware of any that do, but it wouldn't be that hard and so seems like someone would try it.
10000truths 2 hours ago [-]
Which is why I said "allocate and pin". POSIX systems have mlock()/mlockall() to prefault allocated memory and prevent it from being paged out.
interroboink 59 minutes ago [-]
Random curious person here: does mlock() itself cause the pre-fault? Or do you have to scribble over that memory yourself, too?

(I understand that mlock prevents paging-out, but in my mind that's a separate concern from pre-faulting?)

10000truths 3 minutes ago [-]
FreeBSD and OpenBSD explicitly mention the prefaulting behavior in the mlock(2) manpage. The Linux manpage alludes to it in that you have to explicitly pass the MLOCK_ONFAULT flag to the mlock2() variant of the syscall in order to disable the prefaulting behavior.
kibwen 1 hours ago [-]
Aha, my apologies, I overlooked that.
wavemode 3 hours ago [-]
I imagine people who care about this sort of thing are happy to disable overcommit, and/or run Zig on embedded or specialized systems where it doesn't exist.
dlisboa 3 hours ago [-]
There are far more people running/writing Zig on/for systems with overcommit than not. Most of the hype around Zig come from people not in the embedded world.
xyzzy_plugh 3 hours ago [-]
If we can produce a substantial volume of software that can cope with allocation failures then the idea of using something than overcommit as the default becomes feasible.

It's not a stretch to imagine that a different namespace might want different semantics e.g. to allow a container to opt out of overcommit.

It is hard to justify the effort required to enable this unless it'll be useful for more than a tiny handful of users who can otherwise afford to run off an in-house fork.

kibwen 2 hours ago [-]
> If we can produce a substantial volume of software that can cope with allocation failures then the idea of using something than overcommit as the default becomes feasible.

Except this won't happen, because "cope with allocation failure" is not something that 99.9% of programs could even hope to do.

Let's say that you're writing a program that allocates. You allocate, and check the result. It's a failure. What do you do? Well, if you have unneeded memory lying around, like a cache, you could attempt to flush it. But I don't know about you, but I don't write programs that randomly cache things in memory manually, and almost nobody else does either. The only things I have in memory are things that are strictly needed for my program's operation. I have nothing unnecessary to evict, so I can't do anything but give up.

The reason that people don't check for allocation failure isn't because they're lazy, it's because they're pragmatic and understand that there's nothing they could reasonably do other than crash in that scenario.

AlotOfReading 4 minutes ago [-]
Have you honestly thought about how you could handle the situation better than an crash?

For example, you could finish writing data into files before exiting gracefully with an error. You could (carefully) output to stderr. You could close remote connections. You could terminate the current transaction and return an error code. Etc.

Most programs are still going to terminate eventually, but they can do that a lot more usefully than a segfault from some instruction at a randomized address.

bluGill 19 minutes ago [-]
Even when I have a cache - it is probably in a different code path / module and it would be a terrible architecture that let me access that code.
wavemode 3 hours ago [-]
I never said that all Zig users care about recovering from allocation failure.
incompatible 3 hours ago [-]
I don't know Zig. The article says "Many people seem confused about why Zig should exist if Rust does already." But I'd ask instead why does Zig exist when C does already? It's just a "better" C? But has the drawback that makes C problematic for development, manual memory management? I think you are better off using a language with a garbage collector, unless your usage really needs manual management, and then you can pick between C, Rust, and Zig (and C++ and a few hundred others, probably.)
throwawaymaths 27 minutes ago [-]
yeah, its a better c, but like wouldnt it be nice if c had stadardized fat pointers so that if you move from project to project you don't have to triple check the semantics? for example and like say 50+ "learnings" from 40 years c that are canonized and first class in the language + stdlib
Guvante 4 hours ago [-]
If you are pre-allocating Rust would handle that decently as well right?

Certainly I agree that allocations in your dependencies (including std) are more annoying in Rust since it uses panics for OOM.

The no-std set of crates is all setup to support embedded development.

munificent 2 hours ago [-]
> Stack space is not treated like magic - the compiler can reason about its maximum size by examining the call graph, so you can pre-allocate stack space to ensure that stack overflows are guaranteed never to happen.

How does that work in the presence of recursion or calls through function pointers?

10000truths 2 hours ago [-]
Recursion: That's easy, don't. At least, not with a call stack. Instead, use a stack container backed by a bounded allocator, and pop->process->push in a loop. What would have been a stack overflow is now an error.OutOfMemory enum that you can catch and handle as desired. All that said, there is a proposal that addresses making recursive functions more friendly to static analysis [0].

Function pointers: Zig has a proposal for restricted function types [1], which can be used to enforce compile-time constraints on the functions that can be assigned to a function pointer.

[0]: https://github.com/ziglang/zig/issues/1006 [1]: https://github.com/ziglang/zig/issues/23367

2 hours ago [-]
deathanatos 3 hours ago [-]
> I’m not the first person to pick on this particular Github comment, but it perfectly illustrates the conceptual density of Rust:

But you only need about 5% of the concepts in that comment to be productive in Rust. I don't think I've ever needed to know about #[fundamental] in about 12 years or so of Rust…

> In both Go and Rust, allocating an object on the heap is as easy as returning a pointer to a struct from a function. The allocation is implicit. In Zig, you allocate every byte yourself, explicitly. […] you have to call alloc() on a specific kind of allocator,

> In Go and Rust and so many other languages, you tend to allocate little bits of memory at a time for each object in your object graph. Your program has thousands of little hidden malloc()s and free()s, and therefore thousands of different lifetimes.

Rust can also do arena allocations, and there is an allocator concept in Rust, too. There's just a default allocator, too.

And usually a heap allocation is explicit, such as with Box::new, but that of course might be wrapped behind some other type or function. (E.g., String, Vec both alloc, too.)

> In Rust, creating a mutable global variable is so hard that there are long forum discussions on how to do it.

The linked thread is specifically about creating a specific kind of mutable global, and has extra, special requirements unique to the thread. The stock "I need a global" for what I'd call a "default situation" can be as "simple" as,

  static FOO: Mutex<T> = Mutex::new(…);
Since mutable globals are inherently memory unsafe, you need the mutex.

(Obviously, there's usually an XY problem in such questions, too, when someone wants a global…)

To the safety stuff, I'd add that Rust not only champions memory safety, but the type system is such that I can use it to add safety guarantees to the code I write. E.g., String can guarantee that it always represents a Unicode string, and it doesn't really need special support from the language to do that.

Mawr 25 minutes ago [-]
> Rust can also do arena allocations,

Is there a language that can't?

The author isn't saying it's literally impossible to batch allocate, just that the default happy path of programming in Rust & Go tends to produce a lot of allocations. It's a take more nuanced than the binary possible vs impossible.

seanmcdirmid 22 minutes ago [-]
Pretty hard to do arena allocation in Java without JVM primitive support.
dnautics 32 minutes ago [-]
> there is an allocator concept in Rust, too.

aren't allocators types in rust?

suppose you had an m:n system (like say an evented http request server split over several threads so that a thread might handle several inbound requests), would you be able to give each request its own arena?

Capricorn2481 2 hours ago [-]
>> In Go and Rust and so many other languages, you tend to allocate little bits of memory at a time for each object in your object graph. Your program has thousands of little hidden malloc()s and free()s, and therefore thousands of different lifetimes.

> Rust can also do arena allocations, and there is an allocator concept in Rust, too. There's just a default allocator, too.

Thank you. I've seen this repeated so many times. Casey Muratori did a video on batch allocations that was extremely informative, but also stupidly gatekeepy [1]. I think a lot of people who want to see themselves as super devs have latched onto this point without even understanding it. They talk like RAII makes it impossible to batch anything.

Last year the Zig Software Foundation wrote about Asahi Lina's comments around Rust and basically implied she was unknowingly introducing these hidden allocations, citing this exact Casey Muratori video. And it was weird. A bunch of people pointed out the inaccuracies in the post, including Lina [2]. That combined with Andrew saying Go is for people without taste (not that I like Go myself), I'm not digging Zig's vibe of dunking on other companies and languages to sell their own.

[1] https://www.youtube.com/watch?v=xt1KNDmOYqA [2] https://lobste.rs/s/hxerht/raii_rust_linux_drama

zozbot234 2 hours ago [-]
"Batch allocation" in Rust is just a matter of Box-ing a custom-defined tuple of objects as opposed to putting each object in its own little Box. You can even include MaybeUninit's in the tuple that are then initialized later in unsafe code, and transmuted to the initialized type after-the-fact. You don't need an allocator library at all for this easy case, that's more valuable when the shape of allocations is in fact dynamic.
kibwen 1 hours ago [-]
> You don't need an allocator library at all for this easy case, that's more valuable when the shape of allocations is in fact dynamic.

Though I'd still reach for something like Bumpalo ( https://crates.io/crates/bumpalo ) unless I had good reason to avoid it.

vlovich123 4 hours ago [-]
Re UB:

> The idea seems to be that you can run your program enough times in the checked release modes to have reasonable confidence that there will be no illegal behavior in the unchecked build of your program. That seems like a highly pragmatic design to me.

This is only pragmatic if you ignore the real world experience of sanitizers which attempt to do the same thing and failing to prevent memory safety and UB issues in deployed C/C++ codebases (eg Android definitely has sanitizers running on every commit and yet it wasn’t until they switched to Rust that exploits started disappearing).

wrs 4 hours ago [-]
> In Go, a slice is a fat pointer to a contiguous sequence in memory, but a slice can also grow, meaning that it subsumes the functionality of Rust’s Vec<T> type and Zig’s ArrayList.

Well, not exactly. This is actually a great example of the Go philosophy of being "simple" while not being "easy".

A Vec<T> has identity; the memory underlying a Go slice does not. When you call append(), a new slice is returned that may or may not share memory with the old slice. There's also no way to shrink the memory underlying a slice. So slices actually very much do not work like Vec<T>. It's a common newbie mistake to think they do work like that, and write "append(s, ...)" instead of "s = append(s, ...)". It might even randomly work a lot of the time.

Go programmer attitude is "do what I said, and trust that I read the library docs before I said it". Rust programmer attitude is "check that I did what I said I would do, and that what I said aligns with how that library said it should be used".

So (generalizing) Go won't implement a feature that makes mistakes harder, if it makes the language more complicated; Rust will make the language more complicated to eliminate more mistakes.

RVuRnvbM2e 59 minutes ago [-]
> There's also no way to shrink the memory underlying a slice.

Sorry, that is incorrect: https://pkg.go.dev/slices#Clip

> It's a common newbie mistake to think they do work like that, and write "append(s, ...)" instead of "s = append(s, ...)". It might even randomly work a lot of the time.

"append(s, ...)" without the assignment doesn't even compile. So your entire post seems like a strawman?

https://go.dev/play/p/icdOMl8A9ja

> So (generalizing) Go won't implement a feature that makes mistakes harder, if it makes the language more complicated

No, I think it is more that the compromise of complicating the language that is always made when adding features is carefully weighed in Go. Less so in other languages.

auxiliarymoose 4 hours ago [-]
Writing "append(s, ...)" instead of "s = append(s, ...)" results in a compiler error because it is an unused expression. I'm not sure how a newbie could make this mistake since that code doesn't compile.
dlisboa 3 hours ago [-]
> Go programmer attitude is "do what I said, and trust that I read the library docs before I said it".

I agree and think Go gets unjustly blamed for some things: most of the foot guns people say Go has are clearly laid out in the spec/documentation. Are these surprising behaviors or did you just not read?

Getting a compiler and just typing away is not a great way of going about learning things if that compiler is not as strict.

int_19h 3 hours ago [-]
It's not unjust to blame the tool if it behaves contrary to well established expectation, even if that's documented - it's just poor ergonomics then.
throwawaymaths 23 minutes ago [-]
ironically with zig most of the things that violate expectations are keywords. so you run head first into a whole ton of them when you first start (but at least it doesn't compile) and then it you have a very solid mental model of whats going on.
dlisboa 2 hours ago [-]
Outside very simple programming techniques there is no such thing as well-established when it comes to PL. If one learns more than a handful of languages they’ll see multiple ways of doing the same thing.

As an example all three of the languages in the article have different error handling techniques, none of which are actually the most popular choice.

Built in data structures in particular, each language does them slightly differently to there’s no escaping learning their peculiarities.

FridgeSeal 1 hours ago [-]
“Clearly it’s your fault for not realising that we embedded razor blades in our hammers! What did you think, that you could safely pick up a tool?”
librasteve 5 hours ago [-]
I love this take - partly because I agree with it - but mostly because I think that this is the right way to compare PLs (and to present the results). It is honest in the way it ascribes strengths and weaknesses, helping to guide, refine, justify the choice of language outside of job pressures.

I am sad that it does not mention Raku (https://raku.org) ... because in my mind there is a kind of continuum: C - Zig - C++ - Rust - Go ... OK for low level, but what about the scriptier end - Julia - R - Python - Lua - JavaScript - PHP - Raku - WL?

NooneAtAll3 4 hours ago [-]
what's WL?
shawn_w 3 hours ago [-]
Wolfram Language?
librasteve 4 hours ago [-]
I tried to get an LLM to write a Raku chapter in the same vein - naah. Had to write it myself:

Raku

Raku stands out as a fast way to working code, with a permissive compiler that allows wide expression.

Its an expressive, general-purpose language with a wide set of built-in tools. Features like multi-dispatch, roles, gradual typing, lazy evaluation, and a strong regex and grammar system are part of its core design. The language aims to give you direct ways to reflect the structure of a problem instead of building abstractions from scratch.

The grammar system is the clearest example. Many languages treat parsing as a specialized task requiring external libraries. Raku instead provides a declarative syntax for defining rules and grammars, so working with text formats, logs, or DSLs often requires less code and fewer workarounds. This capability blends naturally with the rest of the language rather than feeling like a separate domain.

Raku programs run on a sizeable VM and lean on runtime dispatch, which means they typically don’t have the startup speed or predictable performance profile of lower-level or more static languages. But the model is consistent: you get flexibility, clear semantics, and room to adjust your approach as a problem evolves. Incremental development tends to feel natural, whether you’re sketching an idea or tightening up a script that’s grown into something larger.

The language’s long development history stems from an attempt to rethink Perl, not simply modernize it. That history produced a language that tries to be coherent and pleasant to write, even if it’s not small. Choose Raku if you want a language that let's you code the way you want, helps you wrestle with the problem and not with the compiler.

Rikudou 4 hours ago [-]
I think the Go part is missing a pretty important thing: the easiest concurrency model there is. Goroutines are one of the biggest reasons I even started with Go.
auxiliarymoose 3 hours ago [-]
Agreed. Rob Pike presented a good talk "Concurrency is not Parallelism" which explains the motivations behind Go's concurrency model: https://youtu.be/oV9rvDllKEg

Between the lack of "colored functions" and the simplicity of communicating with channels, I keep surprising myself with how (relatively) quick and easy it is to develop concurrent systems with correct behavior in Go.

PaulKeeble 3 hours ago [-]
Its a bit messy to do parallelism with it but it still works and its a consistent pattern and their are libraries that add it for the processing of slices and such. It could be made easier IMO, they are trying to dissuade its use but its actually really common to want to process N things distributed across multiple CPUs nowadays.
silisili 2 hours ago [-]
True. But in my experience, the pattern of just using short lived goroutines via errgroup or a channel based semaphore, will typically get you full utilization across all cores assuming your limit is high enough.

Perhaps less guaranteed in patterns that feed a fixed limited number of long running goroutines.

kibwen 1 hours ago [-]
I'll disagree with you there. Structured concurrency is the easiest concurrency model there is: https://vorpus.org/blog/notes-on-structured-concurrency-or-g...
macintux 2 hours ago [-]
> the easiest concurrency model there is

Erlang programmers might disagree with you there.

kibwen 59 minutes ago [-]
Erlang is great for distributed systems. But my bugbear is when people look at how distributed systems are inherently parallel, and then look at a would-be concurrent program and go, "I know, I'll make my program concurrent by making it into a distributed system".

But distributed systems are hard. If your system isn't inherently distributed, then don't rush towards a model of concurrency that emulates a distributed system. For anything on a single machine, prefer structured concurrency.

throwawaymaths 18 minutes ago [-]
have you ever deployed an erlamg system?

the biggest bugbear for concurrent systems is mutable shared data. by inherently being distributable you basically "give up on that" so for concurrent erlang systems you ~mostly don't even try.

if for no other reason than that erlang is saner than go for concurrency

like goroutines aren't inherently cancellable, so you see go programmers build out the kludgey context to handle those situations and debugging can get very tricky

dmoy 5 hours ago [-]
For a lot of stuff what I really want is golang but with better generics and result/error/enum handling like rust.
vips7L 3 hours ago [-]
Me too. There’s a huge market for a natively compiled language with GC that has a better type system than Go.

The options I’ve seen so far are: OCaml, D, Swift, Nim, Crystal, but none of them have seen to be able to capture a significant market.

Yoric 5 hours ago [-]
Have you tried OCaml? With the latest versions, it also has an insanely powerful concurrency model. As far as I understand (I haven't looked at the benchmarks myself), it's also performance-competitive with Go.
zozbot234 4 hours ago [-]
There's also ReasonML if you want an OCaml with curly braces like C. But both are notably missing the high-performance concurrent GC that ships with Golang out of the box.
throwaway894345 4 hours ago [-]
How's the build tooling these days? Last I tried, it used some jbuild/dune + makefiles thing that was really painful to get up and running. Also there were multiple standard libraries and (IIRC) async runtimes that wouldn't play nicely together. The syntax and custom operators was also a thing that I could not stop stubbing my toes on--while I previously thought syntax was a relatively unimportant concern, my experience with OCaml changed my mind. :)

Also, at least at the time, the community was really hostile, but that was true of C++, Ada, and Java communities as well well. But I think those guys have chilled out, so maybe OCaml has too?

Yoric 4 hours ago [-]
I'm re-discovering OCaml these days after an OCaml burnout quite a few years ago, courtesy of my then employer, so I'm afraid I can't answer these questions reliably :/

So far, I like what I've seen.

myaccountonhn 4 hours ago [-]
Ocaml community is chill and helpful, and dune works great with really good compilation speeds.

Its a really nice language

Philpax 4 hours ago [-]
You want https://github.com/borgo-lang/borgo, but that project is dead. You might be interested in Gleam?
evanmoran 5 hours ago [-]
I thought the recent error proposal was quite interesting even if it didn't go through: https://github.com/golang/go/issues/71528

My hope is they will see these repeated pain points and find something that fits the error/result/enum issues people have. (Generics will be harder, I think)

Rikudou 4 hours ago [-]
Didn't they say they're not accepting any new proposals for error handling?

I kinda got used to it eventually, but I'll never ever consider not having enums a good thing.

mixedCase 5 hours ago [-]
OCaml is the closest match I'm aware of.
hgs3 2 hours ago [-]
Are you familiar with Zig's error handling? It's arguably more Go-like than the Rust approach.
scythmic_waves 4 hours ago [-]
Borgo [1] is basically that.

Though I think it's more of a hobby language. The last commit was > 1 year ago.

[1] https://news.ycombinator.com/item?id=40211891

throwaway894345 5 hours ago [-]
I cautiously agree, with the caveat that while I thought I would really like Rust's error handling, it has been painful in practice. I'm sure I'm holding it wrong, but so far I have tried:

* thiserror: I spend ridiculous and unpredictable amounts of time debugging macro expansions

* manually implementing `Error`, `From`, etc traits: I spend ridiculous though predictable amounts of time implementing traits (maybe LLMs fix this?)

* anyhow: this gets things done, but I'm told not to expose these errors in my public API

Beyond these concerns, I also don't love enums for errors because it means adding any new error type will be a breaking change. I don't love the idea of committing to that, but maybe I'm overthinking?

And when I ask these questions to various Rust people, I often get conflicting answers and no one seems to be able to speak with the authority of canon on the subject. Maybe some of these questions have been answered in the Rust Book since I last read it?

By contrast, I just wrap Go errors with `fmt.Errorf("opening file `%s`: %w", filePath, err)` and handle any special error cases with `errors.As()` and similar and move on with life. It maybe doesn't feel _elegant_, but it lets me get stuff done.

saghm 53 minutes ago [-]
If you're willing to do what you're saying in Go, exposing the errors from anyhow would basically be the same thing. The only difference is that Rust also gives all those other options you mention. The point about other people saying not to do it doesn't really seem like it's something you need to be super concerned with; for all we know, people might tell you the same thing about Go if it had the ability for similar APIs, but it doesn't
iterance 4 hours ago [-]
I will at least remark that adding a new error to an enum is not a breaking change if they are marked #[non_exhaustive]. The compiler then guarantees that all match statements on the enum contain a generic case.

However, I wouldn't recommend it. Breakage over errors is not necessarily a bad thing. If you need to change the API for your errors, and downstreams are required to have generic cases, they will be forced to silently accept new error types without at least checking what those new error types are for. This is disadvantageous in a number of significant cases.

grufkork 4 hours ago [-]
Indeed, there's almost always a solution to "inergonomics" in Rust, but most are there to provide a guarantee or express an assumption to increase the chance that your code will do what's intended. While that safety can feel a bit exaggerated even for some large systems projects, for a lot of things Rust is just not the right tool if you don't need the guarantees.

On that topic, I've looked some at building games in Rust but I'm thinking it mostly looks like you're creating problems for yourself? Using it for implementing performant backend algorithms and containerised logic could be nice though.

snuxoll 4 hours ago [-]
> Beyond these concerns, I also don't love enums for errors because it means adding any new error type will be a breaking change. I don't love the idea of committing to that, but maybe I'm overthinking?

Is it a new error condition that downstream consumers want to know about so they can have different logic? Add the enum variant. The entire point of this pattern is to do what typed exceptions in Java were supposed to do, give consuming code the ability to reason about what errors to expect, and handle them appropriately if possible.

If your consumer can't be reasonably expected to recover? Use a generic failure variant, bonus points if you stuff the inner error in and implement std::Error so consumers can get the underlying error by calling .source() for debugging at least.

> By contrast, I just wrap Go errors with `fmt.Errorf("opening file `%s`: %w", filePath, err)` and handle any special error cases with `errors.As()` and similar and move on with life. It maybe doesn't feel _elegant_, but it lets me get stuff done.

Nothing stopping you from doing the same in Rust, just add a match arm with a wildcard pattern (_) to handle everything but your special cases.

In fact, if you suspect you are likely to add additional error variants, the `#[non_exhaustive]` attribute exists explicitly to handle this. It will force consumers to provide a match arm with a wildcard pattern to prevent additions to the enum from causing API incompatibility. This does come with some other limitations, so RTFM on those, but it does allow you to add new variants to an Error enum without requiring a major semver bump.

Yoric 4 hours ago [-]
FWIW `fmt.Errorf("opening file %s: %w", filePath, err)` is pretty much equivalent to calling `err.with_context(|| format!("opening file {}", path))?` with anyhow.

What `thiserror` or manually implementing `Error` buys you is the ability to actually do something about higher-level errors. In Rust design, not doing so in a public facing API is indeed considered bad practice. In Go, nobody seems to care about that, which of course makes code easier to write, but catching errors quickly becomes stringly typed. Yes, it's possible to do it correctly in Go, but it's ridiculously complicated, and I don't think I've ever seen any third-party library do it correctly.

That being said, I agree that manually implementing `Error` in Rust is way too time-consuming. There's also the added complexity of having to use a third-party crate to do what feels like basic functionality of error-handling. I haven't encountered problems with `thiserror` yet.

> Beyond these concerns, I also don't love enums for errors because it means adding any new error type will be a breaking change. I don't love the idea of committing to that, but maybe I'm overthinking?

If you wish to make sure it's not a breaking change, mark your enum as `#[non_exhaustive]`. Not terribly elegant, but that's exactly what this is for.

Hope it helped a bit :)

thijsr 4 hours ago [-]
> I also don't love enums for errors because it means adding any new error type will be a breaking change

You can annotate your error enum with #[non_exhaustive], then it will not be a breaking change if you add a new variant. Effectively, you enforce that anybody doing a match on the enum must implement the "default" case, i.e. that nothing matches.

written-beyond 4 hours ago [-]
You have to chill with rust. Just anyhow macro wrap your errors and just log them out. If you have a specific use case that relies on using that specific error just use that at the parent stack.
simgt 4 hours ago [-]
I personally like the flexibility it provides. You can go from very granular with an error type per function and an enum variant per error case, or very coarse with an error type for a whole module that holds a string. Use thiserror to make error types in libraries, and anyhow in programs to handle them.
oncallthrow 4 hours ago [-]
vlang
publicdebates 5 hours ago [-]
Good write up, I like where you're going with this. Your article reads like a recent graduate who's full of excitement and passion for the wonderful world of programming, and just coming into the real world for the first time.

For Go, I wouldn't say that the choice to avoid generics was either intentional or minimalist by nature. From what I recall, they were just struggling for a long time with a difficult decision, which trade-offs to make. And I think they were just hoping that, given enough time, the community could perhaps come up with a new, innovative solution that resolves them gracefully. And I think after a decade they just kind of settled on a solution, as the clock was ticking. I could be wrong.

For Rust, I would strongly disagree on two points. First, lifetimes are in fact what tripped me up the most, and many others, famously including Brian Kernighan, who literally wrote the book on C. Second, Rust isn't novel in combining many other ideas into the language. Lots of languages do that, like C#. But I do recall thinking that Rust had some odd name choices for some features it adopted. And, not being a C++ person myself, it has solutions to many problems I never wrestled with, known by name to C++ devs but foreign to me.

For Zig's manual memory management, you say:

> this is a design choice very much related to the choice to exclude OOP features.

Maybe, but I think it's more based on Andrew's need for Data-Oriented Design when designing high performance applications. He did a very interesting talk on DOD last year[1]. I think his idea is that, if you're going to write the highest performance code possible, while still having an ergonomic language, you need to prioritize a whole different set of features.

[1] https://www.youtube.com/watch?v=IroPQ150F6c

gwd 4 hours ago [-]
> For Go, I wouldn't say that the choice to avoid generics was either intentional or minimalist by nature. From what I recall, they were just struggling for a long time with a difficult decision, which trade-offs to make.

Indeed, in 2009 Russ Cox laid out clearly the problem they had [1], summed up thus:

> The generic dilemma is this: do you want slow programmers, slow compilers and bloated binaries, or slow execution times?

My understanding is that they were eventually able to come up with something clever under the hood to mitigate that dilemma to their satisfaction.

[1] https://research.swtch.com/generic

mirashii 4 hours ago [-]
I’m not sure there’s anything clever that resolved the issues, they just settled on slow execution times by accepting a dynamic dispatch on generics.
zozbot234 4 hours ago [-]
Ironically, the latest research by Google has now conclusively shown that Rust programmers aren't really any "slower" or less productive than Go programmers. That's especially true once you account for the entire software lifecycle, including production support and maintenance.
samdoesnothing 16 minutes ago [-]
"research", it's a bunch of rust fans at google who are claiming it, without any real serious methodology.
4 hours ago [-]
kachapopopow 4 hours ago [-]
I could never get into zig purely because of the syntax and I know I am not alone, can someone explain the odd choices that were taken when creating zig?

the most odd one probably being 'const expected = [_]u32{ 123, 67, 89, 99 };'

and the 2nd most being the word 'try' instead of just ?

the 3rd one would be the imports

and `try std.fs.File.stdout().writeAll("hello world!\n");` is not really convincing either for a basic print.

usrnm 4 hours ago [-]
I will never understand people bashing other languages for their syntax and readability and then saying that they prefer Rust. Async Rust is the ugliest and least readable language I've ever seen and I've done a lot of heavily templated C++
kachapopopow 3 hours ago [-]
I don't really prefer rust, but I'd take that syntax over zig, c++ templating is just evil though. Also it's not about readability, but rather the uniqueness to it.
the__alchemist 3 hours ago [-]
Concur, but non-async rust is a different matter!
bigstrat2003 2 hours ago [-]
Yeah, I like rust but I hate async. I wish it had never been added to the language, because it has so thoroughly infected the crate ecosystem when most programs just do not need async.
kibwen 4 hours ago [-]
> Async Rust is the ugliest and least readable language I've ever seen and I've done a lot of heavily templated C++

No, this is a wild claim that shows you've either never written async Rust or never written heavily templated C++. Feel free to give code examples if you want to suggest otherwise.

rustystump 3 hours ago [-]
Every language i am not deeply familiar with is disgusting.

But for real the ratings for me stem from how much arcane symbology i must newly memorize. I found rust to be up there but digestible. The thought of c++ makes me want to puke but not over the syntax.

kachapopopow 1 hours ago [-]

  template<auto V>
  concept non_zero = (V != 0);

  template<typename T>
  concept arithmetic = std::is_arithmetic_v<T>;

  template<arithmetic T>
  requires non_zero<T{42}>
  struct complicated {
      template<auto... Values>
      using nested_alias = std::tuple<
          std::integral_constant<decltype(Values), Values>...,
          std::conditional_t<(Values > 0 && ...), T, std::nullptr_t>
      >;

      template<typename... Ts>
      static constexpr auto process() {
          return []<std::size_t... Is>(std::index_sequence<Is...>) {
              return nested_alias<(sizeof(Ts) + Is)...>{};
          }(std::make_index_sequence<sizeof...(Ts)>{});
      }
  };
I most definitely agree.
von_lohengramm 4 hours ago [-]
> and the 2nd most being the word 'try' instead of just ?

All control flow in Zig is done via keyword

dwb 4 hours ago [-]
These are extremely trivial, to the point that I don’t really know what you’re complaining about. What would expect or prefer?
kachapopopow 3 hours ago [-]
it's not about triviality, but why not use what is generally accepted already, why did zig decide to be different?
int_19h 3 hours ago [-]
What is "generally accepted" though?

If you mean C-style declarations, the fact that tools such as https://linux.die.net/man/1/cdecl even exist to begin with shows what's wrong with it.

kachapopopow 2 hours ago [-]
<auto/type/name> <name/type> (array?) (:)= (value)

<fn> <generic> <name>(<type/argument>[:] <type/argument> [(->/:) type]

[import/use/using] (<package>[/|:|::|.]<type> | "file") (ok header files are a relic of the past I have to admit that)

I tried writing zig and as someone who has pretty much written in every commonly used language it just felt different enough where I kept having to look up the syntax.

throwawaymaths 4 hours ago [-]
'const expected = [_]u32{ 123, 67, 89, 99 };'

constant array with u32, and let the compiler figure out how many of em there are (i reserve the right to change it in the future)

oncallthrow 4 hours ago [-]
> Other features common in modern languages, like tagged unions or syntactic sugar for error-handling, have not been added to Go.

> It seems the Go development team has a high bar for adding features to the language. The end result is a language that forces you to write a lot of boilerplate code to implement logic that could be more succinctly expressed in another language.

Being able to implement logic more succinctly is not always a good thing. Take error handling syntactic sugar for example. Consider these two snippets:

    let mut file = File::create("foo.txt")?;
and:

    f, err := os.Create("filename.txt")
    if err != nil {
        return fmt.Errorf("failed to create file: %w", err)
    }
The first code is more succinct, but worse: there is no context added to the error (good luck debugging!).

Sometimes, being forced to write code in a verbose manner makes your code better.

edflsafoiewq 4 hours ago [-]
Python's

    f = open('foo.txt', 'w')
is even more succinct, and the exception thrown on failure will not only contain the reason, but the filename and the whole backtrace to the line where the error occurred.
9rx 3 hours ago [-]
But no context, so in the real world you need to write:

    try:
        f = open('foo.txt', 'w')
    except Exception as e:
        raise NecessaryContext("important information") from e
Else your callers are in for a nightmare of a time trying to figure out why an exception was thrown and what to do with it. Worse, you risk leaking implementation details that the caller comes to depend on which will also make your own life miserable in the future.
tayo42 3 hours ago [-]
How is a stack trace with line numbers and a message for the exception it self not enough information for why an exception was thrown?

The exceptions from something like open are always pretty clear. Like, the files not found, and here is the exact line of code and the entire call stack. what else do you want to know to debug?

9rx 2 hours ago [-]
It's enough information if you are happy to have a fragile API, but why would you purposefully make life difficult not only for yourself, but the developers who have their code break every time you decide to change something that should only be an internal implementation detail?

Look, if you're just writing a script that doesn't care about failure — where when something goes wrong you can exit and let the end user deal with whatever the fault was, you don't have to worry about this. But Go is quite explicitly intended to be a systems language, not a scripting language. That shit doesn't fly in systems.

While you can, of course, write systems in Python, it is intended to be a scripting language, so I understand where you are coming from thinking in terms of scripts, but it doesn't exactly fit the rest of the discussion that is about systems.

tayo42 1 hours ago [-]
That makes even less sense becasue go errors provide even less info other then a chain of messages. They might as well be lists of strings. You can maybe reassbmle a call stack your self if all of the error handlers are vigalente about wrapping
arccy 1 hours ago [-]
like why did the program even choose to open this file? a stack trace is useless if your code is even a little bit generic
verdverm 3 hours ago [-]
We were taught not to use exceptions for control flow, and reading a file which does not exist is a pretty normal thing to handle in code flow, rather than exceptions.

That simple example in Python is missing all the other stuff you have to put around it. Go would have another error check, but I get to decide, at that point in the execution, how I want to handle it in this context

pansa2 3 hours ago [-]
In Python, it’s common to use exceptions for control flow. Even exiting a loop is done via an exception: `StopIteration`.
arccy 1 hours ago [-]
isn't break more normal
pansa2 11 minutes ago [-]
I’d say it’s more common for iterator-based loops to run to completion than to hit a `break` statement. The `StopIteration` exception is how the iterator signals that completion.
oncallthrow 4 hours ago [-]
> the exception thrown on failure will not only contain the reason, but the filename and the whole backtrace to the line where the error occurred.

... with no other context whatsoever, so you can't glean any information about the call stack that led to the exception.

Exceptions are really a whole different kettle of fish (and in my opinion are just strictly worse than even the worst errors-as-values implementations).

reissbaker 3 hours ago [-]
Your Go example included zero information that Python wouldn't give you out-of-the-box. And FWIW, since this is "Go vs Rust vs Zig," both Rust and Zig allow for much more elegant handling than Go, while similarly forcing you to make sure your call succeeded before continuing.
howenterprisey 4 hours ago [-]
You can just as easily add context to the first example or skip the wrapping in the second.
snuxoll 4 hours ago [-]
Especially since the second example only gives you a stringly-typed error.

If you want to add 'proper' error types, wrapping them is just as difficult in Go and Rust (needing to implement `Error` in Go or `std::Error` in Rust). And, while we can argue about macro magic all day, the `thiserror` crate makes said boilerplate a non-issue and allows you to properly propagate strongly-typed errors with context when needed (and if you're not writing library code to be consumed by others, `anyhow` helps a lot too).

oncallthrow 4 hours ago [-]
I don't agree. There isn't a standard convention for wrapping errors in Rust, like there is in Go with fmt.Errorf -- largely because ? is so widely-used (precisely because it is so easy to reach for).

The proof is in the pudding, though. In my experience, working across Go codebases in open source and in multiple closed-source organizations, errors are nearly universally wrapped and handled appropriately. The same is not true of Rust, where in my experience ? (and indeed even unwrap) reign supreme.

codys 51 minutes ago [-]
One would still use `?` in rust regardless of adding context, so it would be strange for someone with rust experience to mention it.

As for the example you gave:

    File::create("foo.txt")?;
If one added context, it would be

    File::create("foo.txt").context("failed to create file")?;
This is using eyre or anyhow (common choices for adding free-form context).

If rolling your own error type, then

    File::create("foo.txt").map_err(|e| format!("failed to create file: {e}"))?;
would match the Go code behavior. This would not be preferred though, as using eyre or anyhow or other error context libraries build convenient error context backtraces without needing to format things oneself. Here's what the example I gave above prints if the file is a directory:

    Error: 
       0: failed to create file
       1: Is a directory (os error 21)

    Location:
       src/main.rs:7
kstrauser 3 hours ago [-]
> There isn't a standard convention for wrapping errors in Rust

I have to say that's the first time I've heard someone say Rust doesn't have enough return types. Idiomatically, possible error conditions would be wrapped in a Result. `foo()?` is fantastic for the cases where you can't do anything about it, like you're trying to deserialize the user's passed-in config file and it's not valid JSON. What are you going to do there that's better than panicking? Or if you're starting up and can't connect to the configured database URL, there's probably not anything you can do beyond bombing out with a traceback... like `?` or `.unwrap()` does.

For everything else, there're the standard `if foo.is_ok()` or matching on `Ok(value)` idioms, when you want to catch the error and retry, or alert the user, or whatever.

But ? and .unwrap() are wonderful when you know that the thing could possibly fail, and it's out of your hands, so why wrap it in a bunch of boilerplate error handling code that doesn't tell the user much more than a traceback would?

filleduchaos 2 hours ago [-]
> there's probably not anything you can do beyond bombing out with a traceback... like `?` or `.unwrap()` does.

`?` (i.e. the try operator) and `.unwrap()` do not do the same thing.

bargainbin 4 hours ago [-]
My experience aligns with this, although I often find the error being used for non-errors which is somewhat of an overcorrection, i.e. db drivers returning “NoRows” errors when no rows is a perfectly acceptable result of a query.

It’s odd that the .unwrap() hack caused a huge outage at Cloudflare, and my first reaction was “that couldn’t happen in Go haha” but… it definitely could, because you can just ignore returned values.

But for some reason most people don’t. It’s like the syntax conveys its intent clearly: Handle your damn errors.

mh2266 4 hours ago [-]
I think the standard convention if you just want a stringly-typed error like Go is anyhow?

And maybe not quite as standard, but thiserror if you don’t want a stringly-typed error?

fragmede 4 hours ago [-]
yeah but which is faster and easier for a person to look at and understand. Go's intentionally verbose so that more complicated things are easier to understand.
loeg 4 hours ago [-]

  let mut file = File::create("foo.txt").context("failed to create file")?;
Of all the things I find hard to understand in Rust, this isn't one of them.
snuxoll 4 hours ago [-]
Important to note that .context() is something from `anyhow`, not part of the stdlib.
fragmede 2 hours ago [-]
What's the "?" doing? Why doesn't it compile without it? It's there to shortcut using match and handling errors and using unwrap, which makes sense if you know Rust, but the verbosity of go is its strength, not a weakness. My belief is that it makes things easier to reason about outside of the trivial example here.
loeg 1 hours ago [-]
The original complaint was only about adding context: https://news.ycombinator.com/item?id=46154373

If you reject the concept of a 'return on error-variant else unwrap' operator, that's fine, I guess. But I don't think most people get especially hung up on that.

filleduchaos 1 hours ago [-]
> What's the "?" doing? Why doesn't it compile without it?

I don't understand this line of thought at all. "You have to learn the language's syntax to understand it!"...and so what? All programming language syntax needs to be learned to be understood. I for one was certainly not born with C-style syntax rattling around in my brain.

To me, a lot of the discussion about learning/using Rust has always sounded like the consternation of some monolingual English speakers when trying to learn other languages, right down to the "what is this hideous sorcery mark that I have to use to express myself correctly" complaints about things like diacritics.

snuxoll 4 hours ago [-]
I don't really see it as any more or less verbose.

If I return Result<T, E> from a function in Rust I have to provide an exhaustive match of all the cases, unless I use `.unwrap()` to get the success value (or panic), or use the `?` operator to return the error value (possibly converting it with an implementation of `std::From`).

No more verbose than Go, from the consumer side. Though, a big difference is that match/if/etc are expressions and I can assign results from them, so it would look more like

    let a = match do_thing(&foo) {
      Ok(res) => res,
      Err(e) => return e
    }
instead of:

     a, err := do_thing(foo)
     if err != nil {
       return err // (or wrap it with fmt.Errorf and continue the madness
                  // of stringly-typed errors, unless you want to write custom
                  // Error types which now is more verbose and less safe than Rust).
    }
I use Go on a regular basis, error handling works, but quite frankly it's one of the weakest parts of the language. Would I say I appreciate the more explicit handling from both it and Rust? Sure, unchecked exceptions and constant stack unwinding to report recoverable errors wasn't a good idea. But you're not going to have me singing Go's praise when others have done it better.

Do not get me started on actually handling errors in Go, either. errors.As() is a terrible API to work around the lack of pattern matching in Go, and the extra local variables you need to declare to use it just add line noise.

phibz 4 hours ago [-]
I feel like this misses the biggest advantage of Result in rust. You must do something with it. Even if you want to ignore the error with unwrap() what you're really saying is "panic on errors".

But in go you can just _err and never touch it.

Also while not part of std::Result you can use things like anyhow or error_context to add context before returning if theres an error.

wging 2 hours ago [-]
> But in go you can just _err and never touch it.

You can do that in Rust too. This code doesn't warn:

    let _ = File::create("foo.txt");
(though if you want code that uses the File struct returned from the happy path of File::create, you can't do that without writing code that deals somehow with the possibility of the create() call failing, whether it is a panic, propagating the error upwards, or actual error handling code. Still, if you're just calling create() for side effects, ignoring the error is this easy.)
oncallthrow 4 hours ago [-]
Any sane Go team will be running errcheck, so I think this is a moot point.
afavour 4 hours ago [-]
I think it’s still worth pointing out that one language includes it as a feature and the other requires additional tooling.
dlisboa 4 hours ago [-]
Which can also be said about Rust and anyhow/thiserror. You won't see any decent project that don't use them, the language requires additional tooling for errors as well.
tptacek 4 hours ago [-]
I also prefer Rust's enums and match statements for error handling, but think that their general-case "ergonomic" error handling patterns --- the "?" thing in particular --- actually make things worse. I was glad when Go killed the trial balloon for a similar error handling shorthand. The good Rust error handling is actually wordier than Go's.
nu11ptr 3 hours ago [-]
Nah, you just need to use `map_err` or apply a '.context' which I think anyhow can do (and my crate, `uni_error` certainly can otherwise).
tptacek 2 hours ago [-]
I'm pretty familiar with the idiom here and I don't find error/result mapping fluent-style patterns all that easy to read or write. My experience is basically that you sort of come to understand "this goo at the end of the expression is just coercing the return value into whatever alternate goo the function signature dictates it needs", which is not at all the same thing as careful error handling.

Again: I think Rust as a language gets this right, better than Go does, but if I had to rank, it'd be (1) Rust explicit enum/match style, (2) Go's explicit noisy returns, (3) Rust terse error propagation style.

Basically, I think Rust idiom has been somewhat victimized by a culture of error golfing (and its attendant error handling crates).

nu11ptr 58 minutes ago [-]
> you sort of come to understand "this goo at the end of the expression is just coercing the return value into whatever alternate goo the function signature dictates it needs", which is not at all the same thing as careful error handling.

I think the problem is Rust does a great job at providing the basic mechanics of errors, but then stops a bit short.

First, I didn't realize until relatively recently that any `String` can be coerced easily into a `Box<dyn Error + Send + Sync>` (which should have a type alias in stdlib lol) using `?`, so if all you need is strings for your users, it is pretty simple to adorn or replace any error with a string before returning.

Second, Rust's incomplete error handling is why I made my crate, `uni_error`, so you can essentially take any Result/Error/Option and just add string context and be done with it. I believe `anyhow` can mostly do the same.

I do sorta like Go's error wrapping, but I think with either anyhow or my crate you are quickly back in a better situation as you gain compile time parameter checking in your error messages.

I agree Rust has over complicated error handling and I don't think `thiserror` and `anyhow` with their libraries vs applications distinction makes a lot of sense. I find my programs (typically API servers) need the the equivalent of `anyhow` + `thiserror` (hence why I wrote `uni_error` - still new and experimental, and evolving).

An example of error handling with `uni_error`:

    use uni_error::*;

    fn do_something() -> SimpleResult<Vec<u8>> {
        std::fs::read("/tmp/nonexist")
            .context("Oops... I wanted this to work!")
    }

    fn main() {
        println!("{}", do_something().unwrap_err());
    }
Ref: https://crates.io/crates/uni_error
tptacek 19 minutes ago [-]
Right, for error handling, I'd rather have Rust's bones to build on than Go's. I prefer Go to Rust --- I would use Go in preference to Rust basically any time I could get away with it (acknowledging that I could not get away with it if I was building a browser or an LKM). But this part of Rust's type system is meaningfully better than Go's.

Which is why it's weird to me that the error handling culture of Rust seems to steer so directly towards where Go tries to get to!

sethops1 4 hours ago [-]
I also like about Go that you can immediately see where the potential problem areas are in a page of code. Sure it's more verbose but I prefer the language that makes things obvious.
NooneAtAll3 4 hours ago [-]
it's the other way around

Rust used to not have operator?, and then A LOT of complaints have been "we don't care, just let us pass errors up quickly"

"good luck debugging" just as easily happens simply by "if err!=nil return nil,err" boilerplate that's everywhere in Golang - but now it's annoying and takes up viewspace

oncallthrow 4 hours ago [-]
> "if err!=nil return nil,err" boilerplate that's everywhere in Golang - but now it's annoying and takes up viewspace

This isn't true in my experience. Most Go codebases I've worked in wrap their errors.

If you don't believe me, go and take a look at some open-source Go projects.

frizlab 3 hours ago [-]
Swift is great for that:

    do {
       let file = try FileManager.create(…)
    } catch {
       logger.error("Failed creating file", metadata: ["error": "\(error)"])
    }
Note the try is not actual CPU exceptions, but mostly syntax sugar.

You can opt-out of the error handling, but it’s frowned upon, and explicit:

    let file = try? FileManager.create(…)
or

    let file = try! FileManager.create(…)
The former returning an optional file if there is an error, and the latter crashing in case of an error.
hnaccount19293 2 hours ago [-]
It's just as easy to add context to errors in Rust and plenty of Go programmers just return err without adding any context. Even when Go programmers add context it's usually stringly typed garbage. It's also far easier for Go programmers to ignore errors completely. I've used both extensively and error handling is much, much better in Rust.
nu11ptr 3 hours ago [-]
That isn't apples to apples.

In Rust I could have done (assuming `anyhow::Error` or `Box<dyn Error + Send + Sync>` return types, which are very typical):

    let mut file = File::create("foo.txt")
        .map_err(|e| format!("failed to create file: {e}")?;
Rust having the subtle benefit here of guaranteeing at compile type that the parameter to the string is not omitted.

In Go I could have done (and is just as typical to do):

    f, err := os.Create("filename.txt")
    if err != nil {
        return err
    }
So Go no more forces you to do that than Rust does, and both can do the same thing.
2 hours ago [-]
dystopiandevel 3 hours ago [-]
Also having explicit error handling is useful because it makes transparent the possibility of not getting the value (which is common in pure functional languages). With that said I have a Go project outside of work and it is very verbose. I decided to use it for performance as a new version of the project that mostly used bash scripts and was getting away too cryptic. The logic is easier to follow and more robust in the business domain but way more lines of code.
YmiYugy 4 hours ago [-]
What is the context that the Go code adds here? When File::create or os.Create fails the errors they return already contain the information what and why something failed. So what information does "failed to create file: " add?
edflsafoiewq 3 hours ago [-]
The error from Rust's File::create basically only contains the errno result. So it's eg. "permission denied" vs "failed to create file: permission denied".
the_gipsy 4 hours ago [-]
"Context" here is just a string. Debugging means grepping that string in the codebase, and praying that it's unique. You can only come up with so many unique messages along a stack.

You are also not forced to add context. Hell, you can easily leave errors unhandled, without compiler errors nor warnings, which even linters won't pick up, due to the asinine variable syntax rules.

Mawr 9 minutes ago [-]
I'm not impressed by the careless tossing around of the word "easily" in this thread.

It's quite ridiculous that you're claiming errors can be easily left unhandled while referring to what, a single unfortunate pattern of code that will only realistically happen due to copy-pasting and gets you code that looks obviously wrong? Sigh.

oncallthrow 4 hours ago [-]
> Debugging means grepping that string in the codebase, and praying that it's unique.

This really isn't an issue in practice. The only case where an error wouldn't uniquely identify its call stack is if you were to use the exact same context string within the same function (and also your callees did the same). I've never encountered such a case.

> You are also not forced to add context

Yes, but in my experience Go devs do. Probably because they're having to go to the effort of typing `if err != nil` anyway, and frankly Go code with bare:

    if err != nil {
        return err
    }
sticks out like a sore thumb to any experienced Go dev.

> which even linters won't pick up, due to asinine variable syntax rules.

I have never encountered a case where errcheck failed to detect an unhandled error, but I'd be curious to hear an example.

the_gipsy 4 hours ago [-]
The go stdlib notoriously returns errors without wrapping. I think it has been shifting towards more wrapping more often, but still.

    err1 := foo()
    err2 := bar()
    if err1 != nil || err2 != nil {
        return err1  // if only err2 failed, returns nil!
    }
``` func process() error { err := foo() if err != nil { return err }

    if something {
        result, err := bar()  // new err shadows outer err
        if err != nil {
            return err
        }
        use(result)
    }
    
    if somethingElse {
        err := baz()  // another shadow
        log.Println(err)
    }
    
    return err  // returns foo's err (nil), baz's error lost
} ```
tayo42 3 hours ago [-]
I feel like almost always `?` is a mistake in Rust and should just be used for quick test code like using unwrap.

Go's wrapping of errors is just a crappy exception stack trace with less information.

vegabook 4 hours ago [-]
The last paragraph captures the essence that all the PL theory arguments do not. "Zig has a fun, subversive feel to it". It gives you a better tool than C to apply your amazing human skills, freely, whereas both Rust and Go are fundamentally sceptical about you.
saghm 40 minutes ago [-]
When it comes to our ability to write bug-free code, I feel like humans are not actually not that good at writing bug-free code. We just don't have any better way of producing software than that, and software is useful. This doesn't mean we're particularly good at it though, just that it's hard to motivate people to spend effort up front to avoid bugs when the cost of them is easy to ignore in the short term when they're not obvious. I feel like the mindset that languages that try to make them more apparent up front (which I honestly would not include Go as one of) are somehow getting in the way of us is pretty much exactly the opposite of what's needed, especially in the systems programming space (which also does not really include Go in my mind).
xpe 33 minutes ago [-]
Self-aware people are mindful about what "future them" might do in various scenarios, and they plan ahead to tamp down their worse tendencies. I don't keep a raspberry cheesecake in my fridge, even though that would maximize a certain kind of freedom (the ability to eat cheesecake whenever I want). I much prefer the freedom that comes with not being tempted, as it leads to better outcomes on things I really care about.

In a sense, it is a powerful kind of freedom to choose a language that protects us from the statistically likely blunders. I prefer a higher-level kind of freedom -- one that provides peace of mind from various safety properties.

This comment is philosophical -- interpret and apply it as you see fit -- it is not intended be interpreted as saying my personal failure modes are the same as yours. (e.g. Maybe you don't mind null pointer exceptions in the grand scheme of things.)

Random anecdote: I still have a fond memory of a glorious realization in Haskell after a colleague told me "if you design your data types right, the program just falls into place".

aw1621107 5 minutes ago [-]
> Random anecdote: I still have a fond memory of a glorious realization in Haskell after a colleague told me "if you design your data types right, the program just falls into place".

There's a similar quote from The Mythical Man Month [0, page 102]:

> Show me your flowchart and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowcharts; they’ll be obvious.

And a somewhat related one from Linus [1]:

> I will, in fact, claim that the difference between a bad programmer and a good one is whether he considers his code or his data structures more important. Bad programmers worry about the code. Good programmers worry about data structures and their relationships.

[0]: https://www.cs.cmu.edu/afs/cs/academic/class/15712-s19/www/p...

[1]: https://lwn.net/Articles/193245/

Klonoar 2 hours ago [-]
I mean, if we're going to go there, you could take it a step further: Zig allows the programmer ego to run wild in a way that Rust and Go do not.

This is perhaps somewhat natural; people like and want to be good at things. Where you fall on the trade off is up to you.

vibe_assassin 3 hours ago [-]
I'd rather read 3 lines of clear code than one line of esoteric syntactic sugar. I think regardless of what blogs say, Go's adoption compared to that of Rust or Zig speaks for itself
gaanbal 5 hours ago [-]
OP tried zig last and is currently most fascinated by it
rishabhaiover 3 hours ago [-]
> it is like C in that you can fit the whole language in your head.

This is exactly why I find Go to be an excellent language. Most of the times, Go is the right tool.

Rust doesn't feel like a tool. Ceremonial yet safe and performant.

auxym 1 hours ago [-]
> it is like C in that you can fit the whole language in your head.

Sure, you can fit all of C in your head, including all the obscure footguns that can lead to UB: https://gist.github.com/Earnestly/7c903f481ff9d29a3dd1

And other fun things like aliasing rules and type punning.

ojosilva 5 hours ago [-]
Fine, but there's a noticeable asymmetry in how the three languages get treated. Go gets dinged for hiding memory details from you. Rust gets dinged for making mutable globals hard and for conceptual density (with a maximally intimidating Pin quote to drive it home). But when Zig has the equivalent warts they're reframed as virtues or glossed over.

Mutable globals are easy in Zig (presented as freedom, not as "you can now write data races.")

Runtime checks you disable in release builds are "highly pragmatic," with no mention of what happens when illegal behavior only manifests in production.

The standard library having "almost zero documentation" is mentioned but not weighted as a cost the way Go's boilerplate or Rust's learning curve are.

The RAII critique is interesting but also somewhat unfair because Rust has arena allocators too, and nothing forces fine-grained allocation. The difference is that Rust makes the safe path easy and the unsafe path explicit whereas Zig trusts you to know what you're doing. That's a legitimate design, hacking-a!

The article frames Rust's guardrails as bureaucratic overhead while framing Zig's lack of them as liberation, which is grading on a curve. If we're cataloging trade-offs honestly

> you control the universe and nobody can tell you what to do

...that cuts both ways...

ekropotin 4 hours ago [-]
I pretty new to Rust and I’m wondering why global mutables are hard?

At first glance you can just use static variable of a type supporting interior mutability - RefCell, Mutex, etc…

written-beyond 4 hours ago [-]
I don't think it's specifically hard, it's more related to how it probably needed more plumbing in the language that authors thought would add to much baggage and let the community solve it. Like the whole async runtime debates
umanwizard 4 hours ago [-]
> I pretty new to Rust and I’m wondering why global mutables are hard?

They're not.

  fn main() {
      unsafe {
          COUNTER += 1;
          println!("COUNTER = {}", COUNTER);
      }
  
      unsafe {
          COUNTER += 10;
          println!("COUNTER = {}", COUNTER);
      }
  }
Global mutable variables are as easy in Rust as in any other language. Unlike other languages, Rust also provides better things that you can use instead.
raggi 2 hours ago [-]
People always complain about unsafe, so I prefer to just show the safe version.

  use std::sync::Mutex;

  static LIST: Mutex<Vec<String>> = Mutex::new(Vec::new());

  fn main() -> Result<(), Box<dyn std::error::Error>> {

      LIST.lock()?.push("hello world".to_string());
      println!("{}", LIST.lock()?[0]);

      Ok(())
  }
pm90 3 hours ago [-]
I still don’t get the point of zig, at least not from this post? I really don’t want to do memory management manually. I actually think rust is pretty well designed, but allows you to write very complex code. go tries really hard to keep it simple but at the cost of resisting modern features.
int_19h 3 hours ago [-]
If you don't want to do memory management manually, then you're not the intended target audience for Zig. It's a language where any piece of code that needs to do heap allocation has to receive an allocator as an explicit argument in order to be able to allocate anything at all.
tialaramex 2 hours ago [-]
> In both Go and Rust, allocating an object on the heap is as easy as returning a pointer to a struct from a function.

I can't figure out what the author is envisioning here for Rust.

Maybe, they actually think if they make a pointer to some local variable and then return the pointer, that's somehow allocating heap? It isn't, that local variable was on the stack and so when you return it's gone, invalidating your pointer - but Rust is OK with the existence of invalid pointers, after all safe Rust can't dereference any pointers, and unsafe Rust declares the programmer has taken care to ensure any pointers being dereferenced are valid (which this pointer to a long dead variable is not)

[If you run a new enough Rust I believe Clippy now warns that this is a bad idea, because it's not illegal to do this, but it's almost certainly not what you actually meant]

Or maybe in their mind, Box<Goose> is "a pointer to a struct" and so somehow a function call Box::new(some_goose) is "implicit" allocation, whereas the function they called in Zig to allocate memory for a Goose was explicit ?

saghm 34 minutes ago [-]
Yeah, this is very confusing to me. I don't see how someone can conflate Go implicitly deciding whether to promote a pointer to the heap based on escape analysis without any way for the programmer to tell other than having to replicate the logic that's happening at runtime with needing to explicitly use one on the APIs that literally exist for the sole purpose of allocating on the heap without either fundamentally misunderstanding something or intentionally being misleading.
dmix 44 minutes ago [-]
> If the number of questions online about “Go vs. Rust” or “Rust vs. Zig” is a reliable metric

The human brain demands "vs" articles

teleforce 2 hours ago [-]
>I’m not the first person to pick on this particular Github comment, but it perfectly illustrates the conceptual density of Rust:

https://github.com/rust-lang/rust/issues/68015#issuecomment-...

Wow, Rust does take programming complexity to another level.

Everything, including programming languages, need to be simple but no simpler. I'm of the opinion that most the computing and memory resources complexity should be handled and abstracted by the OS for example the address space isolation [1].

The author should try D language where it's the Goldilocks of complexity and meta programming compared to Go, Rust and Zig [2].

[1] Linux address space isolation revived after lowering performance hit (59 comments):

https://news.ycombinator.com/item?id=44899488

[2] Ask HN: Why do you use Rust, when D is available? (255 comments):

https://news.ycombinator.com/item?id=23494490 [2]

kennykartman 4 hours ago [-]
I find this a nice read, but I don't think it captures the essence of these PL. To me it seems mostly a well crafted post to reach a point that basically says what people think of these languages: "go is minimal, rust is complex, zig is a cool, hot compromise". The usual.

It was fun to read, but I don't see anything new here, and I don't agree too much.

shevy-java 3 hours ago [-]
Until someone creates a new language that is better than these ...
qaq 2 hours ago [-]
with LLMs started defaulting to go for most projects

Rust for WASM

Zig is what I'd use if I started a greenfield DBMS project

throwaway894345 4 hours ago [-]
> [Go] is like C in that you can fit the whole language in your head.

Go isn't like C in that you can actually fit the entire language in your head. Most of us who think we have fit C in our head will still stumble on endless cases where we didn't realize X was actually UB or whatever. I wonder how much C's reputation for simplicity is an artifact of its long proximity to C++?

kanbankaren 4 hours ago [-]
30 years in C/C++ here.

Give an example of UB code that you have committed in real life, not from blogs. I am genuinely curious.

jech 2 hours ago [-]
> Give an example of UB code that you have committed in real life

    struct foo {
        ...
        atomic_int v;
        ...
    };
    
    struct foo x;
    memset(&x, 0, sizeof(x));
throwaway2037 2 hours ago [-]

    > OOP has been out of favor for a while now
I love these lines. Who writes this stuff? I'll tell you: The same people on HN who write "In Europe, X is true." (... when Europe is 50 countries!).

    > Zig is a language for data-oriented design.
But not OOP, right? Or, OOP couldn't do the same thing?

One thing that I have found over umpteen years of reading posts online: Americans just love superlatives. They love the grand, sweeping gesture. Read their newspapers; you see it every day. A smidge more minimalism would make their writing so much more convincing.

I will take some downvotes for this ad hominem attack: Why does this guy have 387 connections on LinkedIn? That is clicking the "accept" button 387 times. Think about that.

dismalaf 3 hours ago [-]
One of these is not like the others...

Odin vs Rust vs Zig would be more apt, or Go vs Java vs OCaml or something...

skywhopper 5 hours ago [-]
Wow, this is a really good writeup without all the usual hangups that folks have about these languages. Well done!
drnick1 3 hours ago [-]
Modern C++ is probably better than all of those if you need to interface with existing code and libraries, or need classic OOP.
reeeli 5 hours ago [-]
if the languages were creations of LLMs, what would be your (relatively refined) chain(s) of (indulgently) critical thought?
raggi 5 hours ago [-]
I really hate the anti-RAII sentiments and arguments. I remember the Zig community lead going off about RAII before and making claims like "linux would never do this" (https://github.com/torvalds/linux/blob/master/include/linux/...).

There are bad cases of RAII APIs for sure, but it's not all bad. Andrew posted himself a while back about feeling bad for go devs who never get to debug by seeing 0xaa memory segments, and sure I get it, but you can't make over-extended claims about non-initialization when you're implicitly initializing with the magic value, that's a bit of a false equivalence - and sure, maybe you don't always want a zero scrub instead, I'm not sold on Go's mantra of making zero values always be useful, I've seen really bad code come as a result of people doing backflips to try to make that true - a constructor API is a better pattern as soon as there's a challenge, the "rule" only fits when it's easy, don't force it.

Back to RAII though, or what people think of when they hear RAII. Scope based or automatic cleanup is good. I hate working with Go's mutex's in complex programs after spending life in the better world. People make mistakes and people get clever and the outcome is almost always bad in the long run - bugs that "should never get written/shipped" do come up, and it's awful. I think Zig's errdefer is a cool extension on the defer pattern, but defer patterns are strictly worse than scope based automation for key tasks. I do buy an argument that sometimes you want to deviate from scope based controls, and primitives offering both is reasonable, but the default case for a ton of code should be optimized for avoiding human effort and human error.

In the end I feel similarly about allocation. I appreciate Zig trying to push for a different world, and that's an extremely valuable experiment to be doing. I've fought allocation in Go programs (and Java, etc), and had fights with C++ that was "accidentally" churning too much (classic hashmap string spam, hi ninja, hi GN), but I don't feel like the right trade-off anywhere is "always do all the legwork" vs. "never do all the legwork". I wish Rust was closer to the optimal path, and it's decently ergonomic a lot of the time, but when you really want control I sometimes want something more like Zig. When I spend too much time in Zig I get a bit bored of the ceremony too.

I feel like the next innovation we need is some sanity around the real useful value that is global and thread state. Far too much toxic hot air is spilled over these, and there are bad outcomes from mis/overuse, but innovation could spend far more time on _sanely implicit context_ that reduces programmer effort without being excessively hidden, and allowing for local specialization that is easy and obvious. I imagine it looks somewhere between the rust and zig solutions, but I don't know exactly where it should land. It's a horrible set of layer violations that the purists don't like, because we base a lot of ABI decisions on history, but I'd still like to see more work here.

So RAII isn't the big evil monster, and we need to stop talking about RAII, globals, etc, in these ways. We need to evaluate what's good, what's bad, and try out new arrangements maximize good and minimize bad.

sestep 3 hours ago [-]
Heh, sounds like you'd love the work-in-progress I'm about to present at MWPLS 2025 :)
iainmerrick 4 hours ago [-]
Have you tried Swift? It has the sort of pragmatic-but-safe-by-default approach you’re talking about.
raggi 2 hours ago [-]
Not enough to say yes in earnest. I help maintain some swift at work, but I put my face in the code base quite rarely. I've not authored anything significant in the language myself. What I have seen is some code where there are multiple different event/mutex/thread models all jumbled up, and I was simultaneously glad to see that was possible in a potentially clean way alongside at least the macos/ios runtime, but the code in question was also a confused mess around it and had a number of fairly serious and real concurrency issues with UB and data races that had gone uncaught and seemingly therefore not pointed out by the compiler or tools. I'd be curious to see a SOTA project with reasonable complexity.
frizlab 3 hours ago [-]
And it is a joy to use, truly.
bsder 3 hours ago [-]
> So RAII isn't the big evil monster, and we need to stop talking about RAII, globals, etc, in these ways.

I disagree and place RAII as the dividing line on programming language complexity and is THE "Big Evil Monster(tm)".

Once your compiled language gains RAII, a cascading and interlocking set of language features now need to accrete around it to make it ... not excruciatingly painful. This practically defines the difference between a "large" language (Rust or C++) and a "small" language (C, Zig, C3, etc.).

For me, the next programming language innovation is getting the garbage collected/managed memory languages to finally quit ceding so much of the performance programming language space to the compiled languages. A managed runtime doesn't have to be so stupidly slow. It doesn't have to be so stupidly non-deterministic. It doesn't have to have a pathetic FFI that is super complex. I see the "strong typing everywhere" as the first step along this path. Fil-C might become an interesting existence proof in this space.

I view having to pull out any of C, Zig, C++, Rust, etc. as a higher-level programming language failure. There will always be a need for something like them at the bottom, but I really want their scope to be super small. I don't want to operate at their level if I can avoid them. And I say all this as someone who has slung more than 100KLoC of Zig code lately.

For a concrete example, let's look at Ghostty which was written in Zig. There is no strong performance reason to be in Zig (except that implementations in every other programming language other than Rust seem to be so much slower). There is no strong memory reason to be in Zig (except that implementations in every other programming language other than Rust chewed up vast amounts of it). And, yet, a relatively new, unstable, low-level programming language was chosen to greenfield Ghostty. And all the other useful terminal emulators seem to be using Rust.

Every adherent of managed memory languages should take it as a personal insult that people are choosing to write modern terminal emulators in Rust and Zig.

zozbot234 3 hours ago [-]
> Every adherent of managed memory languages should take it as a personal insult that people are choosing to write modern terminal emulators in Rust and Zig.

How so? Garbage collection has inherent performance overhead wrt. manual memory management, and Rust now addresses this by providing the desired guarantees of managed memory without the overhead of GC.

A modern terminal emulator is not going to involve complex reference graphs where objects may cyclically reference one another with no clearly-defined "owner"; which is the one key scenario where GC is an actual necessity even in a low-level systems language. What do they even need GC for? Rather, they should tweak the high-level design of their program to emsure that object lifetimes are properly accounted for without that costly runtime support.

raggi 1 hours ago [-]
> How so? Garbage collection has inherent performance overhead wrt. manual memory management, and Rust now addresses this by providing the desired guarantees of managed memory without the overhead of GC.

I somewhat disagree, specifically on the implicit claim that all GC has overhead and alternatives do not. Rust does a decent job of giving you some ergonomics to get started, but it is still quite unergonomic to fix once you have multiple different allocation problems to deal with. Zig flips that a bit on it's head, it's more painful to get started, but the pain level stays more consistent throughout deeper problems. Ideally though I want a better blend of both - to give a still not super concrete version of what I mean, I mean I want something that can be setup by the systems oriented developer say, near the top of a request path, and it becomes a more implicit dependency for most downstream code with low ceremony and allowing for progressive understanding of contributors way down the call chain who in most cases don't need to care - meanwhile enabling an easy escape hatch when it matters.

I think people make far too much of a distinction between a GC and an allocator, but the reality is that all allocators in common use in high level OS environments are a form of GC. That's of course not what they're talking about, but it's also a critical distinction.

The main difference between what people _call a GC_ and those allocators is that a typical "GC" pauses the program "badly" at malloc time, and a typical allocator pauses a program "badly" at free time (more often than not). It's a bit of a common oddity really, both "GC's" and "allocators" could do things "the other way around" as a common code path. Both models otherwise pool memory and in higher performance tunings have to over-allocate. There are lots of commonly used "faster" allocators in use today that also bypass their own duties at smarter allocation by simply using mmap pools, but those scale poorly: mmap stalls can be pretty unpredictable and have cross-thread side effects that are often undesirable too.

The second difference which I think is more commonly internalized is that typically "the GC" is wired into the runtime in various ways, such as into the scheduler (Go, most dynlangs, etc), and has significant implications at the FFI boundary.

It would be possible to be more explicit about a general purpose allocator that has more GC-like semantics, but also provides the system level malloc/free style API as well as a language assisted more automated API with clever semantics or additional integrations. I guess fil-C has one such system (I've not studied their implementation). I'm not aware of implicit constraints which dictate that there are only two kinds of APIs, fully implicit and intertwined logarithmic GCs, or general purpose allocators which do most of their smart work in free.

My point is I don't really like the GC vs. not-GC arguments very much - I think it's one of the many over-generalizations we have as an industry that people rally hard around and it has been implicitly limiting how far we try to reach for new designs at this boundary. I do stand by a lot of reasoning for systems work that the fully implicitly integrated GC's (Java, Go, various dynlangs) generally are far too opaque for scalable (either very big or very small) systems work and they're unpleasant to deal with once you're forced to. At the same time for that same scalable work you still don't get to ignore the GC you are actually using in the allocator you're using. You don't get to ignore issues like restarting your program that has a 200+GB heap has huge page allocation costs, no matter what middleware set that up. Similarly you don't want a logarithmic allocation strategy on most embedded or otherwise resource constrained systems, those designs are only ok for servers, they're bad for batteries and other parts of total system financial cost in many deployments.

I'd like to see more work explicitly blending these lines, logarithmically allocating GC's scale poorly in many similar ways to more naive mmap based allocators. There are practical issues you run into with overallocation and the solution is to do something more complex than the classical literature. I'd like to see more of this work implemented as standalone modules rather than almost always being implicitly baked into the language/runtime. It's an area that we implicitly couple stuff too much, and again good on Zig for pushing the boundary on a few of these in the standard language and library model it has (and seemingly now also taking the same approach for IO scheduling - that's great).

raggi 2 hours ago [-]
Can you give some examples of " ... not excruciatingly painful" and why you think they're inherent to RAII?
simonask 3 hours ago [-]
Go ahead, invent a GC that doesn’t require at least 2-4x the program’s working set of memory, and that doesn’t drizzle the code with little branches and memory barriers.

You will be very rich.

echelon 5 hours ago [-]
> Many people seem confused about why Zig should exist if Rust does already. It’s not just that Zig is trying to be simpler. I think this difference is the more important one. Zig wants you to excise even more object-oriented thinking from your code.

I feel like Zig is for the C / C++ developers that really dislike Rust.

There have been other efforts like Carbon, but this is the first that really modernizes the language and scratches new itches.

> I’m not the first person to pick on this particular Github comment, but it perfectly illustrates the conceptual density of Rust: [crazy example elided]

That is totally unfair. 99% of your time with Rust won't be anything like that.

> This makes Rust hard, because you can’t just do the thing! You have to find out Rust’s name for the thing—find the trait or whatever you need—then implement it as Rust expects you to.

What?

Rust is not hard. Rust has a standard library that looks an awful lot like Python or Ruby, with similarly named methods.

If you're trying to shoehorn some novel type of yours into a particular trait interface so you can pass trait objects around, sure. Maybe you are going to have to memorize a lot more. But I'd ask why you write code like that unless you're writing a library.

This desire of wanting to write OO-style code makes me think that people who want OO-style code are the ones having a lot of struggle or frustration with Rust's ergonomics.

Rust gives you everything OO you'd want, but it's definitely more favorable if you're using it in a functional manner.

> makes consuming libraries easy in Rust and explains why Rust projects have almost as many dependencies as projects in the JavaScript ecosystem.

This is one of Rust's superpowers !

Quothling 4 hours ago [-]
> Rust is not hard. Rust has a standard library that looks an awful lot like Python or Ruby, with similarly named methods.

I would read this in regard to Go and not so much in regards to Zig. Go is insanely productive, and while you're not going to match something like Django in terms of delivery speed with anything in Go, you almost can... and you can do it without using a single external dependency. Go loses a little of this in the embeded space, where it's not quite as simple, but the opinonated approach is still very productive even here.

I can't think of any language where I can produce something as quickly as I can in Go with the use of nothing but the standard library. Even when you do reach for a framework like SQLC, you can run the external parts in total isolation if that's your thing.

I will say that working with the interoperability of Zig in our C for Python binaries has been very easy, which it wasn't for Rust. This doesn't mean it's actually easier for other people, but it sure was for me.

> This is one of Rust's superpowers !

In some industries it's really not.

unshavedyak 5 hours ago [-]
Rust is hard in that it gives you a ton of rope to hang yourself with, and some people are just hell bent on hanging themselves.

I find Rust quite easy most of the time. I enjoy the hell out of it and generally write Rust not too different than i'd have written my Go programs (i use less channels in Rust though). But i do think my comment about rope is true. Some people just can't seem to help themselves.

nicoburns 5 hours ago [-]
That seems like an odd characterization of Rust. The borrow checker and all the other type safety features, as well as features like send/sync are all about not giving you rope to hang yourself with.
unshavedyak 4 hours ago [-]
The rope in my example is complexity. Ie choosing to use "all teh features" when you don't need or perhaps even want to. Eg sometimes a simple clone is fine. Sometimes you don't need to opt for every generic and performance minded feature Rust offers - which are numerous.

Though, i think my statement is missing something. I moved from Go to Rust because i found that Rust gave me better tooling to encapsulate and reuse logic. Eg Iterators are more complex under the hood, but my observed complexity was lower in Rust compared to Go by way of better, more generalized code reuse. So in this example i actually found Go to be more complex.

So maybe a more elaborated phrase would be something like Rust gives you more visible rope to hang yourself with.. but that doesn't sound as nice. I still like my original phrase heh.

1313ed01 4 hours ago [-]
I would love to see a language that is to C what Rust is to C++. Something a more average human brain like mine can understand. Keep the no-gc memory safety things, but simplify everything else a thousand times.

Not saying that should replace Rust. Both could exist side by side like C and C++.

wtetzner 2 hours ago [-]
I'm curious about what you'd want simplified. Remove traits? What other things are there to even simplify if you're going to keep the borrow checker?
4 hours ago [-]
mh2266 3 hours ago [-]
I feel like it is the opposite, Go gives you a ton of rope to hang yourself with and hopefully you will notice that you did: error handing is essentially optional, there are no sum types and no exhaustiveness checks, the stdlib does things like assume filepaths are valid strings, if you forget to assign something it just becomes zero regardless of whether it’s semantically reasonable for your program to do that, no nullability checking enforcement for pointers, etc.

Rust OTOH is obsessively precise about enforcing these sort of things.

Of course Rust has a lot of features and compiles slower.

kace91 5 hours ago [-]
One question about your functional point: where can I learn functional programming in terms of organization of large codebases?

Perhaps it is because DDD books and the like usually have strong object oriented biases, but whenever I read about functional programming patterns I’m never clear on how to go from exercise stuff to something that can work in a real world monolith for example.

And to be clear I’m not saying functional programming is worse at that, simply that I have not been able to find information on the subject as easily.

myaccountonhn 4 hours ago [-]
There are a lot of lectures/speeches by the creator of elm and Richard Feldman that talk about how to think "functionally"

Here is one about how to structure a project (roughly)

https://youtube.com/watch?v=XpDsk374LDE

I also think looking at the source code for elm and its website, as well as the elm real world example help a lot.

Yoric 4 hours ago [-]
> I feel like Zig is for the C / C++ developers that really dislike Rust.

Also my feeling. Writing this as a former C++ developer who really likes Rust :)

the__alchemist 5 hours ago [-]
Same. Zig's niche is in the vein of languages that encourages using pointers for business logic. If you like this style, Rust and most other new languages aren't an option.
tiltowait 4 hours ago [-]
> Rust has a standard library that looks an awful lot like Python or Ruby, with similarly named methods.

Can you elaborate? While they obviously have overlap, Rust's stdlib is deliberately minimal (you don't even get RNG without hitting crates.io), whereas Python's is gigantic. And in actual use, they tend to feel extremely different.

awesome_dude 5 hours ago [-]
> Rust is not hard. Rust has a standard library that looks an awful lot like Python or Ruby, with similarly named methods.

> If you're trying to shoehorn some novel type of yours into a particular trait interface so you can pass trait objects around, sure. Maybe you are going to have to memorize a lot more. But I'd ask why you write code like that unless you're writing a library.

I think that you are missing the point - they're not saying (at least in my head) "Rust is hard because of all the abstractions" but, more "Rust is hard because you are having to explain to the COMPILER [more explicitly] what you mean (via all these abstractions)

And I think that that's a valid assessment (hell, most Rustaceans will point to this as a feature, not a bug)

999900000999 4 hours ago [-]
Rust is hard because it's just difficult to read.

If you know Java, you can read C#, JavaScript, Dart, and Haxe and know what's going on. You can probably figure out Go.

Rust is like learning how to program again.

Back when I was young and tried C++, I was like this is hard and I can't do this.

Then I found JavaScript and everything was great.

What I really want is JS that complies into small binaries and runs faster than C. Maybe clean up the npm dependency tree. Have a professional commite vet every package.

I don't think that's possible, but I can dream

badmonster 1 hours ago [-]
[dead]
neonsunset 2 hours ago [-]
[dead]
black_13 3 hours ago [-]
[dead]
0x457 5 hours ago [-]
Reads like a very surface level take with a minor crush on Rob Pike.
Aperocky 4 hours ago [-]
Anecdotally, as a result of the traits that made it hard to learn for humans, Rust is actually a great language for LLM.

Out of all languages I do development in the past few months: Go, Rust, Python, Typescript; Rust is the one that LLM has the least churn/problems in terms of producing correct and functional code given a problem of similar complexity.

I think this outside factor will eventually win more usage for Rust.

damslunk 4 hours ago [-]
Yeah that's an interesting point, it feels like it should be even better than it is now (I might be ignorant of the quality of the best coding agents atm).

Like rust seems particularly well suited for an agent based workflow, in that in theory an agent with a task could keep `cargo check`-ing it's solutions, maybe pulling from docs.rs or source for imported modules, and get to a solution that works with some confidence (assuming the requirements were well defined/possible etc etc).

I've had a mixed bag of an experience trying this with various rust one off projects. It's definitely gotten me some prototype things working, but the evolving development of rust and crates in the ecosystem means there's always some patchwork to get things to actually compile. Anecdotally I've found that once I learned more about the problem/library/project I'll end up scrapping or rewriting a lot of the LLM code. It seems pretty hard to tailor/sandbox the context and workflow of an agent to the extent that's needed.

I think the Bun acquisition by Anthropic could shift things too. Wouldn't be surprised if the majority of code generated/requested by users of LLM's is JS/TS, and Anthropic potentially being able to push for agentic integration with the Bun runtime itself could be a huge boon for Bun, and maybe Zig (which Bun is written in) as a result? Like it'd be one thing for an agent to run cargo check, it'd be another for the agent to monitor garbage collection/memory use while code is running to diagnose potential problems/improvements devs might not even notice until later. I feel like I know a lot of devs who would never touch any of the langs in this article (thinking about memory? too scary!) and would love to continue writing JS code until they die lol

ux266478 4 hours ago [-]
Generally a good writeup, but the article seems a bit confused about undefined behavior.

> What is the dreaded UB? I think the best way to understand it is to remember that, for any running program, there are FATES WORSE THAN DEATH. If something goes wrong in your program, immediate termination is great actually!

This has nothing to do with UB. UB is what it says on the tin, it's something for which no definition is given in the execution semantics of the language, whether intentionally or unintentionally. It's basically saying, "if this happens, who knows". Here's an example in C:

    int x = 555;
    long long *l = (long long*)&x;
    x = 123;
    printf("%d\n", *l);
This is a violation of the strict aliasing rule, which is undefined behavior. Unless it's compiled with no optimizations, or -fno-strict-aliasing which effectively disables this rule, the compiler is "free to do whatever it wants". Effectively though, it'll just print out 555 instead of 123. All undefined behavior is just stuff like this. The compiler output deviates from the expected input, and only maybe. You can imagine this kind of thing gets rather tricky with more aggressive optimizations, but this potential deviation is all that occurs.

Race conditions, silent bugs, etc. can occur as the result of the compiler mangling your code thanks to UB, but so can crashes and a myriad of other things. It's also possible UB is completely harmless, or even beneficial. It's really hard to reason about that kind of thing though. Optimizing compilers can be really hard to predict across a huge codebase, especially if you aren't a compiler dev yourself. That unpredictability is why we say it's bad. If you're compiling code with something like TCC instead of clang, it's a completely different story.

That's it. That's all there is to UB.

publicdebates 4 hours ago [-]
I think it's common to be taught that UB is very bad when you're new, partly to simplify your debugging experience, partly to help you understand and mentally demarcate the boundaries of what the language allows and doesn't allow, and partly because there are many Standards-Purists who genuinely avoid UB. But from my own experience, UB just means "consult your compiler to see what it does here because this question is beyond our pay grade."

Interestingly enough, and only semi related, I had to use volatile for the first time ever in my latest project. Mainly because I was writing assembly that accessed memory directly, and I wanted to make sure the compiler didn't optimize away the variable. I think that's maybe the last C keyword on my bucket list.

mirashii 4 hours ago [-]
> But from my own experience, UB just means "consult your compiler to see what it does here because this question is beyond our pay grade."

People are taught it’s very bad because otherwise they do exactly this, which is the problem. What does your compiler do here may change from invocation to invocation, due to seemingly unrelated flags, small perturbations in unrelated code, or many other things. This approach encourages accepting UB in your program. Code that invokes UB is incorrect, full stop.

ux266478 2 hours ago [-]
> Code that invokes UB is incorrect, full stop.

That's not true at all, who taught you that? Think of it like this, signed integer over/underflow is UB. All addition operations over ints are potentially invoking UB.

   int add (int a, int b) { return a + b; }
So this is incorrect code by that metric, that's clearly absurd.

Compilers explicitly provide you the means to disable optimizations in a granular way over undefined behavior precisely because a lot of useful behavior is undefined, but compilation units are sometimes too complex to reason about how the compiler will mangle it. -fno-strict-aliasing doesn't suddenly make pointer aliasing defined behavior.

We have compiler behavior for incorrect code, and it's refusing to compile the code in the first place. Do you think it just a quirky oversight that UB triggers a warning at most? The entire point of compilers having free reign over UB was so they could implement platform-specific optimizations in its place. UB isn't arbitrary.

publicdebates 4 hours ago [-]
I understand, but you have to see how you would be considered one of the Standards-Purists that I was talking about, right? If Microsoft makes a guarantee in their documentation about some behavior of UB C code, and this guarantee is dated to about 14 years ago, and I see many credible people on the internet confirming that this behavior does happen and still happens, and these comments are scattered throughout those past 14 years, I think it's safe to say I can rely on that behavior, as long as I'm okay with a little vendor lock-in.
mirashii 3 hours ago [-]
> If Microsoft makes a guarantee in their documentation about some behavior of UB C code

But do they? Where?

More likely, you mean that a particular compiler may say "while the standard says this is UB, it is not UB in this compiler". That's something wholly different, because you're no longer invoking UB.

ux266478 2 hours ago [-]
Yes, that is still undefined behavior. Behavior being defined or not is a standards-level distinction, not a compiler one.
kibwen 4 hours ago [-]
> But from my own experience, UB just means "consult your compiler to see what it does here because this question is beyond our pay grade."

Careful. It's not just "consult your compiler", because the behavior of a given compiler on code containing UB is also allowed to vary based on specific compiler version, and OS, and hardware, and the phase of the moon.

iainmerrick 4 hours ago [-]
Race conditions, silent bugs, etc. can occur as the result of the compiler mangling your code thanks to UB, but so can crashes and a myriad of other things. [...] That's it. That's all there is to UB.

You don’t think that’s pretty bad?

ux266478 2 hours ago [-]
They can also occur from defined behavior. The point being that they're completely besides one another.