Anon structs, @typeInfo, @TypeOf, @field ... the "right way"?

Is there a better/cleaner way to do this?

pub fn tar(tree: anytype) void {
   inline for (std.meta.fields(@TypeOf(tree))) |element| {
      if (@typeInfo(@TypeOf(@field(tree, element.name))) == .@"struct") {
         std.debug.print("{s} is a nested anon struct\n", .{element.name});
      }
      else { // granted, this could be much more discerning, but, for the example, let it be
         std.debug.print("{s} is a value: {s}\n", .{element.name, @field(tree, element.name)});
      }
   }
}

test "tar" {
   tar(.{
      .a = "ay",
      .b = "bee",
      .c = .{
         .d = "dee",
      },
   });
}

This “works” (output below), but is it “right”? “Ugly”?

a is a value: ay
b is a value: bee
c is a nested anon struct
1 Like

Seems fine to me

1 Like

There is @FieldType(T, "name") which would be slightly more optimal, if you got the @TypeOf(tree) before the loop, as it would do the field access and get the type in one go. But I’m not sure if it would actually be faster, you probably wouldn’t be able to tell unless you had a lot of fields.

It mostly exists for when you have a type, but not an instance that is required by @field, the equivalent would be @TypeOf(@field(@as(T, undefined), "name")), that is not only verbose, but it is 3 operations, where @FieldType is one.

But, ofc, you have an instance already, but not a type, so there isn’t much difference.

1 Like

Not clear if it’s important to your use case, but I want to point out that this just finds struct fields, not “anonymous” ones.

Zig used to have anonymous struct types, and it no longer does. Never mind what they were, the point is that there’s no formal distinction between defining a struct type inline, and defining it with a name and referencing that name.

So it isn’t a quality you can detect. If you’re exclusively parsing a literal dot-syntax Zon-like value, this doesn’t matter, if not, it might.

Oh!? This surprises me, and I’d like to discover more. 0.16 is my reference here. So, in the above example, my .{ .a = “ay”… does qualify as anonymous, doesn’t it, as it refers to no explicit type definition for the struct? If, on the other hand, you’re referring to tuples, a little testing I did suggested that, indeed (and a little surprising), element.name DID give the name “0” (and then “1”, and so on)… I can’t remember if @field(tree, “0”) subsequently gave the field value, but I think it did. I’ll be doing more testing on this later….

If a struct type is defined in such a manner that it isn’t explicitly given a name, Zig generates one. There is no formal distinction between that struct type and any other.

Technically you could get the type name and heuristically search for evidence that this has happened. Please don’t.

Tuples aren’t structs :slight_smile:

Thank you, yes, that’s the kind of insight/extension I’m after. I made an update that const-assigns to eliminate duplicate calls and make everything cleaner; I just left all that in the first iteration to over-visualize all the @calling.

Gotcha, so… I may have missed your original point then, about this not working for “anonymous ones”.

Right, I should’ve been more careful. I just wasn’t sure if, by “I want to point out that this just finds struct fields, not “anonymous” ones,” you meant “anonymous structs” or “anonymous fields”; the latter I would have assumed to mean: in reference to a tuple. But, I wonder if the standard doc should be modified a little; it introduces tuples as:

Anonymous structs can be created without specifying field names, and are referred to as “tuples”.

And yet, reflecting on earlier threads, including your seminal one on tuples and structs, I agree, and appreciate the distinction between structs and tuples.

This turned up some more interesting detail…

My working code is more than the trim I posted to start this thread - it actually recurses into embedded structs. BUT I actually have some tuples embedded in my test tree, right along with the named-field structs. A little to my surprise, everything “just worked”. So I looked and noticed that std/builtin.zig’s Type union does NOT have a field for tuple. Indeed, the tuples that were encountered in my tree were “just structs” as far as this branching cared, and recursion worked just fine with them. I guess a question that might emerge would be: should I expect this to fail, once tuples are cleanly distinguished from structs? I’d be fine with that, actually (as long as there was a tuple field in the builtin.zig Type union), but I’m curious to know something about the likelihood. I guess, “aren’t we all?!” is one acceptable answer. :slight_smile:

Ok, to push the pawn, here’s a ridiculous nascent protohack implementing a specific use case. I’ll tell you why if you make it to the end.

pub const T = enum { html, head, title, body, div, span, }; // ...

pub fn render(tree: anytype) void {
   var close = false;
   // tag:
   if (@hasField(@TypeOf(tree), "tag")) {
      std.debug.print("<" ++ @tagName(@field(tree, "tag")), .{});
      close = true;
   }
   // attrs:
   if (@hasField(@TypeOf(tree), "attrs")) {
      const attrs = @field(tree, "attrs");
      if (@typeInfo(@TypeOf(attrs)) != .@"struct") unreachable;
      inline for (std.meta.fields(@TypeOf(attrs))) |attr| {
         std.debug.print(" {s} = {s}", .{attr.name, @field(attrs, attr.name)});
      }
   }
   // alternately, embedded trees:
   inline for (std.meta.fields(@TypeOf(tree))) |field| {
      const tup: ?usize = std.fmt.parseInt(usize, field.name, 10) catch null;
      if (tup != null) {
         const content = @field(tree, field.name);
         if (@typeInfo(@TypeOf(content)) == .@"struct") { // incl. tuples!
            render(content);
         }
      }
   }
   // content:
   if (@hasField(@TypeOf(tree), "content")) {
      std.debug.print(">\n", .{});
      const content = @field(tree, "content");
      if (@typeInfo(@TypeOf(content)) == .@"struct") { // incl. tuples!
         render(content);
      } else { // TODO: validate that it's a []const u8
         std.debug.print(content ++ "\n", .{});
      }
      std.debug.print("</" ++ @tagName(@field(tree, "tag")) ++ ">\n", .{});
   } else if (close) {
      std.debug.print(" />", .{});
   }
}

With goofy debug-prints, it spits HTML (without any containment validation, etc.) given something like this:

test "html" {
   const doc = .{ .tag = T.html, .content = .{ .{ .tag =
      T.head, .content = .{ .{ .tag =
         T.title, .content =
            "A bitty HTML sample"
         } },
      }, .{ .tag =
      T.body, .content = .{ .{ .tag =
         T.div, .attrs = .{ .id = "crack", .class = "warnings", }, .content =
            "text in first div"
         }, .{ .tag =
         T.div, .content =
            "text in second div"
         }, .{ .tag =
         T.div, .content = .{ .{ .tag =
            T.span, .content =
               "text in span"
            } },
         } },
      } },
   };
   render(doc);
}

Why? I think this is pretty useless (to me) by itself. But ages ago I decided I didn’t like templating languages (or the motif at all, really), and I stumbled upon dominate, which was much to my liking, and for years I wrote python whose “template” layer was just python code. This is most valuable for the dynamic cases (building the structure from some data, e.g.), and, in my case, 90% of the HTML was NOT boilerplate, so using a real language was a joy for me - far superior to making for-loops in a template language. But, of course there’s always some static stuff, too, and it was nice that dominate made it easy to do, often in-line, to make things pretty readable.

In my opinion, if I were to take on this pet project idea, dominate could be an inspiration, but the end result would probably not bear as much resemblance as anticipated, due to language differences. But who knows, maybe a creative turn will lead to something even nicer.

Anyway, the hack above is just a proof-of-concept for the static case. I’m more interested in working on the dynamic tree building, but I wanted to be sure I could do something like this to scaffold the static/boilerplate stuff.

Why the message? As usual, I’d love critique, in the form of: “what an awful/awesome idea”, and in the form of: “Jonny already did that, look here”, and in the form of: “you’re doing it wrong - do this instead”. Thanks.

The point is your code does not operate on anonymous structs, rather just normal structs, because they are the same thing in the type system.

There is no practical way to distinguish them. You could guess based on the name, but nothing is stopping someone from giving an anonymous looking name to a non-anonymous struct.

There are no plans (AFAIK) to make tuples their own class of types. But if that did happen, then it would break your code, though it would be easy to fix.

FYI tuples do have some different properties than normal structs in the type system, so the compiler does need to differentiate them which it does with the is_tuple field on the struct info. No need to check if the field names are numbers, which a normal struct can have btw struct { @"1": bool }

Tuples special properties are:

  • their type is determined by their shape, not name. Meaning identical tuple definitions will be the same type, this is not the case for non tuple structs
  • tuples can not have any declarations
  • they can be destructured const a, const b = .{1, 2}, you can create new const/var, or assign to existing variable(s) by omitting the const/var
  • they can be indexed, and even coerce to arrays if all their fields are the same type.
3 Likes

Ah! Awesome. That parseInt was stupid; this is much better.