home

One method declaration; two Zig annoyances

Nov 20, 2024

In the following code, we create a Post structure with a skeleton format method. Despite being pretty comfortable with Zig, I could stare at this code for hours and not realize that it has two issues.

pub fn main() !void {
}

const Post = struct {
    raw: []const u8,

    pub fn format(self: Post, format: Format) void {
        _ = self;
        _ = format;
        // TODO;
    }
};

const Format = enum {
    html,
};

The first is relatively superficial. If we try to run the above, we'll get a compiler error:

blog.zig:7:31: error: function parameter shadows declaration of 'format'
    pub fn format(self: Post, format: Format) void {
                              ^~~~~~
blog.zig:7:9: note: declared here
    pub fn format(self: Post, format: Format) void {
    ~~~~^~

I've moaned about this before, but Zig is strict about disallowing shadow declaration. Here we're seeing the error because a function and one of its parameters have the same name. Because every Zig file is an implicit structure, you'll run into this error pretty often (at least I do). For example, if you const socket = @import("socket.zig"), you can forget about using the socket identifier throughout your file.

Like most automatically enforced stylistic errors, we can solve this by making the code less readable

// rename the format parameter to fmt
pub fn format(self: Post, fmt: Format) void {
    _ = self;
    _ = fmt;
    // TODO;
}

Our Post struct is now valid; our code compiles and runs. Of course, we're not doing anything yet, so let's make a small addition:

const std = @import("std");
pub fn main() !void {
    const post = Post{.raw = "## Hello"};
    std.debug.print("{s}\n", .{post.raw});
}

This outputs "## Hello", but we're really close to a more subtle compiler error. If we change the last line to print our post (using the {any} format specifier):

const std = @import("std");
pub fn main() !void {
    const post = Post{.raw = "## Hello"};
    std.debug.print("{any}\n", .{post});
}

We get a compiler error:

/opt/zig/lib/std/fmt.zig:506:25: error: member function expected 1 argument(s), found 3
        return try value.format(actual_fmt, options, writer);
                   ~~~~~^~~~~~~
blog.zig:10:9: note: function declared here
    pub fn format(self: Post, fmt: Format) void {
    ~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    format__anon_5125: /opt/zig/lib/std/fmt.zig:188:23
    print__anon_2772: /opt/zig/lib/std/io/Writer.zig:24:26
    6 reference(s) hidden; use '-freference-trace=8' to see all references

In a previous blog post we purposefully defined a format method to control how our structure is formatted. In another post, we learned about std.meta.hasMethod, which is how Zig's fmt package detects the presence of the format method.

The problem is that the check is dumb:

if (std.meta.hasMethod(T, "format")) {
    return try value.format(actual_fmt, options, writer);
}

It doesn't check the number or type of parameters. I particularly dislike this issue because it won't show up until someone tries to print the structure. When you're writing a library, you might never std.debug.print a Post, but a user of your library might - especially if it's just a nested field of some other structure they're trying to look at.

The good news is that, as far as I can tell, Zig only "reserves" the format, jsonStringify and jsonParse method names. But it would be nice if it could be made more explicit.