Looking at Zig programming language

Back when I wrote my rant about C++ and its bad influence on C (yeah, about three quarters of year ago) I got recommendations to look at Zig and finally decided to download 0.9.0 release and play it. Long story short: it’s an interesting language with some good ideas but not the one I’d use.

First of all, Zig feels more like a cross between D and Go, while Rust coming from ML family of programming languages ironically feels more like C—and I’m still a C programmer. The features like importing (sub)packages like const fs = @import("std").fs; or being able to compile C with the same compiler are well-known features of D. While postfix notation (e.g. var foo: u8 or pub fn main() !void), defer and having JSON support in the standard library definitely reminds of Go (and I was surprised to find support for various binary formats like ELF, COFF or PDB in std; compared to that std.Progress is nothing special). No idea though where pointer dereferencing as foo.* = 42; comes from. Additionally Zig has optional type and error built-in types which is a bit strange for a language posing as simple (in Rust they rightfully belong to the standard library as there’s nothing there that prevents them from being implemented in a language itself); probably it was a verbosity trade-off since Rust Option<OptValue> and Result<RetType, ErrType> is bulkier than Zig ?OptValue and ErrType!RetType respectively (and it makes applying special rules to those types harder).

And speaking of verbosity and error types, Zig is too verbose in all the wrong places. Error handling all works fine and try doStuff(); is no worse than doStuff()?; in Rust, the problems come when you don’t simply want to return on error. You can’t ignore return result and, developing further the infamous Go result, err := func(); idiom, you always have to process both values. While in Rust I can write if file.write(data).is_err() { ... } in Zig the compiler will complain if you don’t use actual error value in some way or if you do not use result value (but luckily you can send it to bit bucket) which means that proper code in this case looks like _ = file.write(data) catch |err| { silence_err_somehow(err); ... }.

Side note: I find Rust approach of making variables constant by default better in terms of preventing programmer from making certain class of mistakes (but it gets a bit annoying when most of your variables are mutable, so it’s more the matter of taste).

Then you have loops that disagree with my tastes. C has extremely powerful for(;;) loop, in Rust you have for X in Y loop taking an iterator plus there are ranges and other utility functions to replicate some of the C for(;;) features (e.g. for i in (0..10).rev().step_by(3) instead of for(i = 9; i >= 0; i -= 3)). In Zig you have for iter |item[, index]| and for the traditional C-like loop you have to use while with post-condition like while (i < 10) : (i += 1) {}. And I won't even talk about while with else clause.

What I really dislike though is rather inconsistent (again, to my tastes) language design. You have language keywords and you have built-in functions with rather arbitrary grouping. For instance, built-in functions include: package importing directives (@import, @cImport), type casting (@as, @ptrCast, @boolToInt), special instructions (atomic operations, @breakpoint, vector operations) and generic calculations (like @cos or @sqrt). I guess some of those were introduced to make for the lack of generics (and where comptime can't help) but I'd rather have many of those functions either moved into the standard library or associated with the proper types i.e. val.clz() instead of @clz(val) (and I'd make undefined a built-in function too instead of a keyword). And I'd like to have them separated in syntax by purpose. I'm not going to mention how it's done in Rust but even in C with all its deficiencies you had preprocessor directives starting with a grille sign e.g. #include and built-in compiler functions were conventionally prefixed with two underscores e.g. __builtin_clz(). The rest looked like normal language constructs and I didn't have to care how offsetof(struct, field) is implemented or whether the compiler is smart enough to use x87 instruction (or SSE nowadays) for cosine calculation. To repeat it again, I know that the current scheme is caused by some self-imposed limitations coming from the language and the compiler design principles but they don't align with me.

To offset negativity I should mentions some things in Zig that I found useful or at least interesting. First of all, not explicitly specifying array size (you can't do that in Rust for constants even if the compiler should be able to figure out array size from its contents). Built-in tests that have a free-form description are also a good idea (in Rust you can only name test function more creatively). Explicit wrapping operations for integers are nice (and writing foo -%= bar; is not as verbose as foo = foo.wrapping_sub(bar);). Sentinel-terminated arrays/slices is an interesting idea. Explicit compile-time calculations look very helpful. A dedicated opaque sounds like a good idea. The ability to compile C or C++ code is a nice feature (and I wish rustc would have a stand-alone assembly files support). There are some other nice compiler features (like cross-compiling out of the box) but they seem to be related to the compiler itself and not the language.

Overall, Zig is by no means is a bad language (and looks good enough to write a multimedia framework in it among other things), it just does not align well with my tastes. Let's wait and see how it develops.

12 Responses to “Looking at Zig programming language”

  1. duck says:

    Zig is a fad, a friend of my tried it and he likes it alot though.

  2. Kostya says:

    There are many programming languages around and you never know which one will be popular. I’m all for the language diversity—even if it does not succeed it may bring new ideas to other languages (just read about CLU).

  3. Paul says:

    Ultimate programming language is assembly.

  4. EHT_shiniori says:

    yeah but is it (Zig) useful for “beginners”? as in, people who may do some programming without knowing anything about it beforehand.

    i’m currently doing stuff with Python but not on a regular basis.

  5. Kostya says:

    @Paul
    Indeed it is except on some platforms it can get too far from the real hardware (just read about IBM HLASM).

    @EHT_shiniori
    I’m not a beginner nor a CS teacher so it’s hard for me to tell. On one hand Zig is simple (but as Paul noted, assembly is even simpler), on the other hand it seems to lack proper tutorial (in other words, the one you can learn it without knowing other programming languages already) and some concepts are not easy to grasp (like why you should always write _ = try file.write(data); while in other languages it can be done simpler).

    I suppose you should take a scripting language (Python or Ruby nowadays) if you want some task done quickly and without much hassle. And learning programming language concepts is probably better done with some language developed for that purpose like Pascal (even if I don’t like it).

  6. EHT_shiniori says:

    that’s just about fair enough. the way i learn programming languages is not exactly “easy” by any sense of the word, sometimes i just can’t understand certain things and just give up on them. never to return until i somehow come back to it.

    it’s been this way with Python.

  7. Lee says:

    Assembly is not simple in the slightest, compared to lambda calculus. Assembly semantics differ between processors, even those in the same family. Lambda calculus is the same no matter where you are.

    I’ve been following Zig for a long while, but the one time I tried to give it a shot, I encountered a compiler bug, so it was too early. I too like its ideas but trying to be “a better C” was probably never a great goal.

    I’m still waiting for the day of approachable FPs to be popular amongst the masses.

  8. Kostya says:

    The word “calculus” alone tells you enough. And that’s the main divide between imperative and functional programming languages: in one case you tell the computer what to do (with varying level of abstraction), in another you describe formally what you want to have and leave it up to the compiler to figure out how to do it.

    Neither approach is bad and both have perfect use cases. Now if only people learned how to recognize them…

  9. hanz says:

    What is zig? is it a new tool programmers or hackers whether white, gray, or black hat can use to attain their goals? For months I been trying to understand the language: what is about? Is this just another language that will try to get popular for a year or two then begin to decline and never reach the position where c/c++ and java as it claims that it’s better than c/c++ or java just like others? I really don’t know. But, I like it, learning it, and one day, if it becomes popular just like python with lots of libraries, then, I can use it. No one really knows what’s gonna happen in the future.

  10. Kostya says:

    I got the impression that Zig is an attempt to improve of C (not C++, not Java). Its author starts from C and wonders what he can improve there to make it better (in his own eyes at least, YMMV): mandatory error handling, stricter types, additional language features and so on.

    Probably it’ll not grow into a language for writing large application projects but maybe it’ll replace C for writing system tools. You never know.

  11. who says:

    Personally, coming from C# and Python, Zig has taught me how to deal with memory without the fear of shooting myself in the foot with C or spending months learning Rust. I didn’t like some of the syntax initially, like the while/for or the @functions but with time it became whatever, differences without distinction.

  12. Kostya says:

    That sounds like a good feature of the language (even if it does not work for everybody). As for the language syntax, the usual Ford quote about faster horses applies.