-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
new for loops don't want to give pointers to elements of an array #14734
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
Comments
Iiuc these are the new intended semantics. If you want a pointer to the array elements, you need a pointer to the array, e.g. |
Indeed - see the second note in #14671 (comment) |
Uh, thank you both. I'm sure there are a few implementation details that motivated this choice, but setting aside my expectations (it did feel like a bug to me), there's one thing that maybe could be kept into consideration. When I wrote Easy Interfaces with Zig 0.10.0, a point arose about how to have interface methods that can modify the underlying implementation. You can read the exchange in the comments at the bottom of the article. In the end, @SpexGuy confirmed that this is correct code: const Cat = struct {
hp: usize,
pub fn eat(self: *Cat) void {
self.hp += 1;
}
};
const Animal = union(enum) {
cat: Cat,
dog: Dog,
pub fn eat(self: *Animal) void {
switch (self.*) {
inline else => |*case| case.eat(),
}
}
}; Note how the This does make sense, but it seems to be at odds with the fact that the I'll leave this issue open in case this consideration does have comparable weight to the other reasons that led to the current design. |
This also surprised me and think it would be interesting to find out why this choice was made. My thinking goes, given this: The * already indicates that I'm explicitly interested in modifying the element of the array. To me the for loop is a language construct that takes something that can be iterated over, and there would be absolutely zero confusion about what I want to do even without the &. Given that it's a language construct it could just be part of the language spec how it behaves. In fact I'm a bit more confused because what is the problem that could happen if someone used * and didn't use &? Clearly it's fine to iterate over an array without the &, so what does that mean, is it being copied by value? Clearly not because that would be pointlessly bad for performance. So it's not clearer, it's not more explicit, but it does add an extra, unintuitive, easily forgettable step (requiring going through another compile cycle). But - I haven't thought it through from all possible angles, there may very well be something that hasn't crossed my mind. But this is what's going through my mind as a user of this new (great!!) feature. |
Arrays in Zig are pass-by-value, unlike C, where array identifiers are pointers. Think of it as sort of like a |
This is true, but I think this change is very unintuitive. While I can see how this would make the compiler simpler, I think bending the language around that particular optimization is a mistake. IMO we should instead pursue other methods to rethink the way zir treats values vs pointers, potentially fixing these problems in other places as well. |
It would be nice if tagged unions also did this coercion, so that you don't have to dereference Every time I read code that dereferences a tagged union pointer in a switch statement, I ask myself:
The syntax makes it read like a stack copy, but real world code typically doesn't want to mutate a stack copy created in the const Animal = union(enum) {
cat: Cat,
dog: Dog,
pub fn eat(self: *Animal) void {
switch (self.*) {
inline else => |*case| case.eat(),
// ^~~~~
// is case a pointer to a stack copy of self?
}
}
};
Instead: const Animal = union(enum) {
cat: Cat,
dog: Dog,
pub fn eat(self: *Animal) void {
switch (self) {
inline else => |*case| case.eat(),
// ^~~~~
// no ambiguity. case is clearly &@field(self, field)
}
}
}; The more uncommon usecase of a switch statement on a pointer address value could still be covered via |
Yeah 👍 this is what I mean about it being a construct. Also, for function calls it makes sense that you explicitly add an & because you can see at the call site that you're passing it in to be modified (presumably). But with the for loop it's right there next to it, in the capture. Again it's not a huge deal but it is a bit annoying as it doesn't seem to give anything to the reader of the program. And to the writer it is in effect worse, especially coupled with the compile error when you comment out the thing that changes the capture - now you'll need to remove two characters to make it compile. And then re-add them when you comment that thing back in again. |
IMO you need to build a different mental model for this. I had a similar question a while ago and talking with Spex made it clear that constructs like It would be nice if the language had semantics for this stuff that were simple enough to be intuitively understood, so probably that's a goal worth pursuing explicitly, but in the meantime we need to avoid using 1 model for everything and base our analysis of language features only from that perspective. As an example, your proposal would mean that we would be switching on a pointer but the cases would not be pointer values, which would just move the weird quirk from one place to another (as switching over a value of type IMO moving the quirk from one place to the other wouldn't be an improvement and, more importantly, the |
This is working as designed. When a by-ref capture is present in a switch case, Zig implicitly takes a pointer to the switch condition. The reason As for It is possible to make the changes suggested by @Jarred-Sumner to I do think that Jarred's
I think if all of my suggested changes are applied, it would result in consistency across |
This feels very deeply unintuitive to me. You absolutely should not need to look at the contents of every capture to determine how to interpret a switch. This is easy for the compiler but difficult for humans. If we were going to do this (and for the record I don't think we should), we should instead give It's worth noting that this "implicit |
This also doesn't solve the underlying problem that stage 2 doesn't know how to handle references, which still affects RLS and parameter semantics. Instead it bleeds the known-to-be-problematic implementation details of the current compiler into the design of the language. That's why I don't like it. "Simpler for humans" feels like an after-the-fact justification here to make "simpler for a compiler that uses SSA in places it shouldn't" sound ok. |
As this has been tagged as a proposal, maybe wider-reaching syntax suggestions are on the table. a: *Animal = &instance;
switch byref (a.*) {
dog => |*dog| {...},
cat => |cat| {...}, //not taking a pointer is also ok, but error if none of the captures do.
}
var jumped_or_error = a.jump();
if byref (jumped_or_error) |*jumped| {
jumped.height *= 2; //context: we are on the moon
} else byref |*e| { //not many use cases, but I guess for consistency
e.* = error.DidNotJumpButAlsoOnTheMoon;
}
while byref (a.eatNext().*) |*eaten| {
eaten.tasteLevel += 1; //food tastes better on the moon
}
for byref (a.name) |*char| {
char.* = upperCaseAssertAscii(char.*); //makes the text a little more readable from far away (like the earth)
} While we could also signal it by the We could put it in front of every capture as |
IMO What I mean by that is that if you do:
then:
I think that would be more consistent and be a simpler mental model. |
In the past article introducing soa, a data structure // Heal all monsters
for (soa.mut_items(.hp)) |*hp| {
hp.* = 100;
} |
Are there any real world use cases where switching on a pointer to a tagged union is actually useful? Or any pointer for that matter. I've never seen it out in the wild. I'd say that prioritizing the common case (mutating the member of a union through a switch capture) over the uncommon case would make more sense. A really simple way to do that would be to just disallow switching on pointer values; so to switch on a pointer value, you'd need to use |
@haoyu234 Method |
I don't think switching on a tagged union pointer is a thing, that is why I propose that you have to pass a pointer to actually switch on the union value but being allowed to mutate the content. `switch(&val)` would actually switch on `val` but allow you to mutate the content.
It is less "theorically correct" as it is some kind of syntax sugar, but more natural and less error prone.
…--
Nicolas Goy
https://www.kuon.ch
-------- Original Message --------
From: InKryption ***@***.***>
Sent: March 1, 2023 3:39:41 PM GMT+01:00
To: ziglang/zig ***@***.***>
Cc: Nicolas Goy ***@***.***>, Comment ***@***.***>
Subject: Re: [ziglang/zig] new for loops don't want to give pointers to elements of an array (Issue #14734)
Are there any real world use cases where switching on a pointer to a tagged union is actually useful? Or any pointer for that matter. I've never seen it out in the wild. I'd say that prioritizing the common case (mutating the member of a union through a switch capture) over the uncommon case would make more sense.
A really simple way to do that would be to just disallow switching on pointer values; so to switch on a pointer value, you'd need to use ***@***.***` - or we make an exception for something like `*anyopaque`, so you'd need to use ***@***.***` to switch on a pointer value. Then from there, it's logical to just allow implicitly dereferencing a pointer to a tagged union in a switch.
|
Just my 2 cents, but I find the need to write this very counter-intuitive: for (&good_digits) |*d| {
d.* = 6;
} If |
I think the key here is understanding how arrays and slices relate to each other. A pointer to an array is basically just a slice of comptime-known length. And it makes perfect sense to iterate over a slice. for (&good_digits) |*d| { is equivalent to for (good_digits[0..]) |*d| {
If you need further help I'd suggest to join one of the Community Spaces. From what I have seen the discord server and the ziggit forum are the most active and both are very welcoming towards newcomers who need help. |
Thanks for explaining. I still don't understand though, in this case, the need to handle the array as a slice and not just as an array. I found this topic on Ziggit that digs more on this aspect. |
Rejecting in favor of #23509 |
Zig Version
0.11.0-dev-1817+f6c934677
Steps to Reproduce and Observed Behavior
Causes this compile error:
Expected Behavior
Should compile and change the array to 666.
The text was updated successfully, but these errors were encountered: