Comment by hansvm

1 year ago

It's not a perfect analogy, but if you want to put yourself in the shoes of the people making it:

1. Rust is an immensely complicated language, and it's not very composable (see the async debacle and whatnot). On the simple<->complex slider, it's smack dab on the right of the scale.

2. Ignoring any nitpicking [0], Zig is memory-safe enough in practice, placing it much closer to Rust than to C/C++ on the memory safety axis. My teammates have been using Zig for nearly a year, and the only memory safety bug was (a) caught before prod and (b) not something Rust's features would have prevented [1]. The `defer` and `errdefer` statements are excellent, and much like how you closely audit the use of `unsafe` in Rust there is only a small subset of Zig where you actually need to pull your magnifying glass out to figure out if the code has any major issues. In terms of memory issues I've cared about (not all conforming to Rust's narrow definition of memory safety), I've personally seen many more problems in Rust projects I contribute toward (only the one in Zig, plus a misunderstanding of async as I was learning the language a few years ago, many of varying severity in Rust, at this point probably more code written in Zig than Rust, 10yoe before starting with either).

With that in mind, you have C/C++ on the unsafe axis and Zig/Rust on the safe axis. The complexity axis is self-explanatory, fleshing out the analogy.

Is Zig memory-safe? No, absolutely not. Does that mean that Rust will win out for some domains? Absolutely. In practical terms though, your average senior developer will have many memory safety bugs in C/C++ and few in Zig/Rust. It's a reasonable way to compare and contrast languages.

Is it a perfect description? No, the map is not the territory. It's an analogy that helps a lot of people understand the world around them though.

[0] Even Python is simpler than Rust, and it's memory-safe. If we're limiting ourselves to systems languages, you still have a number of options like Ada and Coq. Rust is popular because it offers a certain tradeoff in the safety/performance/devex Pareto curve, and because it's had a lot of marketing. It's unique in that niche, by definition, but it's far from the only language to offer the features you explicitly stated.

[1] It was just an object pool, and the (aggregate) resetting logic wasn't solid. The objects would have passed through the borrow checker with flying colors though.

Edit: To your GC point, many parts of Rust look closer to GC than not under the hood. You don't have a GC pause, but you have object pools (sometimes falling back to kernel object pools) and a variety of allocation data structures. If RC is a GC tactic, the extra pointer increment/decrement is negligible compared to what Rust actually does to handle its objects (RC is everything Rust does, plus a counter). That's one of my primary performance complaints with the language, that interacting with a churn of small objects is both expensive and the easiest way to code. I can't trust code I see in the wild to behave reasonably by default.

> see the async debacle and whatnot

The async featureset in Rust is far from complete, but async is also somewhat of a niche. You're not necessarily expected to use it, often you can just use threads.

> Zig is memory-safe enough in practice

Temporal safety is a huge deal, especially for the "programming in the large" case. Where unstated assumptions that are relied upon for the safety of some piece of code can be broken as some other part of the code evolves. The Rust borrow checker is great for surfacing these issues, and has very few practical alternatives.

> If we're limiting ourselves to systems languages, you still have a number of options like Ada and Coq.

It's easy to be "safe" if the equivalent to free() is marked as part of the unsafe subset, as with Ada. Coq is not very relevant on its own, though I suppose it could be part of a solution for proving the memory safety of C code. But this is known to be quite hard unless you do take your care to write the program in the "idiomatically safe" style that a language like Rust points you to.

  • > The async featureset in Rust is far from complete, but async is also somewhat of a niche. You're not necessarily expected to use it, often you can just use threads.

    I’m sorry but it’s not true. You lose access to most of the ecosystem.

I've been seeing C++ fans talk about how they never see memory safety issues in practice in C++ for a decade and a half. I even believed it sometimes. But if there's ever been a common story over those 15 years, it's that these anecdotes mean little, and the actual memory safety property means a lot.

The only time I've seen "almost memory safe" actually work is in Go and Swift, which have memory safety problems with concurrency, but they're actually rare enough not to matter too much (though in Go's case it would have been easy to design interfaces and slices not to have the problem, and I wish they had). I simply don't believe that Zig is meaningfully more memory safe than C++.

  • I don't see any Zig repositories under https://github.com/pcwalton

    Is your opinion based on anything other than pure speculation?

    I think it makes more sense to form an opinion after actually having tried Rust, C++, and Zig in earnest.

    There are lots of us out there who've done it. Join us!

  • I think it's possible to retrofit race-safe slices and interfaces into Go, and I expect it to happen one day once some actually relevant code execution exploit shows up.

    There's going to be some impact and low-level libraries that manipulate directly the words that constitute slices and interfaces, and there will some slight performance impact and increase in memory usage, but hopefully nothing drastic.

    • I think slices and interfaces in Go are both 2×usize width, in which case you just need double-width CAS to make them safe from data races - which most modern architectures support.

      1 reply →

  • >"I've been seeing C++ fans talk about how they never see memory safety issues in practice in C++ for a decade and a half. I even believed it sometimes. But if there's ever been a common story over those 15 years, it's that these anecdotes mean little, and the actual memory safety property means a lot."

    While I use C++ a lot I am not a fan. It is just one of many languages I use. But from my personal experience it is true. I frankly forgot when was the last time I hit memory problem, years for sure. And my code is often a stateful multithreaded backends with high request rate.

    • Are you properly trying to exploit your own software?

      If you're not looking, how would you know?

      You could have a blatant SQL injection in your code and you can always pretend that it doesn't matter, since you haven't been attacked so far.

      2 replies →

> On the simple<->complex slider, it's smack dab on the right of the scale.

Sure, but now you snuck in an Artifact of Death (in TvTropes sense) into your codebase. It won't kill your code base immediately and with proper handling it might work, but all it takes is one mistake, one oversight, for it to cause issues.

  • I agreed with you either way, but I'm currently at a loss as to whether this is a for/against take on rust.

    • You traded ease of writing for ease of long term ease of debugging, I don't think we agree.

> 1. Rust is an immensely complicated language, and it's not very composable.

It is a procedural language and as such, composition of functions is not implemented easily, if at all possible. It is a shame for sure that such powerful techniques are not possible in Rust, but for some people it is worth the trade off.

  • It is implemented. But it requires quite a bit of boilerplate if you want to compose arbitrary closures at runtime (as you would in most FP languages), because that involves non-trivial overhead and Rust surfaces it in the code (see "dyn Fn trait objects" for how that works in detail).

    • Yes i know, i have wrote some FnMut closures muself. I meant, function composition should work like Lisp or Haskell, it should be very easy and intuitive, no boilerplate also. Closures in Rust, feel a lot like a hack compared to functional languages.

      The way i put it, is that Haskell is a language with very strict type system, while Rust has very strict type system, and strict scoping. Strict scopes mean that an Fn closure has to be different from a FnMut closure, which also means several other complications when it comes to async.

      This trade off is fine with me, but for several other people it doesn't worth it.

      3 replies →