One method declaration; two Zig annoyances
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 {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.