What interests me most by zig is the ease of the build system, cross compilation, and the goal of high iteration speed. I'm a gamedev, so I have performance requirements but I think most languages have sufficient performance for most of my requirements so it's not the #1 consideration for language choice for me.
I feel like I can write powerful code in any language, but the goal is to write code for a framework that is most future proof, so that you can maintain modular stuff for decades.
C/C++ has been the default answer for its omnipresent support. It feels like zig will be able to match that.
> I feel like I can write powerful code in any language, but the goal is to write code for a framework that is most future proof, so that you can maintain modular stuff for decades.
I like Zig a lot, but long-term maintainability and modularity is one of its weakest points IMHO.
Zig is hostile to encapsulation. You cannot make struct members private: https://github.com/ziglang/zig/issues/9909#issuecomment-9426...
Key quote:
> The idea of private fields and getter/setter methods was popularized by Java, but it is an anti-pattern. Fields are there; they exist. They are the data that underpins any abstraction. My recommendation is to name fields carefully and leave them as part of the public API, carefully documenting what they do.
You cannot reasonably form API contracts (which are the foundation of software modularity) unless you can hide the internal representation. You need to be able to change the internal representation without breaking users.
Zig's position is that there should be no such thing as internal representation; you should publicly expose, document, and guarantee the behavior of your representation to all users.
I hope Zig reverses this decision someday and supports private fields.
I disagree with plenty of Andrew's takes as well but I'm with him on private fields. I've never once in 10 years had an issue with a public field that should have been private, however I have had to hack/reimplement entire data structures because some library author thought that no user should touch some private field.
> You cannot reasonably form API contracts (which are the foundation of software modularity) unless you can hide the internal representation. You need to be able to change the internal representation without breaking users.
You never need to hide internal representations to form an "API contract". That doesn't even make sense. If you need to be able to change the internal representation without breaking user code, you're looking for opaque pointers, which have been the solution to this problem since at least C89, I assume earlier.
If you change your data structures or the procedures that operate on them, you're almost certain to break someone's code somewhere, regardless of whether or not you hide the implementation.
Most data structures have invariants that must hold for the data structure to behave correctly. If users can directly read and write members, there's no way for the public APIs to guarantee that they will uphold their documented API behaviors.
Take something as simple as a vector (eg. std::vector in C++). If a user directly sets the size or capacity, the calls to methods like push_back() will behave incorrectly, or may even crash.
Opaque pointers are one way of hiding representation, but they also eliminate the possibility of inlining, unless LTO is in use. If you have members that need to be accessible in inline functions, it's impossible to use opaque pointers.
There is certainly a risk of "implicit interfaces" (Hyrum's Law), where users break even when you're changing the internals, but we can lessen the risk by encapsulating data structures as much as possible. There are other strategies for lessening this risk, like randomizing unspecified behaviors, so that people cannot take dependencies on behaviors that are not guaranteed.
> I've never once in 10 years had an issue with a public field that should have been private, however I have had to hack/reimplement entire data structures because some library author thought that no user should touch some private field.
Very similar experience here. Also just recently I really _had_ to use and extend the "internal" part of a legacy library. So potentially days or more than a week of work turned into a couple of hours.
Like unclad, I disagree that not having private fields is a problem. I think this comes down to programming style. For an OOP style (Just one example), I can see how that would be irritating. Here's my anecdote:
I write a lot of rust. By default, fields are private. It's rare to see a field in my code that omits the `pub` prefix. I sometimes start with private because I forget `pub`, but inevitably I need to make it public!
I like in principle they're there, but in practice, `pub` feels like syntactic clutter, because it's on all my fields! I think this is because I use structs as abstract bags of data, vice patterns with getters/setters.
When using libraries that rely on private fields, I sometimes have to fork them so I can get at the data. If they do provide a way, it makes the (auto-generated) docs less usable than if the fields were public.
I suspect this might come down to the perspective of application/firmware development vice lib development. The few times I do use private fields have been in libs. E.g. if you have matrix you generate from pub fields and similar.
This is only a problem if you can't modify the library you're using for whatever reason (usually a bad one). If you have the source of all your dependencies, you can just fork and add methods as needed in the rare cases where you need to do this.
Some years ago I started to just not care about setting things to "private" (in any language). And I care _a lot_ about long term maintainability and breakage. I haven't regretted it since.
> You cannot reasonably form API contracts (...) unless you can hide the internal representation.
Yes you can, by communicating the intended use can be made with comments/docstrings, examples etc.
One thing I learned from the Clojure world, is to have a separate namespace/package or just section of code, that represents an API that is well documented, nice to use and more importantly stable. That's really all that is needed.
(Also, there are cases where you actually need to use a thing in a way that was not intended. That obviously comes with risk, but when you need it, you're _extremely_ glad that you can.)
I have the opposite experience. Several years ago I didn't worry too much about people using private variables.
Then I noticed people were using them, preventing me from making important changes. So I created a pseudo-"private" facility using macros, where people had to write FOOLIB_PRIVATE(var) to get at the internal var.
Then I noticed (I kid you not) people started writing FOOLIB_PRIVATE(var) in their own code. Completely circumventing my attempt to hide these internal members. And I can't entirely blame them, they were trying to get something done, and they felt it was the fastest way to do it.
After this experience, I consider it an absolute requirement to have a real "private" struct member facility in a language.
I respect Andrew and I think he's done a hell of a job with Zig. I also understand the concern with the Java precedent and lots of wordy getters/setters around trivial variables. But I feel like Rust (and even C++) is a great counterexample that private struct variables can be done in a reasonable way. Most of the time there's no need to have getters/setters for every individual struct member.
> The idea of private fields and getter/setter methods was popularized by Java, but it is an anti-pattern.
I agree with this part with no reservations. The idea that getters/setters provide any sort of abstraction or encapsulation at all is sheer nonsense, and is at the root of many of the absurdities you see in Java.
The issue, of course, is that Zig throws out the baby with the bath water. If I want, say, my linked list to have an O(1) length operation, i need to maintain a length field, but the invariant that list.length actually lines up with the length of the list is something that all of the other operations need to maintain. Having that field be writable from the outside is just begging for mistakes. All it takes is list.length = 0 instead of list.length == 0 to screw things up badly.
You can have a debug time check.
Just prefix internal fields with underscore and be a big boy and don't access them from the outside.
If you really need to you can always use opaque pointers for the REALLY critical public APIs.
I am not the only user of my API, and I cannot control what users do.
My experience is that users who are trying to get work done will bypass every speed bump you put in the way and just access your internals directly.
If you "just" rely on them not to do that, then your internals will effectively be frozen forever.
[flagged]
I recently, for fun, tried running zig on an ancient kindle device running stripped down Linux 4.1.15.
It was an interesting experience and I was pleasantly surprised by the maturity of Zig. Many things worked out of the box and I could even debug a strange bug using ancient GDB. Like you, Iām sold on Zig too.
I wrote about it here: https://news.ycombinator.com/item?id=44211041
I've dabbled in Rust, liked it, heard it was bad so kind of paused. Now trying it again and still like it. I don't really get why people hate it so much. Ugly generics - same thing in C# and Typescript. Borrow checker - makes sense if you have done low level stuff before.
If you don't happen to come across some task that implies a data model that Rust is actively hostile towards (e.g. trees with backlinks, or more generally any kind of graph with cycles in it), borrow checker is not much of a hassle. But the moment you hit something like that, it becomes a massive pain, and requires either "unsafe" (which is strictly more dangerous than even C, never mind Zig) or patterns like using indices instead of pointers which are counter to high performance and effectively only serve to work around the borrow checker to shut it up.
> patterns like using indices instead of pointers which are counter to high performance
Using indices isn't bad for performance. At the very least, it can massively cut down on memory usage (which is in turn good for performance) if you can use 16-bit or 32-bit indices instead of full 64-bit pointers.
> "unsafe" (which is strictly more dangerous than even C, never mind Zig)
Unsafe Rust is much safer than C.
The only way I can imagine unsafe Rust being more dangerous than C is that you need to keep exception safety in mind in Rust, but not in C.
> the moment you hit something like that, it becomes a massive pain, and requires either "unsafe" (which is strictly more dangerous than even C, never mind Zig) or patterns like using indices instead of pointers
If you need to make everything in-house this is the experience. For the majority though, the moment you require those things you reach for a crate that solves those problems.
>which is strictly more dangerous than even C, never mind Zig
No it's not? The Rust burrow checker, the backbone of Rust's memory safety model, doesn't stop working when you drop into an unsafe block. From the Rust Book:
>To switch to unsafe Rust, use the unsafe keyword and then start a new block that holds the unsafe code. You can take five actions in unsafe Rust that you canāt in safe Rust, which we call unsafe superpowers. Those superpowers include the ability to:
Dereference a raw pointer
Call an unsafe function or method
Access or modify a mutable static variable
Implement an unsafe trait
Access fields of a union
Itās important to understand that unsafe doesnāt turn off the borrow checker or disable any of Rustās other safety checks: if you use a reference in unsafe code, it will still be checked. The unsafe keyword only gives you access to these five features that are then not checked by the compiler for memory safety. Youāll still get some degree of safety inside of an unsafe block.(This is a reply to multiple sibling comments, not the parent)
For those saying unsafe Rust is strictly safer than C, you're overlooking Rust's extremely strict invariants that users must uphold. These are much stricter than C, and they're extremely easy to accidentally break in unsafe Rust. Breaking them in unsafe Rust is instant UB, even before leaving the unsafe context.
This article has a decent summary in this particular section: https://zackoverflow.dev/writing/unsafe-rust-vs-zig/#unsafe-...
Haters gonna hate. If you're working on a project that needs performance and correctness, nothing can get the job done like Rust.
unless you have to do anything that relies on a C API (such as provided by an OS) with no concept of ownership, then it's a massive task to get that working well with idiomatic rust. You need big glue layers to really make it work.
Rust is a general purpose language that can do systems programming. Zig is a systems programming language.
(Safety Coomers please don't downvote)
Both are great languages. To me there's a philosophical difference, which can impact one to prefer one over the other:
Rust makes doing the wrong thing hard, Zig makes doing the right thing easy.
Zig seems to be simpler Rust and better Go.
Off topic - One tool built on top of Zig that I really really admire is bun.
I cannot tell how much simpler my life is after using bun.
Similar things can be said for uv which is built in Rust.
Zig is nothing like Go. Go uses GC and a runtime while Zig has none. While Zigās functions arenāt coloured, it lacked the CSP style primitives like goroutines and channels.
Zig is like a highly opinionated modern C
Rust is like a highly opinionated modern C++
Go is like a highly opinionated pre-modern C with GC
In a previous comment, you remarked you donāt even know Zig.
Go should be as much in this discussion as JavaScript.
> In fact, even state-of-art compilers will break language specifications (Clang assumes that all loops without side effects will terminate).
I don't doubt that compilers occasionally break language specs, but in that case Clang is correct, at least for C11 and later. From C11:
> An iteration statement whose controlling expression is not a constant expression, that performs no input/output operations, does not access volatile objects, and performs no synchronization or atomic operations in its body, controlling expression, or (in the case of a for statement) its expression-3, may be assumed by the implementation to terminate.
C++ says (until the future C++ 26 is published) all loops, but as you noted C itself does not do this, only those "whose controlling expression is not a constant expression".
Thus in C the trivial infinite loop for (;;); is supposed to actually compile to an infinite loop, as it should with Rust's less opaque loop {} -- however LLVM is built by people who don't always remember they're not writing a C++ compiler, so Rust ran into places where they're like "infinite loop please" and LLVM says "Aha, C++ says those never happen, optimising accordingly" but er... that's the wrong language.
> Rust ran into places where they're like "infinite loop please" and LLVM says "Aha, C++ says those never happen, optimising accordingly" but er... that's the wrong language
Worth mentioning that LLVM 12 added first-class support for infinite loops without guaranteed forward progress, allowing this to be fixed: https://github.com/rust-lang/rust/issues/28728
For some context, 12 was released in April 2021. LLVM is now on 20 -- the versions have really accelerated in recent years.
Sure, that sort of language-specific idiosyncrasy must be dealt with in the compiler's front-end. In TFA's C example, consider that their loop
while (i <= x) {
// ...
}
just needs a slight transformation to while (1) {
if (i > x)
break;
// ...
}
and C11's special permission does not apply any more since the controlling expression has become constant.Analyzes and optimizations in compiler backends often normalize those two loops to a common representation (e.g. control-flow graph) at some point, so whatever treatment that sees them differently must happen early on.
In theory, in practice it depends on the compiler.
It is no accident that there is ongoing discussion that clang should get its own IR, just like it happens with the other frontends, instead of spewing LLVM IR directly into the next phase.
You don't really need comptime to be able to inline and unroll a string comparison. This also works in C: https://godbolt.org/z/6edWbqnfT (edit: fixed typo)
Yep, you are correct! The first example was a bit too simplistic. A better one would be https://github.com/RetroDev256/comptime_suffix_automaton
Do note that your linked godbolt code actually demonstrates one of the two sub-par examples though.
I haven't looked at the more complex example, but the second issue is not too difficult to fix: https://godbolt.org/z/48T44PvzK
For complicated things, I haven't really understood the advantage compared to simply running a program at build time.
To be honest your snippet isn't really C anymore by using a compiler builtin. I'm also annoyed by things like `foo(int N, const char x[N])` which compilation vary wildly between compilers (most ignore them, gcc will actually try to check if the invariants if they are compile time known)
> I haven't really understood the advantage compared to simply running a program at build time.
Since both comptime and runtime code can be mixed, this gives you a lot of safety and control. The comptime in zig emulates the target architecture, this makes things like cross-compilation simply work. For program that generates code, you have to run that generator on the system that's compiling and the generator program itself has to be aware the target it's generating code for.
> As an example, consider the following JavaScript codeā¦The generated bytecode for this JavaScript (under V8) is pretty bloated.
I don't think this is a good comparison. You're telling the compiler for Zig and Rust to pick something very modern to target, while I don't think V8 does the same. Optimizing JITs do actually know how to vectorize if the circumstances permit it.
Also, fwiw, most modern languages will do the same optimization you do with strings. Here's C++ for example: https://godbolt.org/z/TM5qdbTqh
In general it's a bit of an apples to fruit salad comparison, albeit one that is appropriate to highlight the different use-cases of JS and Zig. The Zig example uses an array with a known type of fixed size, the JS code is "generic" at run time (x and y can be any object). Which, fair enough, is something you'd have to pay the cost for in JS. Ironically though in this particular example one actually would be able to do much better when it comes to communicating type information to the JIT: ensure that you always call this function with Float64Arrays of equal size, and the JIT will know this and produce a faster loop (not vectorized, but still a lot better).
Now, one rarely uses typed arrays in practice because they're pretty heavy to initialize so only worth it if one allocates a large typed array one once and reuses them a lot aster that, so again, fair enough! One other detail does annoy me a little bit: the article says the example JS code is pretty bloated, but I bet that a big part of that is that the JS JIT can't guarantee that 65536 equals the length of the two arrays so will likely insert a guard. But nobody would write a for loop that way anyway, they'd write it as i < x.length, for which the JIT does optimize at least one array check away. I admit that this is nitpicking though.
You can change the `target` in those two linked godbolt examples for Rust and Zig to an older CPU. I'm sorry I didn't think about the limitations of the JS target for that example. As for your link, It's a good example of what clang can do for C++ - although I think that the generated assembly may be sub-par, even if you factor in zig compiling for a specific CPU here. I would be very interested to see a C++ port of https://github.com/RetroDev256/comptime_suffix_automaton though. It is a use of comptime that can't be cleanly guessed by a C++ compiler.
I just skimmed your code but I think C++ can probably constexpr its way through. I understand that's a little unfair though because C++ is one of the only other languages with a serious focus on compile-time evaluation.
> High level languages lack something that low level languages have in great adundance - intent.
Is this line really true? I feel like expressing intent isn't really a factor in the high level / low level spectrum. If anything, more ways of expressing intent in more detail should contribute towards them being higher level.
I agree with you and would go further: the fundamental difference between high-level and low-level languages is that in high-level languages you express intent whereas in low-level languages you are stuck resorting to expressing underlying mechanisms.
I think this isn't referring to intent as in "calculate the tax rate for this purchase" but rather "shift this byte three positions to the left". Less about what you're trying to accomplish, and more about what you're trying to make the machine do.
Something like purchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?; is full of intent, but you have no idea what kind of machine code you're going to end up with.
In other words, high-level languages express high-level intents, while low-level languages express low-level intents.
In yet other words, tautology.
Maybe, but from the author's description, it seems like the interpretation of intent that they want is to generally give the most information possible to the compiler, so it can do its thing. I don't see why the right high level language couldn't give the compiler plenty of leeway to optimize.
That for loop syntax is horrendous.
So I have two lists, side by side, and the position of items in one list matches positions of items in the other? That just makes my eyes hurt.
I think modern languages took a wrong turn by adding all this "magic" in the parser and all these little sigils dotted all around the code. This is not something I would want to look at for hours at a time.
Such arrays are an extremely common pattern in low-level code regardless of language, and so is iterating them in parallel, so it's natural for Zig to provide a convenient syntax to do exactly that in a way that makes it clear what's going on (which IMO it does very well). Why does it make your eyes hurt?
It looks to me like:
for (one, two, three) |uno, dos, tres| { ... }
My eyes have to bounce back and forth between the two lists. When the identifiers are longer than this example it increases eye strain. Maybe it's better when you wrote it and understand it, but trying to grok someone else's code, it feels like an obstacle to me.> Rust's memory model allows the compiler to always assume that function arguments never alias. You must manually specify this in Zig.
I've avoided such manual specification of aliasing because:
1. few people understand it
2. using it erroneously can result in baffling bugs in your code
> The flexibility of Zig's comptime has resulted in some rather nice improvements in other programming languages.
Compile time function execution and functions with constant arguments were introduced in D in 2007, and resulted in many other languages adopting something similar.
Get a daily email with the the top stories from Hacker News. No spam, unsubscribe at any time.