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?
"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.
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.
How so? It’s clear that `a + b` calls the `+` function.
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.
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.
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.
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.
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.
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 :-/
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
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.
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.
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.
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?
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.
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.
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.
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;I believe Go does not.
> 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.
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?
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"?
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.
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?
What about Ada, Pascal, Swift, Nim, Crystal, Carbon, Jai, etc.
$ 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)