Curious how others feel about using constructors in struct definitions. This is very OOP to me and from my understanding Zig is a very functional language. As an example I have provided below a snippet of code that shows exactly what im describing. I am creating a new view from data within the parent object. Is this idiomatic Zig? Coming from Golang I understand the balance of using some OOP ideas but only what makes sense. I was curious what others thought about the use of constructors over using an object agnostic function.
I did notice we use .init() on objects quite a lot, so figured my example below might just be okay?
Youâre correct that creating constructor methods religiously for every type is not idiomatic in Zig.
If a structâs fields all have default field values, then initializing with .{} is fine.
Otherwise, a method for initializing is needed to avoid redundancy. An init method on the struct, or a method on another type like youâve done (perhaps a parent object has a method for creating its child) is fine.
Overall, Zig tends to favor an imperative style, does not favor object-oriented programming, and certainly does not encourage functional programming. Actually, I donât think object-oriented programming is a concept opposed to functional programming, even though they are often presented as opposites.
The term OOP carries too many concepts. In my view, OOP is like a mixed bag of features. When different people mention this term, they are talking about different things. Some talk about encapsulation when discussing OOP, some talk about ânamespacing of types,â some about âinfix notation in function calls,â and others about inheritance-based polymorphism.
I donât think Zig particularly cares about whether it needs to strictly guard against OOP. Features that many people like, such as âtypes as namespacesâ and âinfix syntax sugar for functions,â Zig embraces them. As for the controversial inheritance-based polymorphism, Zig neither embraces nor rejects it; in fact, it allows you to design any style of interface scheme, including inheritance-based ones.
OOP is a very loaded term. I donât personally think struct initialization has anything to do with OOP. Itâs more of language level features that allow inheritance and such.
Since the introduction of decl literals, default field initialization has become more frowned upon. The idiomatic approach is to have .empty.init.some_variant instead as decl literal or as a function that decides how the struct is initialized.
The remaining problem is that only default-field-init in the struct declaration allows to define defaults which can be overridden, I hope Zig gets a solution that works with the new .init idiom.
E.g. I canât do this with .init:
// define default values in struct declaration:
const Bla = struct { x: u32 = 1, y: u32 = 2, z: u32 = 3 };
// create a 'Bla' but only override .y with a non-default value:
const bla: Bla = .{ .y = 23 };
âŚe.g. the alternative pattern of overriding the defaults after initialization comes with all sorts of downsides (bla cannot be a const, and readability suffers dramatically):
const bla: Bla = .init;
bla.y = 23; // error: cannot assign to constant
Pretty much all my APIs rely on the âsane - but overridable - defaultsâ, and the move to the .init pattern doesnât really make me happy (as long as defaults in the struct declaration work I donât care much of course)⌠I would like to see more syntax sugar for flexible struct initialization to be sure though.
It gets tricky quickly because it also needs to work for nested structs and arrays (ideally - currently arrays also canât be partially initialized with Zigâs designated init syntax).
But Iâm having deja-vue, I think we discussed those options already elsewhere
you can define fn (type) â type, and you can also ++ comptime slices easily, so while itâs true that most of your Zig code is going to be imperative in runtime, there are quite a bit of FP opportunities in comptime. just a minor addition to your answer
If you move the constructor out of the Proc namespace and into the ProcIdentity namespace then you enable this calling form (which I believe is considered especially idiomatic):
The main disadvantage of moving the constructor to the target is that you lose chaining. You canât do
const z = (
a
.toB()
.toC()
.toD()
âŚ
.toZ()
);
I am personally a big fan of this pattern in the other languages I work in but I donât really miss it in Zig because we have blocks.
const z = blk: {
const b: B = .fromA(a);
const c: C = .fromB(b);
const d: D = .fromC(c);
âŚ
const z: Z = .fromY(y);
break: blk z;
};
Itâs maybe a bit more verbose, but it has the same namespace-nonpollution property and is a lot more flexible! Just think: itâs suddenly easy to slap a debug statement anywhere inside the block, whereas anyone familiar with the chaining style has surely fought against it at debug time, either laboriously breaking it back into variables just for debugging, or doing weird indirect stuff like jamming âprint internal state but return selfâ into the chainable API.
The pattern they wanted to prevent is const list: ArrayList = .{ .len = 16 } that segfault on next usage.
Tbf this is the one situation where fine-grained field-access rights are nice. Not necessarily public/private (thatâs too brutish), but similar to how a file system manages access rights (e.g. readable, writable, âinaccessibleâ in specific scopes (e.g. anywhere outside the implementation module, or in specific modules).
which also means you can fit 7x less of these on a screen.
Nowadays I tend to postpone writing fn init() as long as I can, basically until I can prove to myself that it improves the production code.
In tests where I may need to initialize them a lot, i will just create a local fn makeFoo() instead of polluting the code.
eg.
test {
const TT = struct { // often already justified to exist for other reasons
fn makeToken( ... ) Token ...
};
const t = TT.makeToken; // sometimes even going this far helps
try std.testing.expectEqualSlice(Token, &.{
t(.word, 0),
t(.whitespace, 3),
t(.word, 4),
}, try parseTokens("bla bla"));
}
Though I try my best to use data structures that are narrow enough where all possible values are valid, sometimes this is not possible. In these cases, I use constructors to communicate that there is only a few fixed ways to validly construct the struct. In my codebase, I follow the convention that presence of a method starting with init means the object needs special consideration on construction to avoid invalid values.
Youâre right that for the case where no field defaults are overridden, then a decl should probably be provided rather than requiring use of .{}, and I should have said that.
But I think default fields should be used when they make sense, and donât violate that rule that any combination is valid. Otherwise there is no good way to provide option structs, for example. If some new way appears in the language, then of course that may be better, but for now we do have a way to do it that makes sense.
I thought a bit about your comment and realized that you can get more or less the same behavior as option structs even if you constrain yourself to never using default field values. Instead, you can just let your init take an anytype that is supposed to be a struct (likely passed as an anonymous struct literal) whose fields are a subset of the fields of the target struct. init would then reflect on the fields that it actually has, and provide its own values for any fields it doesnât. The form at the callsite is the same -
x1: X = .init(.{ .a = 1 });
x2: X = .init(.{ .a = 2, .b = b });
but .init is generic and there is no concrete âOptionsâ type, which is of course a downside for readability and discoverability.
Personally, after trying to invent some sort of over-engineered repeatable metaprogramming construct for this sort of initialization, I just landed on this:
const bla = blk: {
var bla: Bla = .init;
bla.y = 23;
break :blk bla;
};
It is a bit of a mouthful, but actually pretty readable and I can still have my const where it matters. Coming from C, I sometimes tend to overlook the power of block expressions.
IMHO, itâs one of the unfortunate limitations in Zig, Iâd love to have some syntax sugar for spreads. This is just noise.
Another arbitrary limitation in Zig is that you cannot really do classic mutable builder pattern because you canât use *T with const. I can see why is that but IIRC Rust allows that, or at least I remember that I could do mutable builders and save the result in const (let).
Yes, it does get annoying, I also do get annoyed when I have to type a lot, but Only one obvious way to do things is the zen. And I do actually appreciate that Zigâs language scope is this narrow â after having written a few (not even too big) programs myself, I basically never have to look up extra langauge constructs to understand other peopleâs code. Itâs a peaceful life.
Zig just wants to be very explicit about where memoryâand especially mutated memoryâlives. Personally, if I saw the need for a builder pattern, I would probably go with something like this:
const bla = blk: {
var b: Bla.Builder = .init;
break :blk b.something(16).complexAction(a).build();
};
Again, a bit more of a mouthful than one might want, because of the block expression, but it does do the trick. But then, I havenât been writing programs where this pattern would be very useful lately.