I think Zig is hard...but worth it

Page created: 2023-05-22 , updated: 2023-05-31
drawing of a sad rat sitting on the curb while three lizards say…​

"I learned Zig in a weekend! …​Six hours! …​6┬Ás!" say the blissful lizards. Followed by something to the effect of, "It’s easy to pick up because the syntax is so simple."

That’s a sentiment I’ve seen thrown around a lot since 2016’s announcement, Introduction to the Zig Programming Language (andrewkelly.me).

Clearly, many folks really do find Zig easy to learn. Veteran C programmers, in particular, seem to find Zig a natural and logical next step. Kelley’s fun and enlightening talk, The Road to Zig 1.0 (youtube.com), presents Zig as exactly this: "C, but with the problems fixed."

And it’s also true that Zig has a relatively small amount of syntax. For some, it is apparently possible to "pick it up" in the span of time it takes to read a single page of documentation (ziglang.org). (Mind you, that "single page" is on the order of 256 printed pages.)

The problem, of course, is that there’s not a direct correlation between the slimness of a language’s syntax and ease of learning. If there were, the simplicity of Lisp’s S-expressions would make it trivial to "pick up" during lunch. Or Forth, with it’s space-separated words would take, well, you just learned it. Enjoy! Of course, I’m being a bit silly here. Zig is a relatively "small" and "simple" language and that does aid learning. (By contrast, nobody but a savant is going to "pick up" C++ or Rust from scratch in an afternoon. They’re just too big for that.)

But I’d like to state for the record that if you find Zig difficult to master, you are not alone. If your programming background is like mine (or even if it’s less weird than mine), Zig can be challenging.

I’d like to enumerate the reasons I believe this is, and why most of the reasons are actually very wonderful things about the language and well worth the effort to overcome.

Zig is new

Let’s get this out of the way: You can’t just go into the bookstore and buy a Zig book. And even if you could, it would be out of date in a month. Zig is changing rapidly. (If I didn’t have help, I doubt I would have been able to keep Ziglings (github.com) up to date on my own when I was busy with other projects!)

Crucially, there is basically no documentation for the standard library except for the source code itself. (To be fair, some parts are well commented and much of it is surprisingly readable.) Everything else is scattered across the Web, but you’ll have to see for yourself if the examples still compile.

Why that’s good: It’s not. It may be exciting, but it’s terrible for learning. Let’s move on.

Zig forces you to make choices

Depending on your background, you might also not be used to having to think in terms of exact numeric types (u8, i16, f64, etc.) for every single runtime value in your program. Handling "strings" means dealing with pointers, sentinel termination, arrays, and slices. This will slow you down very quickly if you’re used to dynamic languages which (very conveniently) handle these details for you.

If you’re new to manual memory management, you already have a pretty big hurdle to get over.

That’s true regardless of language.

But Zig throws another level of decision-making at you that (most?) developers are rarely asked to make: Choosing a memory allocation strategy.

And you won’t get far without making a decision. A significant portion of the Zig standard library requires that you provide an allocator.

That’s a heck of a thing to ask of a beginner!

Why that’s good: This is actually one of the coolest things about Zig. To write performant software (or even just as a learning exercise), we should learn about obtaining and using memory. We should be allowed to pick the best types and allocators for our application.

(It’s also perfectly fine to pick something like i64 for numbers and the GeneralPurposeAllocator to get you going. You can always change strategies later.)

Zig is pedantic

Something that makes Zig harder to learn up front, but easier in the long run is lack of undefined behavior. While C presents itself as a compact language, learning how to avoid undefined behavior takes time to master because the language itself is more than happy to let you do all sorts of incorrect things. Zig makes undefined behavior an error.

I’ve been a Rust noob and a Zig noob. The thing they have in common is fighting with the compiler because I know what I want to do, but I don’t know how to express it.

Zig’s type system is logical enough, but it still takes time to learn how to create (and especially) cast types correctly because there are so many possible combinations of the basic building-blocks:

  • var vs const

  • optional values (?)

  • error unions (!)

  • single vs many-item pointers (* and [*])

  • slices of arrays ([] and [x..y])

  • sentinel termination ([n:null])

Put enough of these together and you can end up with something like [*:null]const ?[*:0]const u8 (a real example I personally struggled with early on).

There are certain bits of code I could have written faster in assembly language than Zig because it took me a long time to figure out how to express my intent.

Why that’s good: Zig is trying to help. C and assemblers don’t much care what I do with my memory, so they make it "easy" to write code that performs actions regardless of type. But if I get it wrong (and I will), the program will segfault. The language won’t get in my way, but it won’t help me either. Zig makes you get it right, and that’s a good thing. Slower and more tedious, especially at first, but good.

Zig has comptime

Plenty of languages have metaprogramming, but none are exactly alike. C’s preprocessor won’t prepare you for Zig’s compile time execution. Macros won’t prepare you for it. The run-time introspection of various dynamic languages won’t prepare you for it either.

Zig’s comptime is its own thing. And you’ll need to learn about it or you’ll run into errors early on that otherwise won’t make sense because it’s happening whether you ask for it or not.

Within a specific set of rules (including the explicit use of the comptime keyword), "regular" Zig code will run at compile time, resulting in a runtime executable with values pre-calculated, unneeded code removed, loops "unrolled", and code generated inline to work with different data types.

Why that’s good: Only time will tell if comptime is the "greatest thing since sliced bread" or not. But so far, it seems to be a pretty solid concept. By executing portions of the program at compile time, a large number of tricky language problems have been solved without introducing too many additional concepts.

What is "hard" versus "easy", anyway?

The Zig Programming Language (ziglang.org) home page doesn’t contain the word "easy", but it does contain the word "simple".

As Rich Hickey teaches us in his brilliant Simple Made Easy (youtube.com), easy things seem easy primarily because they are familiar. Easy is subjective. But simple things are simple because they do not complicate; they have fewer concepts. Simple is objective.

(Simple is often elegant as well, but then we’re back to being subjective.)

Zig may be easy for some and not for others (like me!), but it definitely strives to be simple, uniform, and correct. And for that reason, I think it’s worth the investment.

Back to my Zig pages.