Notes on the Zig programming language
Sun Feb 04 2024E.W.Ayers
Created at: 2024-02-04 Last edited at: 2025-02-02
I had the chance to work on some Zig code over the weekends:
I forked zigup
I had a go at using Mach, but gave up.
I used raylib to render maps from this dataset. Raylib is very good.
I wrote some variants of A* plus visualisations on these maps.
Overall it's been a pleasant experience. It's much easier to work with than C and C++. I know that I should really just learn Rust instead but I'm just not excited by it. Zig really scratches an itch. The previous choices for system programming languages are:
C and C++ -- absolutely riddled with footguns and terrible choices
Go -- it looks cool but also some ugly choices like errors are returned as a tuple. Also it's GCed so I might as well use javascript.
Rust -- makes you be an abstraction astronaut. The borrow checking stuff is a waste of time. Macros are super ugly.
Odin -- looks ok but hasn't hit critical mass of programmers
Zig is just C with
stupid footguns removed,
obvious QoL features that you would expect from any modern language,
and the language feature of the century:
comptime
.
Comptime is massive. I get giddy thinking about it. It removes so much abstraction trash that you have to put up with in other languages:
macros
code-gen
preparing assets
generics, and bumping against limitations of type systems
overcomplicated type systems
I'm not going to explain it here, have a look at the links in the next section to find out more.
1. Links about Zig
I'll keep updating this section.
Karl Seguin -- Learning Zig + many other blog posts about Zig. Imo this is the best technical writing about Zig.
2. Things that are annoying but are deliberate choices
No operator overloading. This is insufferable for geometry and linear algebra. You can use the builtin operators with
@Vector
, but no broadcasting except with clunky@splat()
builtin. Vectors are a pain to use with casting.Casting between different numeric types is verbose
Most of my error handling is to handle
OutOfMemory
errors. If there was special language support for allocators then 90% of mytry
s would go away and the code would be much less cluttered.Having to deal with integer cast issues gets old. The fact that Zig is littered
@intCast()
calls is because integer casts in C are traumatic. To understand why Zig was designed this way we have to look at how integer casts work in C: it's completely mental. I found this article really useful for understanding why it is done this way.Having to pass allocators around everywhere gets old. I prefer the Jai approach where there is a
context
keyword containing an allocator.The type hinting doesn't really work for some generics like Reader.
If you want your type to be formatted in a certain way, you need to implement this method called
format
which has a really long type signature.
This github issue is interesting becuase it's so divisive: compile errors for unused things.
It's a 1:2
ratio upvotes to downvotes on having unused vars be a compiler error.
I'm on team Kelley for this particular decision, but this issue tells me is this is Kelley's project.
Leading an OSS project is a tough job.
It makes me worry about forks and ecosystem fragmentation.
3. Things that are annoying because they are not mature
Hidden pass-by-reference unsoundness is unforgivable. Values are values and pointers are pointers. The optimiser should obviously not be allowed convert values to a reference if the value is being mutated.
The LSP isn't good enough yet.
Something has gone wrong in my VSCode setup which means that it doesn't show diagnostics on save.
There needs to be an official zig version manager. It's kind of cute just downloading zig as a file but it becomes old setting PATH variables all the time. You also need the correct
zls
to be on the path, so now you need to manage the version of two things and it's too much fuss. see alsoReader
vsAnyReader
vsGenericReader
is confusing and not clearly documented. This post helps: In Zig, What's a Writer?no coroutines. Having to explicitly write state for iterators is annoying. This is on a roadmap. I am excited to try out the Zig approach.
the type inference is not great, and I found myself having to tediously write down types where it shouldn't really need them. Iirc comptime means it's theoretically undecidable to do type checking, but 99% of the time it should be able to do it with decent heuristics.
Maybe I missed it but the capture in a for loop is always
usize
?The debugger works, but it's a C++ debugger so it's rough around the edges. Here is a third-party zig debugger project that looks promising: μscope.
4. Things that are amazing
Not having to worry about linkers and header files and cmake and ninja and cross-platform compiles is amazing and I love it. It just works.
defer
is so good.comptime
is so good. There is a blog post in me about what type theorists can learn from comptime.I like how down-to-earth it is compared to Rust. Rust wants you to be an architecture astronaut.
5. If I was designing zig
I would include the ZLS project as a core part of Zig.
Version manager
zigup
is built in to zig.Have a
context
system like Jai. Maybe this can be implemented in Zig itself. Something like context variables.I would revert to a more conventional exception handling system, (
try
,catch
,finally
like in C++ or JavaScript), but keep the!
and error sets. The exceptions that can be thrown (ie the error set) are inferred statically (eg how Koka does it). You can hover over a method and see the exceptions that might be thrown. You get compile time errors if there is an unhandled exception kind. You can tag a method as pure meaning it never throws and this is enforced by the compiler. I think the Zig error design falls into the same 'coloured function' trap thatasync/await
falls into in other languages (which is elegantly avoided in Zig).Be way more chill about implicitly casting between the different numeric types. Delete
@intToFloat
,@floatToInt
,@intCast
. 90% of the time it gets in the way. I am ok with runtime detectable-undefined-behaviour. At the very least it should be less cluttered, eg just writeu16(x)
to cast to au16
. Or maybex as u16
. It's particularly annoying because I couldn't figure out how to type thei
infor (0..10) |i| {}
. It seems to beusize
no matter what.Coroutines. We have
resume/suspend
so it's a small step to supporting general coroutines. I think this is on roadmap.Operator overloading should be supported. I would love to be able to define my own operators for types. I guess this could quickly spiral out of control into a rust-style trait system. I wonder if traits could be implemented with comptime.
I want to contribute more to the docs of Zig. There is something about Rust that meant it never caught my excitement, I don't really get it because it has everything I want. Meanwhile I am really excited about Zig despite all the teething problems and issues. I don't get it. Maybe I am over functional programming.