Zig Code Smells

I have to disagree here. I love zig’s try. I like that it’s not exception-handling and not plain error-code returning; instead, it has the benefits of each, that I really like, without the drawbacks. It has to be taken on its own terms, and there are pitfalls, as with all design tradeoffs, but it’s definitely not code-smell in my book. Plus, you can handle every single error immediately, at every level in the stack, if you want - nothing is stopping one from coding that way. I think it’s great.

EDIT: Sorry… I only just noticed the dates and realized that this is a very old thread that predates my participation in ziggit. Hmnnn…

2 Likes

Interfaces looks beautiful, however it makes generics quite restricted. Just look Go for an example. It hurts the simplicity/flexibility/powerlessness of anytype.

i changed my mind since then so I agree with you :slight_smile:

1 Like

I’m actually kinda on the fence about it.

In one of my projects I have a type which is made up of about 30 different fields, most of which are allocated strings or even hashmaps.
So I group all these allocations in an arena (which gets its backing allocator from the user) and save the arena as a field too.

But one might also have quite a few instances of these things around in an array. But one might also only want to keep one or two around long term. (I have written user code for all of these cases (at least the ones I find reasonable).)

But I guess there are worse things than an arena using an arena as its backing allocator.

One (unenforced) lint I go by is that public functions must have an explicitly defined error set.

Private ones are fine since these are only available locally.

2 Likes

The problem is if you want to do arithmetic with strong types. For example if these types are physical measurements like lengths or mass.

Obviously adding a mass to a length is nonsensical, so you want distinct types to prevent doing that accidentally.
But right now you always need to either do @enumFromInt(@intFromEnum(a) + @intFromEnum(b)), which removes the point of creating these types in the first place, or you need to create explicit functions for every single arithmetic operation, which makes it harder to read:

const e = a.add(b).multiply(c.add(d));
const e = (a + b) * (c + d); //with proper distinct types

anytype sucks and the only real use I can see for it is anonymous tuples to imitate variable arguments anywhere else its just smelly

I think it’s right now very easy to make ZLS give up.
I actually manage to trigger it without using comptime at all from time to time…

And even then, there are problem domains where you want to heavily lean on comptime because the only other practical alternative would be a code generator which run as part of the build. But that’s a lot less ergonomic than comptime.

And that’s not even considering the cases where comptime is a simple compile time ↔ runtime tradeoff. There just are cases where doubling your compile time to make a small 0.1% runtime dent is a worthwhile tradeoff (for example in HPC where that 0.1% can still be multiple days of computation time).

I once had a professor who works on tectonic plate simulation and as he said, some of these simulations can take multiple months.

undefined as a default field value hides the fact that it is undefined from the user, if they are unaware, they could easily use it before assigning a meaningful value. Using an undefined value is always a mistake!

It is much preferred to force the user to specify undefined themselves when they create an instance, not only is it more visible, but it is also a decision they made; Now it is far less likely they would make this mistake.

7 Likes

Thank you. I don’t regret accidentally resurrecting an old thread (though it was really @abdurrd’s fault).

Quite awhile ago I stopped setting default field values at all, but with .init()-ish functions and declaratives like .default, I suggest that the user should create via these schemes. This is then just another benefit of not setting default field values at all. (Perhaps it was one of my reasons to start doing that… at least implicitly… but I’ve forgotten the head-journey by now.) Anyway, thanks, I’d missed that the focus of the thought was on default field values.

I think the more likely reasons you stopped using default values are

  1. constants are more descriptive
  2. you have multiple presets, not just one
  3. default values make it easy to break invariants between fields

That last one is the main motivation for adding the decl literal syntax .default, as even in the std library, which had a rule to prefer constants where appropriate, people were still using default values for types that shouldn’t have used them.

Take ArrayList{items: []T, capacity: usize} as an example, items.len is only the used length and capacity is the full length of the buffer items.len must always be <= capacity. It used to use default values for an empty list, but it is absurdly easy to provide a buffer without updating the capacity or slicing to 0 length, or even set capacity without providing a buffer which will get you a segfault.

On the other hand, default values can be great, a good example is the option pattern used heavily in the build system api.
It is just a feature to be mindful of, not avoid

2 Likes

Touché. Agreed. There’s a time for everything, and default values aren’t intrinsically evil, even if more rarely appropriate.