Overall, I think the biggest problem with this syntax is that it’s too narrowly applicable. It only works for composite types where all fields have default values and can express them using .{}
. This syntax doesn’t work for any composite type with non-default fields that can’t be initialized with .{}
. This syntactic sugar is so narrowly applicable that I don’t think it’s particularly meaningful. The validity of C99’s syntax is based on its ability to ensure that all subtypes have default values (the agreed-upon magic value “0”).
This is where I don’t agree - it can just as well be an uninitialized error when defaults are missing, but let’s keep it at that
FWIW though, all the sokol C APIs are built around the idea of default-initialized ‘option-bag-structs’, and this maps pretty well to some non-C languages, but not to others (meaning the API is less convenient to use in some non-C-languages - but still as convenient as possible within the syntax limits of the target language - it just looks messier than what it could be
The ‘zero-means-default’ thing of C is really just a detail that’s not relevant to the API user - for the API user the only thing to know is that an omitted struct item is default-initialized - and this is what makes the API convenient to use, because some structs have dozens of items but only a handful need to be non-default-initialized in the most common situations - and Zig offers this feature and it’s also used extensively in std APIs, just not for arrays.
For API users, the core difference between zig and C99 is that C99 guarantees that all composite types have default initialization, while zig does not. The main advantage of C99’s guarantee of default initialization for all composite types is that the default initialization is 0. This is not just an implementation detail, but a guarantee of guaranteed default initialization.
In terms of syntax, zig currently has two types of anonymous composite literals: anonymous structures and tuples. Zig’s array initialization is currently based on tuples, while designated positional initialization syntax requires the introduction of a new type of anonymous composite literal. Given that zig itself does not guarantee default initialization for composite types, introducing a new composite literal suitable only for very specific scenarios is inappropriate for a narrowly applicable syntax.
A potentially more general concept is “mass assignment table.” Imagine there is a built-in function @massAssignment
:
@massAssignment(mass_assignment_table: anytype) anytype
const array :[6]u8 = .{ 1, 1, 4, 5, 1, 4 };
array = @massAssignment(.{
.{ .index = 1, .value = 4 },
.{ .index = 2, .value = 1 },
.{ .index = 4, .value = 9 },
.{ .index = 5, .value = 2 },
});
try std.testing.expectEqual(.{ 1, 4, 1, 5, 9, 2 }, array);
Therefore, it is possible to combine @splat and @massAssignment as an array initialization method:
@splatAndMassAssignment(scalar: anytype, mass_assignment_table: anytype) anytype
…but Zig produces a compile error if the initialization (user-provided + default-values) would leave anything uninitialized, which is much better than C’s zero-init behaviour. The ‘filling up missing items with defaults’ would just be extended to missing array items - but would fail to compile just the same if the initialization would leave anything uninitialized.
@massAssignment( … )
I could probably cobble together a generic helper function for a quite similar functionality, but IMHO it would be better to have syntax for array initialization, not builtins or std.meta helpers - since this smells too much like the C++ philosophy of throwing missing language features into the stdlib instead (which quickly leads to messy/unreadable source code - and as I mentioned elsewhere a couple of times - Rust has that exact same problem and that’s why “idiomatic Rust code” looks just as bad as “modern C++ code”).
The @massAssignment
builtin would also not offer the ‘random access’ feature which is quite important (essentially none of the C99 designated init features are unimportant, they all perfectly click together) - and adding even more builtins looks like adhoc-fixing around on the symptoms instead of designing a syntax where “the whole is greater than the sum of its parts”, and this is exactly what the C99 designated init syntax is. Each individual feature doesn’t look all that impressive, but together they elevate initialization to a level that’s rarely seen in other languages.
A good example is the half-assed designated init in C++20. The C++20 committee didn’t understand what’s great about the C99 designated init feature set - they only picked a few features which they thought were useful and fit into C++. The result is that C++20 designated init is pretty much useless for anything but trivial structs. Zig is already much better than C++20 and closer to C99 - but not quite there yet
This is a significant inconsistency, meaning that a composite value type whose fields all have default values is privileged over other types. A specific syntax is allowed only for this composite value type; it’s invalid for any other type, whether it’s a primitive value type, a pointer type, or an array of any type with non-default fields. Why should a specific syntax be reserved for extremely special types? Don’t arrays of other types require that most elements have the same value, but a few special elements require additional specification? Of course they do.
Convenient but a bit strange. Rust has the same thing I believe: this partial initialization when default values are provided. I also often wonder how things boil down to final assembly.
Tbh I can’t follow anymore, I can’t see any language design inconsistency, or why a specific syntax would be required (even the random array item access doesn’t require new syntax since Zig already has syntax for identifying an array item via [index]
).
A compound type that can be fully default initialized without any user-provided values isn’t anything special in Zig - rather that this is not working for array items seems like an inconsistency to me.
Rust has the special ..Default::default()
convention to fill up defaults, e.g. it’s closer to the JS/TS spread syntax (Default::default()
returns a fully initialized struct, and the spread syntax ..
‘spreads’ those default values into the gaps where the user hasn’t provided values).
It’s more flexible, but looks very messy/redundant for deeply nested structs.
Rust has the same problem as Zig though when it comes to nested array initialization. Array literals in Rust also must explicitly define all array items - Rust has a somewhat more convenient init-block syntax to work around the problem though:
…but Rust isn’t really a great example when it comes to a clean language syntax - it’s at best a good example for what language syntax should not be
Talking about arrays / slices…
Wouldnt this be nice?
part1 = slice[0...1]
;
part2 = slice[2...3]
;
instead of the
part1 = slice[0..2]
;
part2 = slice[2..4]
;
which often overburdens my off-by-one-oh-yes-minus-one brain parts.
Zig already has this inclusive thing in switch statements.
I’m fine with exclusive ranges but the inconsistency indeed isn’t great.
Other languages have syntax like 0..<3
vs 0..<=3
for exclusive versus inclusive ranges, which I find easier to grasp
I tried Rust for a few months. Impossible reading, impossible writing.
The whole concept of the borrowchecker is just totally wrong. I like freedom
The initialization of arrays and structures is fundamentally different. Structures are initialized using anonymous structure literals, while arrays are initialized using tuples. The .{}
does not have a ‘type default value’ semantics; it just happens that all fields of this structure type have default values, so there is no need to specify any additional fields to explicitly initialize a structure. This is also why version 0.14 encourages the use of declaration literals instead of .{}
, which reiterates that the semantics of .{}
itself is not that of default values.
For what it wants to do it’s okay IMHO. It’s almost anything else in Rust I dislike - outside of memory-safety Rust copies too many problems from C++ (mostly that there’s no clear separation between the language and stdlib)
I almost always define const empty
in my structs.
like here
Yeah, I get where the restrictions are coming from, but this sounds like a parser implementation detail to me which shouldn’t dictate syntax limitations. I guess this is where my mindset fundamentally collides with the Zig language design philosophy
E.g. I don’t mind much that the ‘struct literal initialization syntax’ in C99 is its own ‘mini-language’ that can’t be found outside struct literals - the advantages of that approach in daily coding are more important than the minor ‘impurities’ IMHO. But to fix that inconsistency it would be better to fix the ‘outside’ rather than restrict the init-syntax.
We need to clarify what the semantics of this syntax are.
For example, const array :[len]Type = .{[i] = value}
has the following semantics: initialize all elements of array
to the default value of Type
, except for the specified index i
—the element at index i
will be initialized to value
.
Now, let’s discuss the question: what is the default value of Type
?
In C99, this problem is easily solved: it assumes that the default value of all types is the magic value “0” (corresponding to pointers, that is, NULL).
In zig, there is no syntactical concept of a “default value” for a type. For types that can be initialized with .{}
, .{}
does not semantically refer to a default value; it simply “happens” that all fields of the structure have default values, eliminating the need to explicitly specify additional fields for initialization.
For zig, to achieve a similar goal, the best semantics would be “initialize all elements of the array to a specified value (not the “default value”), except for the element at the specified index, which will be initialized.”
I’ve only skimmed this thread, so may have missed some things, but just wanted to mention some relevant points. There are a few different things being discussed here, so these points are a bit all over the place.
- #5039 proposing
{ inits, without, the, dot }
is still on the table, though I personally dislike that syntax because it is visually ambiguous with blocks, which are a very different construct. I don’t like that{foo}
and{foo;}
would both be valid expressions but have very different meanings and you don’t know which one you’re dealing with until you hit the;
. [ inits, like, this ]
I think could work in terms of the grammar, though it’s a little awkward to parse because when you see[expr]
you don’t know if it’s an initializer or a type ([expr]T
creating an array type) until you figure out whether the following code is an expression.- With that syntax,
[a][b]
is a particularly weird expression. Does it mean.{a}[b]
(creating a tuple and indexing it), or[a].{b}
(array type syntax whereT
is.{b}
)? Obviously the latter is always semantically invalid (.{b}
isn’t a type), so we’d probably want to treat it as the former, but the fact that this question exists at all seems weird to me. I can’t see a situation where you’d ever write[a][b]
, so this perhaps isn’t a huge deal – though to be honest, I think[ a, b, c ][i]
looks kind of odd too, but perhaps that’s just me.
- With that syntax,
const arr: [10]S = .{};
is not and will (almost certainly) never be allowed. Aggregate default-inititalization is not recursive. This is important, because.{ ... }
is not a “catch-all” initializer; it is an aggregate initializer. It wouldn’t make sense for.{}
to initialize the elements to.{}
, because that only makes any sense if the elements are aggregates.- It also introduces a problem of “friction in the wrong direction”. The
.{}
initializer is usually only useful when struct fields have default values, and that should be relatively rare. The vast majority of usages of struct default fields are incorrect and should instead be default-init declarations or functions. This is also true in the compiler and standard library code – the proper conventions were not really followed until Decl Literals were introduced in the 0.14.0 release cycle. Allowing.{}
instead of@splat(.{})
introduces friction to doing the right thing, because@splat(.init)
is much longer than.{}
, making people likely to wrongly prefer the latter. - TL;DR: please stop giving struct fields default values, it’s rare that they’re correct at all, and extremely rare that every field on a struct should have one. Read and absorb the language reference’s guidance on the topic.
- It also introduces a problem of “friction in the wrong direction”. The
- Labeled blocks are fantastic, and people should use them more! Using imperative code just to initialize data structures is perhaps “inelegant” in some abstract way, but it’s typically very readable; complicating the language with extra initialization features would harm readability compared to just writing labeled blocks occasionally. Sure, it’s a couple more lines, but everyone reading your code knows what it means provided they understand labeled blocks.
This one’s not a bullet-point just because it ended up very long: @floooh, the sokol example you linked (this one) is an example I’d like to dive into a little. It looks to me like this usage is clunky because the API is a direct mirror of a C API instead of adjusting it to Zig’s design. A simple enhancement which doesn’t require changing the generated code (texcube.glsl.zig
) would be to add an init
to VertexLayoutState
:
pub const VertexLayoutState = extern struct {
buffers: [8]VertexBufferLayoutState,
attrs: [16]VertexAttrState,
pub fn init(mapped_attrs: []const struct { u4, VertexAttrState }) VertexLayoutState {
var attrs: [16]VertexAttrState = @splat(.{ .format = .invalid });
for (mapped_attrs) |pair| {
const idx, const attr = pair;
assert(attr.format == .invalid); // the user presumably shouldn't be passing this?
assert(attrs[idx].format == .invalid); // not already initialized
attrs[idx] = attr;
}
// ignoring `buffers` for now because I don't know how it's used
// presumably it should be init'd with a second parameter to this function
return .{ .buffers = @splat(.{}), .attrs = attrs };
}
};
…making the usage look like this:
.layout = .init(&.{
.{ shd.ATTR_texcube_pos, .{ .format = .float3 } },
.{ shd.ATTR_texcube_color0, .{ .format = .ubyte4n } },
.{ shd.ATTR_texcube_texcoord0, .{ .format = .short2n } },
}),
See how this is more legible and simpler at the usage site, and also gives us safety (via the assert
s) against accidentally passing the same index twice? C APIs are designed very specifically to play into C’s design, and in particular are often reliant on its (quite unsafe) initialization semantics; when adapting such an API to Zig, you often need to adjust it to make the usage fit in, and you shouldn’t be afraid of doing that! This is only one option – if you change how the generated code looks, you could probably do even better. Perhaps the generated code can include pub const Attrs = enum(u4) { pos, color0, texcoord0 }
, then you have VertexLayoutState.init
take that type and do a little bit of trivial metaprogramming, ending up with a usage like:
.layout = .init(shd.Attrs, .{
.pos = .{ .format = .float3 },
.color0 = .{ .format = .ubyte4n },
.texcoord0 = .{ .format = .short2n },
}),
I’m generally of the opinion that Zig users tend to go overboard with metaprogramming, but it seems to me like this example would probably be a perfectly tasteful use case for it! The meaning is clear, the syntax is clean, only a small amount of code (this init
function) is generic, and the user is statically prevented from accidentally omitting a field or accidentally duplicating one. The sky’s the limit
I don’t agree with this one point, but otherwise thanks for hopping in and clarifying things
The .init()
helper function looks like a good compromise, and I can probably code-generate those helpers automatically in the bindings-generation. It is very similar to what I do in the Nim bindings (except that Nim has a special concept called ‘converters’ for this):
For initialization ‘random’ array slots via code-generated indices I would still need to fallback to labeled blocks though I think.
PS: wait I missed this part:
.layout = .init(shd.Attrs, .{
.pos = .{ .format = .float3 },
.color0 = .{ .format = .ubyte4n },
.texcoord0 = .{ .format = .short2n },
}),
This looks pretty great!
PPS: still a bit tricky because ideally I’d like the .init()
helper directly on an array and not on the parent struct which nests the array (in that sense the VertexLayoutState
example is a bit misleading, e.g. it should be more like this:
.layout = .{
.buffers = .init(...),
.attrs = .init(...),
},
…where both buffers and attrs are nested arrays of the .layout
struct item.
But thanks for nudging me into a different direction (using some sort of .init()
helper. I just need to figure out how I can make that fit into all places where nested arrays are initialized. Maybe I generally need to wrap arrays into a struct so that I can add a .init()
‘member’ and hoping that the memory layout is still compatible with a ‘bare’ C array.
The other complication is that all structs need to be C compatible if I want to avoid a copy from a Zig struct into a C struct (and if I do that anyway I could also replace those nested arrays with slices which would also solve most (or at least some) init-problems).
I think if we use []
as the array definition literal, then we must change the index access method from [a]
to .[a]
. I personally like this access index scheme, which expresses the meaning of “accessing an element” more explicitly.
What then?
If we have a struct with many fields and need:
- an initial empty value
var s: MyStruct = .empty;
- to clear the struct for re-use
fn clear(self: *MyStruct) { self.* = .empty; }
it is quite conventient to have these defaults and a .empty
const, otherwise we need to re-type all that struct fields in some init()
function.
At least… I think defaults are better than undefined
.