-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Destructured Assignment #3897
Comments
This looks pretty much like a selective const (A, B) = c; // requires c to _only_ have A and B
const (A, B, _) = c; // requires c to have A and B This would work for both static members via type name and fields via struct instances. Or we could improve/extend usingnamespace @import("std") with Mutex, Thread; // would only import std.Mutex and std.Thread This would be my preferred way as it would allow keeping a single assignment syntax |
@MasterQ32 that sounds like a reasonable variation. It means that you can see that it's a destructured assignment earlier on in the declaration and is more consistent with other languages like Python. const a b ..=.. c; VS const (a, b) = c; I'm going to edit the proposal description with your syntax variation. |
The var (width, height) = getSize();
// VS
usingnamespace getSize() with width, height; const (a, b, c) = init: {
break :init {.a=1, .b=2, .c=3};
};
// VS
usingnamespace init: {
break :init {.a=1, .b=2, .c=3};
} with a, b, c; I'm also not sure how you would declare the new variables as |
So with this, // 1
const (...) = @import("std.debug");
// 2
const (*) = @import("std.debug"); |
I want to explain this a bit further: I am against destructuring of structure values. Why? Your example only proposes destructuring on assignment, but it if done right, it should be allowed on every access to variables. This means: const (a, b) = c;
var (a,b) = c;
while(c) |(a,b)) { }
while(c) |*(a,b)) { } // does this even make sense?!
for(c) |(a,b)| { }
for(c) |*(a,b)| { }
for(c) |(a,b), i| { }
for(c) |*(a,b), i| { }
if(c) |(a,b)| { }
if(c) |*(a,b)| { }
… There's a whole lof of new options that must be taken into account and this will make the language a lot more complicated and is against (Only one obvious way to do things. and even more Reduce the amount one must remember.). Or you disallow the destructuring of mutable variables which will make it a special case only available for tl;dr: I don't like adding destructuring of values as it adds a lot of stuff to the language. Having a selective alternative to |
@MasterQ32 thanks for your feedback. I agree there's alot to analyze in how this interacts with the rest of the language and there may be alternatives that are simpler or more complex. We should be sure to explore all of these. In my initial exploration, I found that only supporting this in "variable declaration" makes it simple (a new grammar rule to support You said "if it is done right it should be allowed on every access to variable". Then you went on to describe how the "right" way of doing this is overly complicated :) You may have said it was the "right" way but I think you showed that it actually isn't. I intentionally limited this proposal to variable declaration because it is simple but still covers most uses case I could think of. Every language feature has a balance and it's important to find the right balance between simplicity and flexibility. The questions we should be asking is what use cases does each variation of a new feature enable and how much does each variation complicate the language. Note that this proposal has 2 parts as well.
Only supporting one field is less complicated, but supporting multiple provides some nice "functional" language benefits because it allows you to create multiple variables from single expression without having to give that expression a name. For example, supporting multiple variables allows you to return multiple values from a labeled block but only supporting one does not. However it also makes it more complicated, so how do we find the right balance? I do this by trying to identify real-world use cases and trying to think of all the ways this can interact with other parts of the language. Community feedback and group effort is also critical here, someone always thinks of something that no one else has thought of. I think you showed that a feature like this needs to be designed carefully at the risk of creating an explosion of complication. |
// 1
const (...) = @import("std.debug");
// 2
const (*) = @import("std.debug"); Syntatically that looks quite nice. However, I think In fact, one of the benefits of this feature is that it will encourage people to use usingnamespace @import("foo.zig"); I would hope to see more of: const (a, b, c, d, e, f) = @import("foo.zig") However, today you would have to do this to avoid const a = @import("foo.zig").a;
const b = @import("foo.zig").b;
const c = @import("foo.zig").c;
const d = @import("foo.zig").d;
const e = @import("foo.zig").e;
const f = @import("foo.zig").f; |
That was probably the whole idea behind this. The "right" was meant in: "Why does it work at this place but not in any other? I like the feature in this place and want to use it everywhere!" Note: All following statements are only on value destructuring as i think that For real-world usecases for value destructuring, i can quote some of my C++ code code: // Iteration over entity-component pairs in a ECS
for(auto [ entity, component ] : xcs::get_components<NameComponent>(universe))
{
std::cout << "entity " << entity.id << " is named '" << component.name << "'";
} This would be possible in Zig if we'd allow destructuring everywhere. For other real-world examples, const (window, renderer) = try SDL.createWindowAndRenderer(…); On the two parts:
I personally don't think this is a existing use case for value destructuring as i cannot imagine a real world use case for this. If a function returns a struct instead of a single primitive value, it has a reason to do so. Otherwise i could just change the return type if not all of the results are relevant.
As shown above, there are use cases where multi-value return types are quite useful to prevent the code to be filled with clobber, so it may increase readability of the code. |
I think usingnamespace @import("foo.zig");
// OR
const a = @import("foo.zig").a;
const b = @import("foo.zig").b;
const c = @import("foo.zig").c;
const d = @import("foo.zig").d;
const e = @import("foo.zig").e;
const f = @import("foo.zig").f; By making it easier to assign multiple values from a struct, hopefully people will do this more often: const (a, b, c, d, e, f) = @import("foo.zig") Another thought I have on supporting "selective imports" with
The first half of the description is only about destructing one field. Main use case being that you want to pull in a symbol from a module, and your intent is that the local name matches the name in the module. Although, I can't really think of alot of use cases outside of that for single destructured assignment. |
so selective imports are going to work on both fields and top level declarations? |
There is already an accepted proposal for destructured assignment of tuples (not decls): #498 |
Would prefer this greatly over |
Id want to have at least 3 features:
|
I recently attended a specs meeting where I learned more about Andrew's perspective on new language features. He described that some languages (like the D language) will accept features if they seem like a good idea/improvement and have a low cost to implement. However, he says Zig has a much higher bar. Namely, before accepting a feature he says to ask:
The next question I wanted to gauge is how ugly can the alternative solution be but still not be enough to warrant a language feature? I brought up the Now that I have a better gauge on how high the bar is for new features, I am certain that this proposal does not meet that bar. The alternative solution to this proposal is much less ugly than the std.ArrayList example. For that reason I'm certain it won't be accepted so I'll be closing it to save others' time, and hopefully, this explanation helps others gauge the viability of future proposals and help converge the communities perspective on what Zig is trying to be. |
@marler8997 I understand that you opened the issue but there are 15 👍 on it currently. This is extremely useful/convenient and an almost ubiquitous feature in programming languages, nowadays. |
@marler8997 I'm going to have to respectfully disagree here. I would argue Zig code is in fact significantly more ugly without this proposal. Additionally, given the amount of positive +1's on this issue I find it very odd you chose to close this issue prematurely. |
Note that we have an I support what @marler8997 said in #3897 (comment) and to further clarify: This proposal is reasonable and solves a problem. However, as pointed out, language features are added in Zig only if they are necessary to avoid footguns when solving real world use cases. The null hypothesis for any addition to the language is "no" and then it is an uphill battle to get it added. It's hard to make a language that is small and simple. It requires saying "no" to a lot of reasonable ideas. |
@andrewrk could you give us 3 example snippets for the 3 use cases (rename, discard, rest) using the #498 proposal? I am asking because I am seeing stuff I don't know about. |
I really hope that doesn't get added. |
Destructured Assignment
EDIT: proposal modified to use a syntax based on suggestion from @MasterQ32
I'm exploring a new "variable declaration syntax" that requires the variable name match the field name of a struct. For now I'm calling it "Destructured Assignment". For example:
Normal assignment:
Destructured Assignment
In this case the
(
parenthesis)
surrounding the variable name enable "destructured assignment", which will assign the variable to the value of the field on the right hand side of the assignment with the same name.To generalize, normal assignment from a struct field is of the form
const V = S.F;
and destructured assignment is of the formconst (V) = S;
. The two forms have a subtle difference in semantics. In normal assignment, a new variableV
is being declared whose name is unrelated to the field nameF
. In destructured assignment, the compiler verifies the new variable nameV
matches the name of a field inS
. It's a subtle difference but supporting it can prevent some types of errors and enable other features as I'll explain below. The following example shows a potential error that is not possible with destructured assignment:Multi-Variable Declaration
With this new feature that requires a variable name to match the field name of a struct, it provides a one-to-one mapping from variable to field, which enables multiple variables to be declared at once.
Note that these 2 types of assignment are not expressing the same intention. The destructured assignment explicitly requires that
V1
throughV4
are valid field names inS
but the normal assignment does not. Also note that you could forgo the multi-variable part and just use single-variable destructured assignment to get this intention:Here are some examples multi-variable assignment would enable:
Multi-value assignment from labeled blocks:
In this example, if we changed the break statement to
break :init {.a=1, .b=2, .d=3}
, then we would get an error indicating that the struct does not have a field namedc
. Note that this also works with functions:Multiple module import declarations:
To see a real-world example,
std.zig
exposes some types from other modules. This change would ensure that the exposed names always match the internal names. It would look something like this:Here is the grammar change required to support this:
The text was updated successfully, but these errors were encountered: