home

Zig Removes Anonymous Struct

Nov 28, 2024

A recently merged pull request removed anonymous struct from Zig. I was surprised by this change - it seemed like a big deal. But it turns out that I didn't understand what an anonymous struct were, and this change isn't quite as big as I thought. Consider this code:

const std = @import("std");

pub fn main() !void {
    const user = .{.id = 2, .name = "Leto"};
    std.debug.print("{any}\n", .{user});
}

The above continues to works, so it seems like, despite the PR's title, anonymous structs still exits. But user isn't an anonymous struct. It's a normal struct that is named and defined by the compiler. If I print its type, by changing the above .{user} to .{@TypeOf(user)}, I get blog.main__struct_2347.

So if this isn't an anonymous struct, what did the PR remove? Let's expand our code a little:

const std = @import("std");

const User = struct {
    id: i64,
    name: []const u8,
};

pub fn main() !void {
    const user = .{.id = 2, .name = "Leto"};
    printUser(user);
}

fn printUser(user: User) void {
    std.debug.print("{d} {s}\n", .{user.id, user.name});
}

If you run the above using a version of Zig before this PR was merged (for example, using Zig 0.13), it'll work. But if you try to run the above using a new version of Zig, you'll get an error:

error: expected type 'blog.User', found 'blog.main__struct_2347'
    printUser(user);

Essentially, anonymous structs could be coerced to an inferred type. Obviously with the removal of anonymous struct, this behavior is no longer valid. I saw this as a neat form of compile-time duck-typing.

I know I'm not the only one confused by this distinction and I didn't find Zig's documentation useful. Internally, it seems like the difference between an ad-hoc generated compile-time struct (which we still have) and an anonymous struct was pretty significant. So removing anonymous struct helped to simplify the code and removed edge cases. But for those of us not working on Zig's compiler, it still feels like anonymous structs exists, it's just that inferred type coercion from an anonymous struct to a concrete struct has been removed.

Do note that we're strictly talking about type coercion. Nothing has changed with respect to type inference. For example, if we make a small change to the above, we can get the code to run again:

pub fn main() !void {
    // change this:
    //   const user = .{.id = 2, .name = "Leto"};
    //   printUser(user);
    //
    // to this:

    printUser(.{.id = 2, .name = "Leto"});
}

Above we're not creating a temporary variable, so we also aren't creating an ad-hoc struct. Rather, we're using the shorthand form to initialize a User (where .{...} is the same as User{...}, where the type is inferred).

Of course, the other way to fix this code would have been to give user an explicit type:

pub fn main() !void {
    const user = User{.id = 2, .name = "Leto"};
    printUser(user);
}

But in my experience, places where anonymous structs were being leveraged involved complex (i.e. generic) types. For example, in httpz, where this was a small issue, the route configuration was a generic type whose details weren't something the library user really cared about. Using an anonymous struct and having it automatically coerced was pretty convenient.