Not a fan of any sort of out-params tbh, what I’d rather see is a bit more syntax sugar when working with struct and array types (like TS/JS-style destructuring and spreading). IMHO out-params are a left-over relic from the K&R and pre-C99 C era when C didn’t have compound literals and struct return values.
E.g. I’d like to build struct items of one type from struct items of another type without having to write an explicit assignment for each struct item, or I’d want to assign one struct item to another struct item of the same type while overriding some of the member values (all of this can be done very elegantly in TS/JS with the spread syntax)
The current ‘destructuring, but only for tuples’ looks to me like a too limited special case that should be expanded to structs and arrays (the current tuple destructuring syntax is also too verbose IMHO)
…under the hood the compiler would generate exactly the same code as writing ‘verbose assignments’, that’s why I’m classifying this as ‘syntax sugar’ (but the good type of syntax sugar since it saves typing and (IMHO) also can make the code more readable and expressive.
If you’re creating a new variable, it needs to be either const or var. It’s plausible, I’ve done it, for one destructuring to be const and the other to be var, and if you don’t provide either then the variable must already exist.
So I don’t see a way to make this more terse while respecting the existing semantics of the language.
I originally thought that Zig should also have normal-struct destructuring, but have since changed my mind. It’s useful in languages which allow one to implicitly drop fields in the process, but that wouldn’t be acceptable in Zig.
Your suggestion for mixing consts and vars seems pretty nice! Also very much agree that TS/JS destructuring assignment is quite pretty and in comparison, Zig’s syntax is just… weird for no obvious benefit.
var blah = someResource(...);
defer blah.deinit();
const x = blah.x;
const y = blah.y;
I don’t think Zig should make it easy to make that mistake, and there are already decisions, like intermediate values in dot chaining being const, which serve to reduce it.
The destructuring we do have requires explicit discarding of values, a field destructuring would need to do the same, and just using _ wouldn’t really fly here because it still discards too much information. In a sense, six underscores when extracting two fields from an eight field struct, conveys less information than just leaving them out, because it’s just spam on the screen.
Then when you combine it with renaming (the TS convention wouldn’t work, those are types in Zig) it starts to stack up to causing more problems than it solves.
Tuples in Zig are ad-hoc and structural, every tuple with the same slot signature is of the same type, and it’s the only type for which this is the case. So having an ad-hoc destructuring for tuples makes sense.
In general I’m not a fan of proposals which primarily save a bit of typing. If there’s any hint of them tending to make programs harder to understand, and field destructuring carries more than just a hint of this, I’m not interested.
So while yes, one could have a way to elide multiple constancy modifiers, I don’t think we should.
Consider:
// Exhibit A
const a, const b, var c = someStuff();
I have a few problems with introducing a shorthand here. The first is that it’s accidental that the shorthand applies, compare to:
// Exhibit B
const a, var b, const c = someStuff();
The second is, that even if we were to accept this:
// Exhibit C
const .{ a, b }, const c = someStuff();
It would be perverse to prohibit the form in Exhibit A, so now we have two ways to write that line, and that is something best avoided.
Last, and perhaps the most serious problem, is that Exhibit A is easiest to read. In Exhibit C, I have to remember that there’s a special anonymous struct destructuring syntax, and that it means that a and b are both const, I also have to keep in mind, in contradiction of the structure of the text, that the relationship between a and b implied by the grouping is accidental, having nothing to do with the return value of someStuff.
We always favor readers of code over writers of code when there’s a conflict. A bit of extra typing never killed anyone.
Edit: to see what I mean by perverse, try to write a grammar which prohibits Exhibit A.
You could create similar utilities, to assign fields from one type to fields from another variable with a different type, while creating these things you can use arbitrary conventions like input and output field names must match, or don’t have to, etc.
You even could create an embedded dsl-like mini-language that describes how things are copied, renamed and possibly even transformed while being copied.
All of that without making the language less explicit, or even changing anything about the core language, just using the existing possibilities of comptime.
And without adding special syntax that is only useful in some rare corner case and makes the language more complicated.
I am not in general against syntactic additions to the language, but I think they should have a big impact on what is possible to do, instead of being gimmicky one-trick-pony additions that feel like they were just imported from another language, I would want something that fits into the language and is consistent with the other features. While I like de-structuring I haven’t seen a more powerful version of it, that seems to fit.
For example I think decl-literals were a great addition, technically they aren’t needed, but pragmatically they allow a new better way to write Zig code, that encourages initial values over having lots of types with fields with default values, that can be misused, by only setting some of the fields (in a way that doesn’t respect invariants between the fields).
When I first read the Zig docs, I thought it was weird that this kind of destructuring wasn’t allowed- but like everything else that’s raised my eyebrows (so far), someone comes along with an excellent and reasonable explanation. Thank you.
I’m not a fan of writing comptime helper function for what should be syntax tbh (but of course it’s hard to decide where to draw the line, I guess the hardest problem in language design).
In a similar vein, prefering the stdlib over syntax additions is the road that C++ (and also Rust) take with their stdlibs and it sucks, IMHO it’s the main reason for why typical ‘modern C++’ and Rust code looks so messy (in Zig, std.meta may be in danger of taking that road too).
IMHO the whole point of syntax sugar like destructuring and spreading is to make the code look nicer, and make programming more enjoyable.
It’s similar to initializing a struct ‘old school’ with one assignment per struct item versus initializing via designated initialization. Designated init removes redundant clutter from the code.
E.g. the difference between this C99 code:
…and this C++ code:
…one is much more readable than the other because the reader doesn’t need to sift through so much redundant code. For me, destructuring and spreading belongs into the same ‘syntax category’ as designated init, it makes working with structured data nicer (and the code a lot easier to read).
Fields in a normal (non external) Zig struct are not ordered, so what should x and y map to? What if what you’re calling x shoud actually map to bla’s y?
Zig does not want you to look at the struct’s source code to figure out how to declare your variables. Besides, bla’s author could change the order at any moment, and this would silently break code at random places.
You could make it so that this only works if x and y are the exact names of the fields of bla, but that’s a pretty big limitation. In that case, you don’t even have to declare your variables, just say “pull the names of this struct’s fields into the current scope” (and that’s pulling names out of nowhere, which Zig is against).
No, the code const { x, y } = bla; would always be equivalent with this, no matter what the order is (e.g. the destructured variable name defines the field name):
const x = bla.x;
const y = bla.y;
This is just an example taken from Typescript. Whether that’s particularly ‘ziggy’ is open for discussion
This would be an error if the struct type of bla doesn’t have the fields xx and/or yy:
const { xx, yy } = bla;
It would be equivalent with this, which would also be an error without destructuring syntax (the compiler would complain about the non-existing struct fields .xx and .yy):
const xx = bla.xx;
const yy = bla.yy;
If the field name and destructured name should be different it requires additional syntax, for instance:
const my_x = bla.x;
const my_y = bla.y;
…would look like this in TS with destructuring:
const { x: my_x, y: my_y } = bla;
TS takes this quite far (maybe too far) and also allows destructuring in the function definition, you can do smth like:
function bla({ x, y, z }: BlaOptions) ...
…this is a function which takes a BlaOptions ‘struct’ and immediately extracts the variables used in the function, e.g. it’s equivalent with:
function bla(opts: BlaOptions) {
const x = opts.x;
const y = opts.y;
const z = opts.z;
...
}
Destructuring works with arrays.
Yes, my bad, I missed this. PS: which brings me to spreading (destructuring without spreading also feels ‘incomplete’ tbh
E.g. in TS you can also do this with array destructuring:
const first = 1;
const second = 2;
const rest = [ 3, 4, 5 ];
In Zig, rest would probably be a slice though.
I come from a ‘mostly C99’ background, but IMHO the init/destructure/spread syntax in modern-JS-slash-TS is the one good feature of those two languages, and it would fit quite nicely into C / Zig style languages when adapted for the static type system.