Are there any written-down rules for using default arguments in Zig? It looks like there are potentially two different usages for x: u32 = 0 feature:
To supply initial value of the field. E.g., if in the init method you always initialize x to 0, you could specify that on the field itself.
To supply default, overridable value. E.g., a functions taking options: struct { argument effectively has Python-style keyword arguments.
Note that the two usages are in some sense opposed. If you use defaults for initial value, it would be incorrect for the caller to override the default. But for kwargs pattern, in contrast, the caller is supposed to override.
Iāve also read (esp. after the introduction of declaration literals) that the first pattern is not how this is supposed to work, and that it is, in fact, an anti-pattern, but I never quite understood this claim fully, and end-up using it a lot. Why is this bad?
Tbh I donāt think it makes sense to write down any ālanguage rulesā for default initialization, since it is very much an API design choice (e.g. it might make sense for stdlib coding conventions, but not for the Zig language).
In the sokol-gfx API I depend heavily on the āoption-bagā pattern when creating objects. For instance a render-pipeline object is configured by literally dozens of config parameters (when counting parameters in arrays), but in 90% of situations you only need to override a handful or maybe up to 10ā¦15 parameters, and in no situation you need to provide non-default values for all config params.
A typical example (I would really like to get rid of the init-block here, but different issue):
ā¦for comparison, C99:
The defaults are also carefully chosen to ease the pain for beginners, e.g. specifically to avoid a āblack screenā in the typical hello-triangle case.
The only thing thatās not handled by the default values in the struct declaration would be situations where different sets of default values are needed, and the new philosophy to provide an .init decl-literal would almost solve this problem if Zig had a spread/rest operator, e.g. something like this:
const bla: Bla = .{
.a = override_a,
.b = override_b,
// fill up the rest with default values from the `.init` decl-literal:
..= .init,
};
In that case, it would be nice if Zig would automatically fill up the rest with some standard-named decl-literal, (e.g. .defaults) - just for the case that the language decides to drop the declaration-default values, e.g.:
(I must say I really like the ..= syntax for such a hypothetical rest operator)
ā¦the problem with the rest operator however is nested structs, for instance check out this example in Rust how the depth: nested struct needs its own ..Default::default(), for more complex cases this quickly becomes too much noise IMHO:
I donāt know of this being written down anywhere, but my general understanding is: if any single one of the defaults can be overridden during initialization without making the state of the whole struct invalid, go for default arguments. Otherwise, use an init constant/function that will ensure the initial state is always valid.
If the struct is intended to be initialized with init, I purposefully omit any default field values and do so all within the init function.
If the struct is intended to be initialized using literal syntax, I provide sane defaults for every field possible
The point of this is to provide friction where it makes sense. I donāt create init functions just to copy arguments to fields as-is, so if I am providing one, its purpose is reducing complexity to the caller by using it, performing some other necessary initialization, etc.
On the other hand, if it is intended to use struct literal syntax, I want to make this the happy path and make it as simple as possible, requiring the caller to provide the least amount of data possible.
I have, over time, trended away from default values in structs. The above quote from the language reference is one good rule on usage.
When structs are only really initialized in one place, eg an init function, then I think default values are inferior to just setting the value in that initialization. This is counter-intuitively especially important for fields initialized to undefined. I have found it to be a good pattern to push all of the āundefined at definition but initialized in the init functionā, eg allocs, to the bottom of the struct init. That way they serve as an obvious āthe code following should deal with this fieldā.
The addition of decl literals has, imo, removed another use case where you wanted a simple value as the initialization. Eg after copying BoundedArray into my project, Iāve replaced
The second is more clear from the call site the state which it is being initialized into.
I do think that default values makes sense for options structs, but Iām currently undecided on any other use cases. Decl literals handle most of it better. Thereās an argument to be made for using them when adding a field to not break existing API users, but I think thereās a stronger argument that breaking changes are ok, actually.
Something that was recently added that need to be accounted for by conventions is importing from Zon.
As of now, I have been following the following convention:
If it is default initializable (all fields have defaults), then that is OK, if it is not default initializable, I provide an init method or declaration.
Import from zon is useful because it makes it easy to ingest large amounts of data / configuration without allocation at compile time. But now I need to figure out how to communicate the need for validation to the user.
So now I need to provide some validate method and document that if init or decl are not used, then validate must be called. Otherwise I will have to litter my code with assert isValid everywhere before using the structs.