Why Zig When There Is Already C++, D, and Rust?
ziglang.org
ziglang.org
Notably, when it was written before there was not a package manager available. Now there is, so I rewrote that section. It should finish deploying in a couple minutes.
It is true that `c.d` might call a function, but it’s not because of @property functions (which exist but are discouraged), it’s because the parens are optional to call a function/method if there are no arguments.
underneath there is literally assembly to take current state, push it to the stack, then jump into another location in memory and start executing.
And then when done, do the reverse. jump back to another memory location and pop everything back off the aforementioned stack and into registers.
interrupts are also control flow but, again, there are no conditionals there.
control flow is about the order of execution, one common way to get there is using conditionals, but it's not the only way.
Having said that, even if you disagree with Andrew's wording, the overarching idea remains. You always know explicitly if the flow changes.
However I think the notion that “flow control” has to do only with conditionals is not a common one. FWIW the definition on Wikipedia, which aligns with my experience is not this restrictive… the lowly goto is flow control. Basically anything that controls the program location is flow control.
Yeah, flow control isn't just conditional jumps. You can kinda treat branchless code as a form of flow control as well.
That's the common understanding of flow control, the other poster is right in that it's not typically just conditionals. There are a few posters who were under that misapprehension, but that's never been the common understanding of it, it's just that conditionals are typically where people worry over flow control because otherwise it's linear.
And flow control and indirection are not two concepts I’ve ever grouped together as complementary either. A function call is not something I’ve ever heard as “indirection”.
Unless you ditch optimizations completely, it is hard to know without looking at the generated asm. Of course you can make educated guesses.
struct S {
int m_field;
int field() { return m_field; }
void field(int c) { m_field = x; }
}
just to future proof the field access. Don't bother with the access functions until they are actually needed.So I overrode the field access that operated directly on the byte array.
PS: It was in Kotlin, using @get/@set, but it doesn't matter for the philosophy.
If anything, I would argue that explicit accessors make it clearer because in e.g. C# you can't write a method that looks like a getter (but mutates state) by accidentally naming it as such. If someone made something a property, that's because they thought its semantics are property-like, which includes not mutating global state. Sure, people will still get that wrong occasionally, just as they run mutating "get" methods, but it is surprisingly rare.
Suppose you include a logging statement in your accessor, which hides a race condition. You're gonna tear your hair out.
When I was new to their corresponding GUI frameworks, the dot + property notation was appealing because it looked clean. However, as I got more experienced I'm no longer seeing the advantage of saving two parentheses characters at the expense of potentially hiding function calls where anything can happen.
I'm not sure why D joined the club given it doesn't have a corresponding de facto GUI framework, and its users even discourage it.
Attribute access is reading a value, invoking a function is different (as mentioned, using a stack, etc.), although it is certainly possible a function only reads and returns a value.
All that to say my answer is yes, providing flow control mechanisms makes it flow control... and falling trees make a sound!
D also supports the following memory allocation strategies:
1. RAII
2. stack allocation
3. malloc/free
4. custom alloctors
Each has its tradeoffs, you can pick the most appropriate one. The D compiler itself uses all of them :-/
I'm not saying Zig will go the same way, I hope it succeeds. But success in industry funding is not a guarantee, successful transition to large team contributions is not a guarantee, and even if all of that does pan out, by then the language may be markedly different.
Look at Rust in 2012 vs its 1.0 in 2015 and ask yourself if you'd want you and your team to have to make so many changes to production code. Ask yourself how many unknown costs you are willing to risk in exchange for whatever you believe is the actual upside of using Zig for a particular project.
Sure, somebody has to break that cycle by making those bets, but be sure before deciding to be among them. Somebody else making that bet only changes the odds so much.
(I'm not even willing to entertain the notion that they would fork Zig and maintain it long term. Even if they somehow did which would be nuts on its own, they'd still be cut off from the rest of the ecosystem and lose out on that front.)
My best case for Zig is that it offers a worthwhile option to clean up existing C & C++ projects without incurring, or at least deferring, the more substantial costs of rewriting outright in Rust, bindings or no. "Maintain it in Zig" is a sensible value proposition, or at least it will be once that Zig code has a compatibility promise so it's not trading one maintenance cost for another.
Look at how costly it was for the ecosystem to migrate from Python 2 to Python 3, even despite how many participants in that ecosystem were themselves large, well-funded corporations. Situations like that are why Go and Rust have made permanent 1.0 compatibility promises even at the cost of some tech debt which they know will only accrue further. Zig will too, but it hasn't yet, and anybody adopting it now risks unknown migration costs.
> Then again, I can’t predict the future and it could very well be that bun and zig both blow up. Same could be said for Rust.
Sure, lots of things are possible, but ask yourself which is more likely. That's all I'm saying here. Rust has already been where Zig is now and is in a meaningfully different place today: a stability promise and substantial industry adoption and contribution. It no longer depends on any one person and a lot of investment is going in to its continued success.
Anything can still fail, but there aren't that many examples of languages outright failing after reaching this point in industry adoption. There are many examples of languages outright failing by pushing backwards-incompatible changes on the industry right in the middle of what should have been a healthy growth curve. There are also many examples of simply not offering enough marginal value to justify their marginal costs.
The title here is "C++, D, and Rust" and, I'm sorry to say, but one of these is not like the other ones. Zig will have a lot of work to do in order to end up more like Rust than D, especially when memory and thread safety are a big part of why the industry is adopting Rust in the first place. Notice how Chromium does not have a Zig branch just like it never had a D branch. It was all C++ until Rust offered clear improvements to safety in exchange for its adoption costs.
Chromium has a rust branch because “Rust has X feature”, not because zig does too and there was never a decision: rust or zig.
That’s apples and oranges my friend. All I’m saying is Zig, while lagging behind its corporate sponsored behemoths, is going to be around for quite a while. There’s a lot of game dev happening with zig too. Bun is just an easy target to use as an example.
I do agree with your argument about “why choose X over Y to rewrite your codebase” but to clarify, I never made such comparisons. I wouldn’t recommend zig for a c++ refactor. Maybe a rewrite if it was small. Definitely for green field development. A stability promise is still just a promise. Backwards compatibility is a valid argument to make. Will my code from a decade ago compile today? Zig still has some growing to do before that is true.
Ghostty - A new terminal emulator written in Zig - https://zig.show/episodes/32/
Note: the original article title could perhaps be updated to include Golang as it is mentioned a few times.
You can see this happening with the duplicate stories that show up on Hacker News; in general, it's a good thing:
“There are only two kinds of languages: the ones people complain about and the ones nobody uses.” ― Bjarne Stroustrup
I'm glad people make new programming languages without asking permission from the world whether it provides enough value to exist.
(TBC, I realize this page is more about answering questions about differences between zig and other languages, emphasizing its strengths, I just think the framing of the question is unfortunate)
I do think justification is useful though - Why should one use Rust, for instance? Well, one obvious answer is that it places a high priority on memory safety, and so, if memory safety is a critical concern, Rust should be something to look at.
I am also thinking of the xkcd comic on specifications, and how creating a new one merely confuses the already crowded space, but I'm having trouble making the corollary here.
I would, however, really miss ADTs/pattern-matching if I made the switch to Zig.
To me control flow is "if", "switch", "?:", etc. What article describes are abstractions. Abstractions are not bad. They, just like anything else, can be abused. Maybe one may argue are easy to abuse or tend to be abused. But "Zig has no abstractions" is hardly a selling point.
On the other hard there are statements like "defer" and "try" which actually are very hidden and unusual control flow statements. Why the naming? Who the hell knows. I see try, I look for catch/except/finally, but there are none. Try in Zig means something else. "defer" is literally "try/finally", but less explicit about the scope.
"A Portable Language for Libraries" "A Package Manager and Build System for Existing Projects" "drop-in GCC/Clang command line compatibility with zig cc"
Is it still so after ditching LLVM?
Thank you for articulating this, I also don't understand why the author puts operator overloading in the same bucket as exceptions and `defer()`. Yes, `+` might call a function, but it will never affect the control flow of the parent function. Not more than a regular function call, at least.
He is talking about control flow that is not visible to the programmer at all. In Zig, you can understand the control flow of a particular code snippet just by learning Zig. In some other languages, operator overloading means that when you are reading unfamiliar code, you can never be sure what functions might be called.
This just means abstraction were abused. Which as I said, they can be. Nevertheless most popular programming languages (C++, C#, Python, Java, etc.) have overloaded operators, you can write "hello " + "world" (or equivalent) in any of these languages.
I would:
1. Prohibit concatenating strings with different allocators. Strings with the same allocator can be concatenated.
2. Provide StringBuilder for concatenating anything in an effective manner. It's not only about different allocators, but supporting C-style strings and reducing number of allocations.
So '+' is only for strings with same allocator, same UTF encoding, same everything. I'm really not fond of implicit type conversions.
How so? It’s clear that `a + b` calls the `+` function.
"if" and "switch" determine control flow, but you can see the control flow when you're reading that code.
In languages like C++ and Java, if you have a sequence like:
foo();
bar();
whether execution continues to bar() after foo() returns depends on the implementation of foo(), which you can't infer from the callsite. If foo() throws an exception, bar() won't execute.In Zig, the compiler forces you to handle exceptions (errors) at the callsite, so you can see from looking at function calls at the callsite what the potential control flow looks like.
If you saw the foo(), bar() sequence in Zig, you'd know that bar() always executes after foo(). If it were possible for foo() to return an error, that code wouldn't compile unless the callsite handled the error explicitly with a try. The try tells the reader that there's a potential control flow depending on whether foo() throws an error.[0]
Damn, if only Java had something like that. Where you can see from function declaration whether it can throw something. Like if you could check exception. We could call it checked exceptions. And community would certainly like them.
Even if a function is small, each and every line could be the thrower and not allow the rest to execute.
There is no way to make e.g. InterruptedException "work properly" as a checked exception since there is nothing intelligent one can do about it that's specific to this exception class. Rewrapping as RuntimeEx is just as much weaseling out but using more code and messing up stack trace for no good reason.
Also, thanks for answering my request for a fight. Winter is boring.
Exceptions that aren't meant to be handled really shouldn't be exceptions in the first place, but that's a separate design issue.
The syntax problem gets mentioned a lot ("we'll just end up catching and rethrowing it anyway") but at that point you can just add throws to the calling method.
From my experience, when there's surprising behavior in software, I've frequently been surprised by silent exceptions, but I can't think of any time when I was surprised by control flow because a function intentionally shut down the program.
Abstractions are still possible, the only difference is the syntax with which they're presented. For instance, you need an "add" function for your addition abstraction, rather than overloading the + symbol.
The only thing missing in Zig is the syntactic sugar of such abstractions; the argument being that such sugar obscures more than it illuminates.
In Python or C# situation is quite different. For example I struggle recall a single case from my experience when property was confusing or was used in a confusing manner.
Abstractions are fine, of course. In Zig, though, they should be explicit and clear. E.g., the only way to call a function is by name, using the function-calling syntax. Control flow depends only on keywords and syntax directly in front of you.
I think they do assume you known the language -- if you don't then I guess a lot might be unclear when reading code! -- but on the other hand, it's on the simpler, smaller side.
I don't think you can infer anything from the examples being abstractions.
Zig hasn't ditched LLVM, nor does it really plan to. What's planned is for the main Zig executable to not directly depend on LLVM, that way contributors to the compiler can still develop on the code base without needing to set up and compile LLVM on their systems. Zig does plan to eventually supersede LLVM's functions with their own custom backends, but in the short term, this only means using the new backend as a debug compiler (for faster compilations, to allow for a better developer feedback loop) and it won't be until the distant future that LLVM could feasibly be dropped as a dependency entirely, if the devs ever decide to actually go through with that.
If Zig really wanted to do the best job here, it should recognise that we're semantically not using this 4th padding dimension if we're not doing 4D projective stuff. If it really is going to force you up to 4D space, then it should be doing compile time checks e.g. sorry, you tried to add point plus a point which makes no sense, unlike vectors, or vectors and points.
This might become more important if Zig to GPU code translation becomes a thing...
Okay, you can solve the forget problem with static analysis. But since in most cases I don't care about destruction so long as it happens, when reading the code I prefer this hidden.
Abstraction means hiding details and done right is a great thing. Yes abstraction often is abused to hide the wrong thing, but that isn't the fault of abstraction as a concept.
withThing { thing => doStuffWith(thing) }
This makes it clear to me that the object is only valid within this block. There is no copy constructor madness where I might completely lose track of where the object might be alive and in scope.Objects also won't be silently copied unless they are types that explicitly marked as trivial to copy (i.e. bytewise).
It then also guarantees cleanup if the object actually does exist.
Rust already does what many people want for deterministic, safe cleanup, which like C++ includes RAII, but unlike C++ is not limited to RAII.
But you might want to care about where in the code the destruction happens. This is quite normal in a low level language. Of course I do agree that it should be easy to avoid leaking resources, but static analysis can ensure this in most cases as you point out.
If the new variable has a smaller scope, then the object will be destroyed sooner than it would have been otherwise. In particular, to assist in early cleanup, the standard library has a drop(value) function, that takes ownership of any value and immediately lets it fall out of scope, destroying it.
This is especially useful for things like destroying RAII mutex guards to immediately unlock the corresponding mutex.
If you're writing the kind of things where you don't care at all about destruction so long as it happens, why even consider something as low-level as Zig? There are many other languages already covering that niche.
As an aside, solely for the purpose of making Zig usable as an environment for people whose primary professional responsibility is not really software (quantitative researchers, data scientists, etc.) I would prefer if RAII was somehow very painful to implement for a type rather than impossible. That's the status quo for dynamic dispatch.
I'm sure Zig has its use cases. For what I write, I not only don't care if there's a hidden function call or error handling, I see those as 100% necessary for a modern language.
Needing to handle errors inline is a huge mess for anything nontrivial. It distracts from the logic that's important at that point in the code. Being able to override an accessor to do something instead of being a raw access is incredibly useful; a tiny change and rebuild is all that's required to track information that you would otherwise need to rewrite an entire app to support.
If you're writing extremely low level code and libraries, especially embedded, then fine, minimizing hidden behavior is important. Being able to operate without a standard library is also important in that case. Outside of that niche, though, there are few places I'd call those "features" of Zig an advantage.
Maybe try zig first before you make such a blanket statement?
It isn't that much more work and it's not something you end up doing terribly often. I think destructors are a much more important to help day to day development and avoid bugs.
That doesn't mean Zig couldn't do something similar if they want to, just pointing out that it's not always the case that it means dynamic dispatch.
This is true for the standard library, where I can trust that no hidden allocations will be made. If I pull in a third party package, nothing guarantees that that lib won't init a new `std.heap.GeneralPurposeAllocator(...)`, right?
But this is one of those things which just wouldn't happen in practice: custom allocators are so embedded in the Zig philosophy that I cannot imagine competent Zig programmers writing a library that did something like this. It's just not what you do in Zig.
The kind of "capabilities" style languages you are talking about almost always have either a runtime that handles the actual syscalls, or they don't have the capability to compile directly to the assembly you need, everything has to pass through some library. Zig does not fit into either category: it has no runtime, and the whole point of the language is to be a low-level C replacement.
For example, I could see these being attractive in embedded work. I could see the simplicity becoming a headache in other contexts, like making abstracted all-purpose numerical libraries a la Eigen.
Maybe I am wrong about those particulars! But I would like the arguments completed: Zig has X distinctive features, which you should prefer if you do Y.
This is relevant to my interests right now: I am working on scaling up some scientific algorithms for astronomy research which have been well prototyped in Python, but which need to be faster. I am currently, unhappily, doing my work in C++ after abandoning Rust for being too immature in its CUDA and SIMD support, while also feeling pretty complex. Would Zig do well enough for me? I want a little more ink on the page to help me think this through.
So, read the article and determine - "Does Zig with X features solve my use case of me trying to build Z after trying with Y"?
> Go’s defer allocates memory to a function-local stack.
Not really hidden if there's keyword indicating it's happening, no? I guess you could argue that "defer" isn't called "defer_and_allocate", but on the other hand, the docs make it clear that it allocates.
Using async/await as an example, you have to have an async implementation (red) of a function and a non-async implementation (blue). In this case, the callee chooses the behavior.
If instead you had a function that took some sort of runtime as a parameter that it could use to run downstream code synchronously or asynchronously, then you'd have something comparable to Zig's allocator. The red and blue functions would instead be one function where you pass a different runtime.
From a code organization perspective, slightly, only in the sense that must ensure that you free memory using the same allocator that allocated it.
Maybe this could be resolved by distinguishing between writing new libraries in Zig and building applications. New libraries can be clean and reusable even if the applications are a hodgepodge of dependencies.
For libraries, reuse versus rewrite is still a question. With any new language ecosystem that provides new guarantees, there’s a drive to build a new set of libraries within the ecosystem to get those guarantees. This is an enormous effort. An end goal of making libraries more reusable, to stop rewriting them, seems in tension with the means of getting there, which is by rewriting rather than reusing libraries.
Also, if you write a library in Zig, aren’t you limiting your audience?
The Zig compiler has a C backend so at worst you could just compile your library to C so that anyone with a C compiler can use it. However you can also export C-compatible functions, which is roughly analogous to having an `extern "C"` interface for your C++ library. The consumer would need a Zig compiler, of course, if they wanted to build from source.
Rust's addition of operator overloading is also a perplexingly bad decision. I don't think there is a single worse design decision in C++ or Rust than allowing someone to redefine what + means on some arbitrary type.
Why is that? Won’t it be more ergonomic than needing to call, say `.add`, when you want to do something clearly similar to addition. It’s just sugar, no?
In Java, there is no operator overloading, so if you want to compare 2 strings by contents rather than their memory addresses, you have to use a `.equals` method. Comparing 2 strings by their contents is the most common use case, so it not being the default is a design flaw (similar to how you need to `break` out of a `case` in most languages).
If you can’t overload the addition operator, then why have a whole language construct that can only be used on primitive integers and floats?
Especially since, in a typed language, this function would be desugared at compile time and (probably) aggressively inlined, making it hardly different from the compiler builtins for adding floats and ints. Unless, of course, you're doing something like allocating to concatenate strings.
Really, though, it's more of a developer common-sense issue than a language one.
This complaint is always made in the context of some pathological case where a library author tries to do clever things with operator overloading; if any such libraries exist in the first place, you can guarantee that you are not using them by simply not using any libraries with fewer than 10k downloads. The horror stories that fill HN comments pretty much never make it into real code.
What you are referring to as readability is actually terseness, which I think is a lousy metric to optimize for, especially for systems software where correctness is important and people will read code a lot more than they will write it.
This has perplexed us for D. Experience with C++'s iostream's operator overloading meant running away screaming. Another terrible thing is people would code up DSL's using operator overloading, such as a Regex language. The horror there is the source code looks like ordinary C++ arithmetic, but it is actually doing Regexes.
So, how to allow operator overloading for arithmetic, but not for other porpoises?
1. Only allow overloading of arithmetic operators (i.e. no overloading of unary *) and [ ]
2. Only allow < overloadable, instead of < <= > >=. This enforces symmetry.
3. Don't allow overloading of && || ?:
4. A strong Compile Time Function Execution feature which enables DSLs in the form of string literals
5. Develop a culture of operator overloading is for arithmetic
This has worked well.
There's nothing wrong with this if your language has proper hygienic macros. Then you can have all of math_expr![ … ], float_expr![ … ] (dangerous! order of operations may affect results), regex_expr![ … ], or even stream_concat_expr![ … ] all using the same operators while meaning completely different things and preserving complete extensibility. They would even be composable since each macro invocation would desugar its own operators and leave those in other contained macros unaltered.
You know, I don't even have a side on this never ending debate... but it's perplexing that similarly intelligent people, with similar interests and backgrounds can both make so confident blank statements such as this, one way or another! IMO that is a pretty good indication that there's no right answer, it's simply a matter of preference... and the fact that people still feel like they're right and the people who disagree with them must be making "perplexing bad decisions" is, for lack of a better word, hilarious.
Yes. The poor man’s custom operators.
$ time make build-game
real 0m0.387s
user 0m0.309s
sys 0m0.077s
$ time make build-sgame
real 0m0.334s
user 0m0.245s
sys 0m0.057s
Complete full rebuilds of my game and game server on linux (on windows it is the double)Depends on what you work on, I guess, but in my area of expertise string operations like concatenation are bread-and-butter.
string b = "betty";
string s = "hello " ~ b;Although among those new statically compiled languages, zig would be my favorite choice.
I just wish it was just even simpler.
But there is a huge gap between ability to learn C and ability to write working software in C.
I've inherited a small (500 loc) C codebase with ldpreloadable library. Guess how many crashes I've fixed already? 7! And it's a security sensitive code supposed to run under root.
Can you elaborate?
I believe Go does not.
Person A claims that X is true.
Person A is an expert in the field concerning X.
Therefore, X should be believed.
Error handling != exceptionsIf it quacks is a duck.
You can use poor man's exceptions in Go via panic/recover pattern.
I feel a lot of useful software will be written in Zig once it is picked by young/upcoming clever developers.
But then, it also matters that it not trap you. It's easy to only have to learn a little when a little is all there is. But you don't want to use a language like that for something major, because it won't do enough for you.
So what you need is something like what the UI people call "progressive disclosure". Are there any languages that do that well?
What about Ada, Pascal, Swift, Nim, Crystal, Carbon, Jai, etc.