A Review of the Zig Programming Language (using Advent of Code 2021)
I’ve long thought that Zig was an interesting programming language, potentially more interesting than Rust in many respects given that Zig seems to be targetting a more modern C-like language replacement whereas Rust firmly looks like it is trying to take C++ out back like ol’ yeller. Rust is powerful, but the language is complicated, and no I’m not talking about the borrow-checker (a completely genius idea) but the language itself is vast and complex. Try and read a moderately complex Rust crate and it can be mind boggling to work out what is going on.
On the other hand Zig, with a strong ethos guided by Andrew Kelley, has this guiding light that there should be one way to do something in the language, and that is something that I really appreciate in language design.
Last year I attempted to do Advent of Code 2020 in Zig, but the language was just a little too fresh for me to get into. The documentation was basically non-existent, and even getting the tools and working how to use them was too confusing for me. On day one I gave up and switched to Rust instead. This year though I was determined to try the whole challenge in Zig, and what a difference a year has made to the language! The community is now massive, there are GitHub templates for Advent of Code to just get you coding, and the Zig documentation is so rich and detailed that I could pick up some of the basic concepts quite quickly.
So now that I’ve completed Advent of Code 2021, I thought I’d share the good and the bad about Zig, and some summary thoughts on the language.
Note: I’m assuming a base level of understanding about what Zig is here, there are plenty of guides on the language available elsewhere!
The Good⌗
The best thing about Zig is that the language is small. There isn’t even a
foreach for like structure and Andrew has stated ‘While loops work, why add another
way?’ and I really appreciate this approach. It means I am not wondering about
what tool to reach for when I want to do something, there is a single tool with
a single use. Especially when learning a language (for myself and for anyone
else that would want to pick it up) - brevity is key. I think Rust got lost in
trying to nicely provide so much of what C++ badly provides that random users of
the language looking at any arbitrary code written in Rust suffer for the sheer
breadth of the language. Zig’s approach here meant I could read the standard
library code and understand what it was doing (even with all the comptime
type
fun!).
Nullability is fun in Zig - the fact that optionality is built into the language
with the ?
prefix on types (so ?i32
is maybe-a-32-bit-integer) and that they
have combined this with pointers so that you can assume that any pointer (*i32
for instance) isn’t null. This is great for the compiler, great for the
optimizer, and I think also great for the user.
How things are brought in from the standard library or general foreign code is interesting:
const std = @import("std");
const print = std.debug.print;
There is a builtin compiler marco @import
that does the heavy lifting of
pulling in the code, and then you assign this into a const some_var
variable.
This is really neat because you could call that whatever you wanted (to avoid
naming conflicts). Also when you want to pull in definitions from within an
imported package you just use the same mechanism of assigning the
package.with.a.thing.in-it
into a constant variable. Most other languages have
a using foo::bar::haz::baz;
type mechanism for this, but having it use the
same mechanism for a bunch of different things means that you don’t have to
switch in your head to another tool. I hadn’t considered this language concept
before using Zig, and its a very good idea!
The fact all containers take an allocator on intialization, and you can only get a heap pointer via an allocator is genius in Zig. Memory isn’t free, and allocations are not cheap, and so making getting at heap allocations harder by explicitly getting them through an allocator is a great thing.
Also the error mechanism in Zig is wonderful. Zig has this special prefix for a
type (for example !u32
means ‘an error or a u32
’) and you can cascade
errors from deep in Zig code with the try
statement. So var x = try foo();
means x
is equal to the result of foo()
unless there was an error in the
result. If there was an error, return from the function with the error now. This
meant that you don’t have the messy littering of if conditionals after every
function that you typically get in C, but you also don’t have the complete
disaster that is exceptions in C++/C#. Rust has a similar mechanism to this, but
they use the clunkier Result<T, E>
. While Zig has effectively added another
thing for the frontend to handle by adding in a !
prefix on the types, the
language is certainly nicer for it.
The Bad⌗
There are a collection of things in Zig that I didn’t like. All languages have things that any random subset of users won’t like, so I am not saying Zig should change any of these or anything like that.
Initializing arrays is weird in Zig. Lets say you want to have a 0 initialized
array, you declare it like [_]u8{0} ** 4
which means I want an array, of type
u8
, that is initialized to 0
and is 4
elements long. You get used to the
syntax, but it’s not intuitive.
For loops are a bit strange too - you write for (items) |item| {}
, which means
you specify the container before the per-element variable. Mentally I think of
for as for something in many_things {}
and so in Zig I constantly had to write
it wrong and then rewrite. Also you use the |
character in Zig quite a lot,
and while this may just be a problem with Apple UK keyboards, actually getting
to the |
character on my laptop was uncomfortable. When doing C/C++ or Rust,
you use the |
character much less and so the pain of writing the character
was something I never noticed before.
Jonathan Blow has gone on the record to say
that with his language, Jai, he spent a lot of time working out how easy it
would be to type common things, such that the more common an operation in the
language, the easier it would be to type. That seems to be missing here (well at
least for Apple UK keyboard layouts, I’d need to write Zig extensively on
another layout to know whether this was a universal thing!).
Switch statements where you want to have multiple arguments resolve to the same
code I wrote as a | b
, whereas in Zig it is a, b
. Nothing major with this,
but I constantly tripped up on this.
Zig test was a bit clunky - you have to specify the file you want to test. So to
test src/foo.zig
, you’d do zig test src/foo.zig
. I wanted something more
like Rust’s cargo test
that’d find all tests and run them. Maybe Zig does have
this but I just didn’t find it?
And how you declare functions is a little strange. Like a function in a struct would be:
const foo = struct {
pub fn bar() {}
};
Everything in Zig is const x = blah;
, so why are functions not
const bar = function() {};
?
The Ugly⌗
Zig is still a little raw in a few areas. Some compile errors are less than
useful. For instance if you forgot to put !T
on a return type, but were using
try
in the body of the function, the compiler error was very confusing. This
is only really an issue for new Zig users (like I was when I first hit this),
because you quickly learn that when the compiler spits out something less than
useful and you are using try
, check the return type first. Occassionally Zig
would spit out 100’s of lines of notes after an error, giving me flashbacks to
the C++ template mess errors you’d get.
The builtin compiler macros (that start with @
) are a bit confusing. Some of
them have a leading uppercase, others a lowercase, and I never did work out any
pattern to them. Is it @as
or @As
? I still couldn’t tell you without looking
at the manual.
The type system in Zig is loose in some ways and tight in others. If Zig can
detect the type of the right hand side of a variable declaration, you don’t need
an explicit type. But if you had something like var x = 0;
you have to specify
a type. It’d be nice for users (but obviously harder for the compiler team!) if
the compiler would be able to deduce these types too.
But the worst bit about Zig at present is the standard library documentation is broken and non-existent. This is probably the one reason I wouldn’t recommend Zig more generally at present, because I resorted to looking at the source files of the standard library on GitHub to work out what I could do with what provided stuff in the standard library. I know there is a plan that with the new compiler frontend (written in Zig!) to fix this, so its just a time problem.
Conclusion⌗
Overall my gut feeling is that Zig is about ready for developing with for people like myself (coders that don’t mind a bit of pain to a lot of benefit), but it is not quite ready for more general usage. Fixing the standard library documentation would be my biggest priority if I worked on Zig, because I think that is the only thing holding back general usage of the toolchain.
One nugget of knowledge I’ve worked out though - Zig is not a replacement
for C. It is another replacement for C++. comptime
which while amazingly
powerful, already has echoes of the hard to reason about C++ template code or
Rust generic mess, and there are still quite a few bits of syntatic sugar hiding
the real cost of certain operations (like the try
error handling, there is
implicit branches everywhere when you use that).
This isn’t to say Zig is any lesser by being a much better C++ replacement rather than a C replacement in my estimation, infact I’d argue that aslong as Zig doesn’t fall into Rust’s trap of constantly adding yet more ways to do the same damn thing and making the language that little bit harder for new people to onboard with, then Zig once it hits a stable language around 1.0 will be my recommended tool going forward.
I really enjoyed doing Advent of Code in Zig, and I think I’ll be writing more software in Zig going forward. I’d highly recommend you check out the language and the community around the language are a great group of people that have been super helpful with my dumb onboarding questions.