/skytwosea /writings /zig_namespaces.html

Printing, Namespaces, and Hello World

I recently found myself wondering: why does it seem like such a hassle to print things to stdout? Why no @print builtin? What is the difference between the relatively accessible std.debug.print() function, and the more cumbersome std.io.getStdOut().writer().print() function? Why do it be like the way it are?

Up to this point, I’ve stubbornly stuck with std.debug.print mostly because I’ve had trouble keeping the writer.print() pattern in my memory for future use, and when I have trouble keeping things in mind, it’s usually because I don’t understand the underlying principles. Time to dig in.


STD{IN,OUT,ERR} Are Files

On Linux systems, there are three fundamental abstractions that we call STDIN, STDOUT, and STDERR. These are assigned the file handles 0, 1, and 2 respectively. The std.fs.File type has a single field, handle, which allows us to see this integer assignment for ourselves via the following:

const std = @import("std");
      const File = std.fs.File;

      pub fn main() void {
          const in: File = std.io.getStdIn();
          std.debug.print("stdin handle: {}\n", .{in.handle});

          const out: File = std.io.getStdOut();
          std.debug.print("stdout handle: {}\n", .{out.handle});

          const err: File = std.io.getStdErr();
          std.debug.print("stderr handle: {}\n", .{err.handle});
      }

which prints:

$ zig run handles.zig
      stdin handle: 0
      stdout handle: 1
      stderr handle: 2

The point of all this is that when printing to the terminal, we are really writing to a file, in the sense of Linux’ everything-is-a-file abstraction. To write something to the terminal, we need to go get the file object that represents the terminal’s output. Most of the time, that file is STDOUT.


The Writer Abstraction

From the horse’s mouth: Zig’s std.io.Writer and std.io.Reader provide standard ways of making use of IO. The std.fs.File type follows suit: it has a writer function which returns a Writer type. The function signature is as follows:

pub fn writer(file: File) Writer

If you navigate to the standard library docs’ std.io page, you’ll see a whole bunch of different types defined at the top: among the mix are AnyWriter, BufferedWriter, GenericWriter, NullWriter, and at the end, Writer. Clicking on Writer takes us to the GenericWriter type function doc page, a minor discordance - an aliasing - we’re going to avoid for a moment.

On this page, we see ten functions: - any - print - write - writeAll - writeByte - writeByteNTimes - writeBytesNTimes - writeInt - writeStruct - writeStructEndian

Finally, we see print. Except it looks a bit wild:

pub inline fn print(self: Self, comptime format: []const u8, args: anytype) Error!void

What’s going on?


Printing, Almost, Finally

Let’s see if we can unpack this. First, recall that we’re working with a Writer object that was derived from a File object. That is what self refers to: the ‘parent’ object of the print() function, if you will. Next, comptime format: that is the array of bytes that we want to print, plus formatting, should we choose to include any. Last, is args, which are the things that get stuffed into the string when the print function is called to do its work.

Let’s put this thing to work. A minimal program looks like this:

const std = @import("std");
      const Writer = std.io.Writer;

      pub fn main() !void {
          const out: Writer = std.io.getStdOut().writer();
          try out.print("hello writer!", .{});
      }

which produces the following output:

hello_writer.zig:64:14: error: expected type 'type', found 'fn (comptime type, comptime type, comptime anytype) type'
          var out: Writer = std.io.getStdOut().writer();
      ...
Damnit. What is it now… Recall the earlier emphasis on how our Writer object is derived from a File object. Then, note in the above snippet that the Writer is declared from std.*io*.Writer. Turns out, writers defined in the io namespace are interfaces, that return a type, and we need to reference the more constrained std.fs.File.Writer in order to properly declare the type of out. I picked up this bit of trivia from this StackOverflow answer. There are four solutions presented in the SO answer:

NB: I don’t fully understand interfaces and generics; they’re on my to-learn list. For this article, I’m shooting from the hip with those two terms, meaning there’s a good chance I’m not using them correctly. Please see Karl Seguin’s blog openmymind.net for several articles that clarify these things much better than I can!

The first and last options describe how to correctly constrain and declare the type of const out for writing to STDOUT, when the Writer type is declared from the std.io namespace. The second and third items describe how to correctly declare the type of const out when the Writer type is declared from the std.fs namespace.

With that, we’re finally ready to do a print:

const std = @import("std");
      const Writer = std.fs.File.Writer;

      pub fn main() !void {
          const out: Writer = std.io.getStdOut().writer();
          try out.print("hello writer!", .{});
      }

which produces the following output:

hello writer!

Wonderful! Not bad at all.


Further Down the Rabbithole, But Not all That Far Really, Considering Just How Deep The Rabbithole Appears to Go

All that is making good sense: io.Writer is Zig’s interface for writing to files, and we want to use a writer that is appropriately constrained to the type we’re working with (e.g., std.fs.File). But… is there a way we can just kinda ignore all that? Well, sort of. There are a few options, each with its own catch.

Option one is probably the most reasonable option: return to old habits and use std.debug.print. The catch here is that it writes only to STDERR, which we can see from peeking at its source code:

pub fn print(comptime fmt: []const u8, args: anytype) void {
          lockStdErr();
          defer unlockStdErr();
          const stderr = io.getStdErr().writer();
          nosuspend stderr.print(fmt, args) catch return;
      }

We can even import it like so, to avoid the std.debug part:

const print = @import("std").debug.print;

Option two is to make our own print() function, with blackjack, and hoo- I mean, with STDOUT, and error handling avoidance. We could try this:

const std = @import("std");
      const Writer = std.fs.File.Writer;

      pub fn main() void {
          print("hello print fn with {} args!\n", .{1});
      }

      fn print(comptime fmt: []const u8, args: anytype) void {
          const stdout: Writer = std.io.getStdOut().writer();
          stdout.print(fmt, args) catch return;
      }

or even this:

fn print(comptime fmt: []const u8) void {
          const stdout: Writer = std.io.getStdOut().writer();
          stdout.print(fmt, .{}) catch return;
      }

The catch of the first print is that we aren’t able to lock the target file: there is no equivalent lockStdOut() function in std.Progress. Sure, you could start messing around with Zig’s mutexes and come up with something fancy, but that is out of my league. For playing around in single-threaded sandboxes, this is fine.

The catch of the second one is that we can’t use formatting: we’re force-feeding an empty args struct to the real print call, and constrain ourselves to hardcoded u8 arrays only.

Both of these tricks miss the point, though. The real lesson I’m taking away from all this is that sure, we can faff about, trying to make Zig look more like Python or behave more like C or Go or whatever. We can fuss and kick the dust and rail against design decisions that feel like they break our own safe, well-entrenched mental models of what a ‘good’ decision would have been in this case or that case. This is a pernicious trapping that I attribute to the less well structured self-taught education path. Instead, a better goal is to take Zig as it is, and work to break out of our limiting expectations for what the language should look like, how it should behave.

Closing Thoughts

If you’ve made it this far, thanks for taking the time! This is my inaugural blog post, and I’m looking forward to learning more and writing more.

I cut my teeth with Python, quite late relative to many folk: I was in my mid-thirties before I even took an interest in computers. I’ve been using Zig to explore the world of lower level software development, and while it has been a bit torturous - the docs can feel a bit spartan in places, for example - it has also been incredibly illuminating. This discussion of print semantics is just one of many cases where I’ve tried to bring my Python habits along for the ride, and where I’ve railed against a design without thinking at all about why the decision was made the way it was.

Go forth and stdout.write() with confidence!