Live data from Hacker News

Thoughts on Go vs. Rust vs. Zig

sinclairtarget.com

211–220 of 599 posts

Re: Thoughts on Go vs. Rust vs. Zig

#211

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.

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.

Re: Thoughts on Go vs. Rust vs. Zig

#212
post #51

Earlier quoted context omitted.

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.

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).

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).

Re: Thoughts on Go vs. Rust vs. Zig

#213
post #210
post #114

Earlier quoted context omitted.

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 who…

> 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)?

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.

Re: Thoughts on Go vs. Rust vs. Zig

#214
post #29

> 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…

> [...] 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.

Re: Thoughts on Go vs. Rust vs. Zig

#215
post #128

Earlier quoted context omitted.

> 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…

> 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 r…

> 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).

Re: Thoughts on Go vs. Rust vs. Zig

#216
post #69

Earlier quoted context omitted.

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.

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.

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.

Re: Thoughts on Go vs. Rust vs. Zig

#217

Earlier quoted context omitted.

It entirely prevents race conditions due to the borrow checker and safe constructs like Mutexes. Logical race conditions and deadlocks can still happen.

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.

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".

Re: Thoughts on Go vs. Rust vs. Zig

#218
post #183
post #170

Earlier quoted context omitted.

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?

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 de…

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

Re: Thoughts on Go vs. Rust vs. Zig

#219
post #170
post #153

Earlier quoted context omitted.

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 th…

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?

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

Re: Thoughts on Go vs. Rust vs. Zig

#220
post #126

Earlier quoted context omitted.

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

In Python, it’s common to use exceptions for control flow. Even exiting a loop is done via an exception: `StopIteration`.

isn't break more normal
Post reply on HN