Conway with the Zig Programming Language

As suggested in a previous article, this year’s candidate of my lifetime programming language learning activity is Zig, and I decided to reimplement Conway with it.

Yes, Zig, apparently the great nemesis of Rust.

I wrote this version of Conway when I was still using Ubuntu, where Zig is only available as a snap package; but since I had uninstalled Snap completely (because seriously) I just downloaded the release archive and decompressed it into ~/.local/bin/zig, adding it to my PATH. Very simple to install, and no Snap thankyousomuch.

In Fedora 38, thankfully, a sudo dnf install zig will give you version 0.9.1 (at the time of this writing.)

For this exercise I used version 0.10.1 of Zig, installed manually. This is what the source code looks like:

const std = @import("std");
const Coord = @import("coord.zig").Coord;
const World = @import("world.zig").World;

var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();

fn clrscr() void {
    std.debug.print("\x1B[2J\x1B[0;0H", .{});
}

pub fn main() !void {
    var alive = try World.blinker(Coord{ .x = 0, .y = 1 }, &allocator);
	defer alive.deinit();

    var world = try World.create(30, alive, &allocator);
    var generation: u16 = 0;

    const stdout_file = std.io.getStdOut().writer();
    var bw = std.io.bufferedWriter(stdout_file);
    const stdout = bw.writer();

    while (true) {
        clrscr();
        generation += 1;
        try world.format(stdout);
        try bw.flush();
        std.debug.print("Generation {d}", .{generation});
        std.os.nanosleep(0, 500000000);
        const new_world = try World.evolve(world);
        world.deinit();
        world = new_world;
    }
    world.deinit();
}

Modern

The first thing that strikes about Zig is that it is indeed a very modern programming language:

  • Built on top of LLVM.
  • There is a zig command, used to create projects, build them, test them, etc. It is a full build system, which means there’s no need for cmake or other similar tools.
  • Error management is not done by throwing exceptions (there isn’t such a thing) but rather using the defer and errdefer keywords. This is similar to Swift or Go, and Zig even has a matching try keyword. This all means that errors cannot be ignored.
  • Unit tests embedded with your code, just like Go and D do. They can report memory leaks at execution, which is a very good idea; this is like having Valgrind built-in. Excellent idea.
  • Short compilation times.
  • Full family of memory allocators, for various uses, which makes the language very suitable for embedded programming.
  • Type inference.
  • Generics, implemented with functions returning type values.
  • Optional types, unwrapped with the orelse keyword.
  • No macros, no preprocessor, no hidden control flows such as properties that are actually functions, like in D or C#.
  • Runtime or compile-time safety mechanisms can be disabled for faster compilation and execution, if needed.
  • Compiles code into small, tight final binaries.
  • It features compile-time reflection.
  • Fully interoperable with C… to the point that the Zig compiler can even compile C!
  • Cross-compilation available off-the-box for a vast array of architectures, ABIs, and stdlib implementations like musl, which would make Zig a great option for wrapping executables in container images based on Alpine, for example.
  • Asynchronous programming with the usual async and await keywords.
  • if and switch statements also work as expressions.
  • enum can have methods.
  • const correctness similar to C++, which means that a const AutoHashMap does not allow its non-const put() method to be called, and so on.
  • It uses similar string formatting parameters to those of Rust.
  • There is a VSCode extension and language server for Zig.

And so much more. The language really feels at home in 2023.

Rough Edges

There are a few things that are, in the humble opinion of this author, a bit of a disappointment.

  • Zig has no string type; it just uses arrays of u8, which some find cuestionable.
    • Maybe this is the reason why I couldn’t find any web API frameworks for Zig. After all, web programming is esentially all about string handling, and this is a point where Rust has a major advantage over Zig, even though strings in Rust are far from being a walk in the park.
    • Zig is not meant for using the heap as a primary storage area, which means that string formatting is very similar to C: instead of returning strings on the heap to callers, one passes an allocated formatter into the function returning void; one does not return heap-allocated data. This characteristic makes Zig very appropriate for embedded devices and kernel development, where heap allocation is not possible or desired.
  • I found compiler errors to be not as readable or understandable at first sight, like those generated by the Rust compiler.
  • The current version of the VSCode extension at the time of this writing does not offer good code completion or type hints, which hindered a bit the experience of discovering the language.
  • The documentation is very much alpha; after all, the project hasn’t reached 1.0 yet. Hopefully this situation will improve over time.
  • The whole idea of allocators, arguably one of the most fundamental and interesting features of the language, is not very well explained in the documentation.

If you want to learn Zig, here are some links for you:

Overall Impression

Zig is a very nice, approachable, and powerful programming language, delivering on its promise of being “a better C than C”. However, I believe Rust is a bit higher-level in the abstraction ladder, and maybe more suitable for situations like web applications, thanks to its higher-level libraries and string manipulation abilities.

In any case, Zig really deserves better documentation and its VSCode extension needs some love. At this point in time, the project appears young and full of promise.