Struct cast to slice

I’m only a little surprised that this works:

const E = enum { a, b, c, };
pub fn farr(arr: []const E) void {
   std.debug.print("arr type: {s}\n", .{ @typeName(@TypeOf(arr)), }); // "slice", basically
}
test "array-or-struct" {
   const a = .{ .a, .b, };
   std.debug.print("a type: {s}\n", .{ @typeName(@TypeOf(a)), }); // "struct", basically
   //farr(a); // obviously can't do this
   farr(&a); // bit of a surprise
}

The advice I’d give to myself here is, “don’t do that. instead:

   const a = [_]E{ .a, .b, };

… if, of course, that’s what you really mean.” But it raises questions about whether this is how it should work – are there cases for… well, what I might call implicit casting from struct to slice? It doesn’t work everywhere; for instance, I can’t ++ that original a with a slice. But is this a “feature” I missed before? Or an accident to stay away from? Or…?

While it uses the struct keyword in the language syntax, Zig actually calls this a Tuple, and tuples do share some behavior with arrays/slices, they have a len field, can be iterated with inline for loops and support ++ and ** (as long the other type matches). So it also makes sense that this coercion exists.

Generally I think the preferred way is const a: [2]E = .{...};, see also this proposal: remove T{} syntax in favor of type coercion · Issue #5038 · ziglang/zig · GitHub

3 Likes

FWIW I’d love to have tuples behave more like arrays and coerce to them more readily, e.g. both this:

const a: u32 = 123;
const b: u32 = 456;
for (.{ a, b }) |i| { _ = i; }

and this:

const a: u32 = 123;
const b: u32 = 456;
for (&.{ a, b }) |i| { _ = i; }

will currently throw:

error: unable to resolve comptime value
for (.{ a, b }) |i| { _ = i; }
     ~^~~~~~~~
note: tuple field index must be comptime-known

instead of just coercing to [2]u32 or *const [2]u32 respectively which would be really nice IMO.

I think homogenous tuples even having the same memory layout as arrays is the de-facto status quo but it’s not (yet?) formalized (though the coercion from tuple to array/slice is already guaranteed to work).

1 Like

I wanted to reference this earlier thread, “Are Tuples Structs?“ by @mnemnion as well, for posterity.

2 Likes

You can actually. The problem with your examples is that they don’t have an array result location, this should work:

const a: u32 = 123;
const b: u32 = 456;
for (@as([2]u32, .{ a, b })) |i| { _ = i; }

I haven’t actually compiled this but, it should work.

Edit: I see that your point is “coerces more readily”, not “doesn’t coerce at all”. I’m not convinced we want more magic in this particular spot though.

1 Like

I like conservatism with magic, too… though I have to admit, I’ve rather adopted the (lesser) magic originating this thread. I’ve basically decided to adopt it intentionally, here. My reasoning is: “well, tuples (used this way, especially) are just as array-ish as they are struct-ish. I guess I honestly don’t see anything wrong with a slightly fuzzy line, at that conceptual level, as long as everything is documented, and the lore can embrace the idea, “tuples are array/slice-ish, so coercion to arrays/slices is generally going to work”. In my case, I have dozens of these enums, with egads of members in each (it represents the spec decently well), and it’s rather ergonomic to do it this way and spare the tons of [_]Enum1{ ... }instances (or, worse in a way, [114]Enum2{ ... } instances). It is actually quite gentle on the eyes this way, and seems quite explicit, really. (The function, in the place of my contrived farr(), is actually an std function whose signature is the same - I have to provide a slice one way or another.)

3 Likes