Structural Typing / Duck Typing Question

I have a little example here where I’d like to take advantage of structural typing or duck-typing;

const std = @import("std");

test "JSON parsing" {
    const basic =
        \\{"foo":"bar"}
    ;
    const out = try std.json.parseFromSlice(
        struct { foo: []const u8 },
        alloc,
        basic,
        .{}
    );
    defer out.deinit();
    try std.testing.expectEqualDeep(.{ .foo = "bar" }, out.value);
}

This won’t compile because out.value and .{ .foo = “bar” } are different anonymous structs despite being structurally the same.

/Users/johndevries/.local/quickzilver/lib/std/testing.zig:737:15: error: incompatible types: 'langlearn.test.JSON parsing__struct_30574' and 'langlearn.test.JSON parsing__struct_30512'
    const T = @TypeOf(expected, actual);
              ^~~~~~~~~~~~~~~~~~~~~~~~~
/Users/johndevries/.local/quickzilver/lib/std/testing.zig:737:23: note: type 'langlearn.test.JSON parsing__struct_30574' here
    const T = @TypeOf(expected, actual);
                      ^~~~~~~~
/Users/johndevries/.local/quickzilver/lib/std/testing.zig:737:33: note: type 'langlearn.test.JSON parsing__struct_30512' here
    const T = @TypeOf(expected, actual);
                                ^~~~~~
src/langlearn.zig:27:36: note: called inline here
    try std.testing.expectEqualDeep(.{ .foo = "bar" }, out.value);
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    test_0: src/root.zig:13:17

I get that I can fix this by naming the struct used in both places as follows;

test "JSON parsing" {
    const basic =
        \\{"foo":"bar"}
    ;
    const Basic = struct { foo: []const u8 };
    const out = try std.json.parseFromSlice(
        Basic,
        alloc,
        basic,
        .{}
    );
    defer out.deinit();
    try std.testing.expectEqualDeep(Basic{ .foo = "bar" }, out.value);
}

But I wonder whether there’s a reason I can’t get this to work in the duck-typing / structural-typing style.

Because duck typing isn’t structural typing :slight_smile:

Zig struct types (not you, tuple) are nominal: whether two are the same type is not decided based on shape, it’s based on declaration. Because of comptime, the exact meaning of that gets complex, but those details aren’t important here.

So there’s no structural typing to take advantage of. Why does it frequently look like there is? Result locations.

If Zig knows what type to expect, you can just use .{ .foo = "bar" } syntax, and that certainly looks like there’s some sort of structural cast from an anonymous struct going on.

In fact, there used to be! It caused problems and was removed, because it turns out that the syntax was usually still available (because of result locations), and that was the nice part about anonymous structs.

expectEqualDeep doesn’t get a result location, it uses anytype, and that’s the real-deal duck typing in Zig. It would be possible, actually, for expectEqualDeep to do a structural comparison: but, Zig isn’t structurally typed, so it shouldn’t do that. If two structs are of different types, they are not equal, that’s what’s consistent with the rest of the language.

Worth taking a look at the source for expectEqualDeep, to see exactly what happens. See the @TypeOf call with two arguments? Same one as the stack trace.

That tries to find a common type for both values, using the coercion rules, and if it can’t, you get the compile error which you were seeing.

3 Likes

In Zig, only tuples have structural typing, all other families of types have nominal typing. Zig does have some rules for type resolution and coercion rules, but these mostly apply to builtin types and pointers.

If you want to achieve what you have in the snippet, you’ll need to create your own equal function, which would be a fun exercise to deal with Zig’s comptime meta programming.

3 Likes