-
Notifications
You must be signed in to change notification settings - Fork 17.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
spec: allow range-over-func to omit iteration variables #65236
Comments
What if it's just a vet check that looks for errors not silenced with |
I don't think I understand what is being proposed. Doesn't the spec text of #61405 already "allow range-over-func to omit iteration variables"? The example :
The proposed text:
Anyways I am confused about what is being proposed here.
If we think there is a subset of functions like |
I don't see how this buys anything other than ceremony. |
We know that the design of We can guess that range-over-func will become the dominant way to iterate over all sorts of sequences where failure is possible: file lines, DB rows, RPCs that list resources, and so on. I'm not saying that we should require the iteration variables, but I am saying that if we don't, we need a good story for how to avoid introducing many bugs into Go code. Like a vet check for when the second return type is |
This comment was marked as outdated.
This comment was marked as outdated.
I agree that not handling the error is a real problem. I do not think that iterators yielding errors solves that problem very well, especially if that means having to immediately buttress that in a way that forces everyone to use range-over-func differently than native iterators. Even if I'm wrong and yielding errors is a boon, then static analysis is more than capable of linting it only in the case when an error is iterated over. @jba here's https://pkg.go.dev/bufio#example-Scanner.Bytes rewritten to use a hypothetical iterator that returns |
The only benefit of passing an I see a few possible patterns related to errors: for line, err := range FileLines("/etc/passwd") {
if err != nil {
// log or handle error
break
}
...
} for line, err := range FileLines("/etc/passwd") {
if err != nil {
return err
}
...
} Both of these are IMO better solutions than handling the error after the loop as with the The other possible pattern ( for line, err := range FileLines("/etc/passwd") {
if err != nil {
// ignore errors
continue
}
...
} If the intent of the developer is to ignore errors, then the above could instead be written as (same semantic meaning): for line, _ := range FileLines("/etc/passwd") {
...
} The However, I would advice against allowing the following pattern if the iterator returns an error, because the for line := range FileLines("/etc/passwd") {
...
} If you want to handle/log individual errors and continue, you would still need to use this pattern: for line, err := range FileLines("/etc/passwd") {
if err != nil {
// handle or log error
continue
}
...
} Handling and logging individual errors is not possible with the handling after the loop approach. |
Looks like they finally fixed it, but for months the marketing page for Copilot showed code with a missing call to |
@meling To clarify: I fully support yielding I oppose an error that signals an abnormal end to (or failure to start) the iteration being handled within the loop. In other words, I think if you can |
What about an iterator that's using a I've been writing such an iterator, and I've changed my mind about the answers to these questions several times now. |
This is a really interesting question - to me a practically useful solution has been to delay passing |
That's already the case for my approach, but it doesn't obviate the question of whether the iterator should yield |
What would be a reason for the iterator to not return the |
This seems certainly an interesting API design question, but I don't think it's something that needs to be answered here. If it doesn't yield an error, then there is no question. If it does, that doesn't seem different from any other error, as far as this issue is concerned. Personally, I'd just leave that up to the third party package, in any case, if there is no clearly correct answer. |
It could just stop the iteration upon detecting that the |
Maybe pure blasphemy, but for err, line := range FileLines("/etc/passwd") {...} helps force the issue for error handling. |
It's a moderately common mistake to write |
It probably could be changed the way loop iteration variable semantics were changed? Different types being iterated over having different rules for omitting loop variables feels very surprising. I think if it gets addressed, it should be addressed for all types that can be iterated over in |
Then it was just re-introduced with range-over-int for range 5 {
...
} (errors are simple values, nothing special) +1 for the vet rule though |
Here is an experience report that bears on this issue. Previously, I advocated that iterators that can fail should return
Over the weekend I converted a hobby project to use the experimental range-over-func feature. Part of the project is a persistent map: I started by defining
As I anticipated, to iterate over keys-value pairs I would need a type for those pairs, since there is no 3-variable range statement. That was always going to be a little awkward. In my mind I was imagining loops like
where I could at least use good names for the keys and values. But these maps are generic, which means
to
And that started to really feel ugly. So I started looking for another way. First I used pointer-to-error arguments, as mentioned above:
That's an improvement, but it still made me uncomfortable. For one thing, although you do have to pass an argument, the compiler doesn't help you check it—it considers passing the pointer to be using the variable, so it doesn't fail with "err declared and not used" if you don't check Another problem is that you might check the error too soon:
That's definitely a mistake— Where I ended up was something that is obvious in hindsight:
You use it like this:
(Like the pointer-to-error approach, this assumes that the first error ends the loop.) This might look like it still has the problems of Having the compiler complain about an unused Why is this obvious in hindsight? Remember that
to functions over sequences:
so we move from errors:
to functions over errors:
A Haskell programmer would say that we've "lifted" our values into the space of functions. (I think.) For every utility function that "collapses" a sequence to a value like a slice, we can have a variant that takes an error function:
So simple uses of iterators, ones where you just want the get the slice (or map or whatever), still can be simple:
For many other functions on iterators, like Map, Filter and Reduce, getting the error out of the Unfortunately functions that take multiple sequences will need special attention. For example, consider the function that Python calls Writing an iterator that returns an error function involves some boilerplate that we can encapsulate in a type:
Here is how you would use it to write an iterator over the lines of a file:
|
@jba IMO that is still problematic. Note that an Not that this isn't also a problem with taking a pointer (which I also don't like, FWIW). I just don't feel the same level of "this solves everything" optimism I read in your post. |
@jba Thank you for the report. Comparing this: for item, err := range productsByID.Items() {
if err != nil {
// handle error or return or continue
}
id, product := item.Key, item.Value
...
} to this: seq, errf := productsByID.Items()
for id, product := range seq {
...
}
if err := errf(); err != nil {
return err
} There are no extra lines. For the drawbacks of the latter, I'm still very much in favor of the former approach; it is simple and easy to understand. Regarding the functional part of your post; the boilerplate and complexity doesn't seem worth it. I think a simpler approach is necessary, if I'm going to like it. But I confess that I have not thought carefully about how iterator functions can be used and combined when there are errors involved. edit: added missing range keyword in first example. |
@Merovius, good point about using the sequence multiple times. The
@meling, I'm not sure what this refers to. What boilerplate? |
I am a bit confused by the examples. Isn't this an issue of nested iterations? I thought the error returning function could perhaps be a good idea until I saw how it would be used and it made me think. I guess my question is whether the error is about the iterator failing (?) , which should not be an error but a fault/panic probably, or the value returned by the iterator being wrong? Said otherwise, is it possible to consider that iterators never fail while it is possible that some of them just return erroneous values (the error value being used as a sentinel)? For some errors, as soon one such error is returned, while designing the iterator, one might decide that this error is always returned so as to indicate that the object of the iteration is in an error state, or one might decide to make the iterator fault-tolerant? In which case the initial error returning code would be fine? Just check for the error within the loop and either continue or break? re. ItemJust thinking that maybe, you might want to compose your generic constructors to have specific maps with specific types that implement your desired interface. Just an idea, not sure it works. |
Interesting. Let me be the first to say, welcome to |
I have just such type which I wrote in 2022 or earlier There is certainly smarter people than me on this thread, but what I can provide is years of experience with trying to get the logic right when it comes to iterators. I wanted them because ECMAScript had them and Go didn’t. I knew how great iterators are https://pkg.go.dev/github.com/haraldrudell/parl@v0.4.183/pfs#ResultEntry It is returned as a single tuple-value-type value like suggested above and has value-receiver methods to avoid allocations It is used to allow the consuming code to decide whether iteration should continue or not, no matter how bad it went. It provides two-way communication with the iterator internals What you are suggesting is possible and I am already doing it. 99% if the time it isn’t worth it, but I coded it up. The value it has is that you can decide you want to look into the red ones but not the blue ones That is a different problem from the iterator unilaterally deciding it is a fail. The error pointer makes implementing such decision dead-simple, reliable and cost-free. You can’t beat free |
@haraldrudell not bad. Still unsure about the error value pointer. How is it simpler than assigning to an error variable in the loop body and breaking? Assuming that the loop iteration returns a pair e.g. func checkEverything() (err error) {
for v, Err := range select.Star() {
if Err != nil{
err = Err
break
}
// ...
}
return err
} Is there a problem that I'm not seeing? |
As I keep using these, it feels very very wrong that range over some types has one rule and range over other types has a different rule. The Go 1.23 release candidate is upon us. I think it seems clear we should remove the special case for range-over-func. |
Based on the discussion above, this proposal seems like a likely accept. The proposal is to make range-over-func the same as range-over-anything else as far as omitting range variables is concerned. So if you have an iter.Seq2[X, Y] you can write
just like if seq2 were a map. (Today because seq2 is a func the language insists you write Same for iter.Seq[X] and (It is okay to submit this change for the release candidate even while the issue is only “likely accept”.) |
Change https://go.dev/cl/592176 mentions this issue: |
Change https://go.dev/cl/592295 mentions this issue: |
For #65236. Change-Id: I63e57c1d8e9765979e9e58b45948008964b32384 Reviewed-on: https://go-review.googlesource.com/c/go/+/592176 Reviewed-by: Robert Griesemer <gri@google.com> Reviewed-by: Robert Findley <rfindley@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Robert Griesemer <gri@google.com>
For #65236. Change-Id: I5a11811cc52467ea4446db29c3f86b119f9b2409 Reviewed-on: https://go-review.googlesource.com/c/go/+/592295 Reviewed-by: Robert Griesemer <gri@google.com> Auto-Submit: Robert Griesemer <gri@google.com> TryBot-Bypass: Robert Griesemer <gri@google.com> Reviewed-by: Ian Lance Taylor <iant@google.com>
A nice side-effect of this is that we won't have to fix |
Not a release blocker at this point. The feature has been implemented and documented but still needs approval (likely to come on Thu 6/20). Leaving open until then. |
No change in consensus, so accepted. 🎉 The proposal is to make range-over-func the same as range-over-anything else as far as omitting range variables is concerned. So if you have an iter.Seq2[X, Y] you can write
just like if seq2 were a map. (Today because seq2 is a func the language insists you write Same for iter.Seq[X] and (It is okay to submit this change for the release candidate even while the issue is only “likely accept”.) |
Change https://go.dev/cl/594555 mentions this issue: |
This reverts the functional change of commit 3629652 (CL 588056) that caused range-over-func to differ from other range operands; but we keep some tests and tidy up the logic. It was decided at the 11th hour to permit redundant blanks in range-over-func statements in the go1.23 spec; see golang/go#65236 (comment). Fixes golang/go#67239 Updates golang/go#65236 Change-Id: Ib3c1c535a1107a05f18732e07d7c8844bbac4d1e Reviewed-on: https://go-review.googlesource.com/c/tools/+/594555 Reviewed-by: Robert Findley <rfindley@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Change https://go.dev/cl/596095 mentions this issue: |
Change https://go.dev/cl/596135 mentions this issue: |
This proposal has been implemented and documented. Closing. |
This adds a test for for range seq2rangefunc { ... } and for onevar := range seq2rangefunc { ... } For #65236. Change-Id: I083f8e4c19eb4ba0d6024d5314ac29d941141778 Reviewed-on: https://go-review.googlesource.com/c/go/+/596135 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Keith Randall <khr@google.com> Reviewed-by: Keith Randall <khr@golang.org>
In discussion during the implementation of #61405 we changed range-over-func to require mentioning iteration variables. The idea is that if we do end up with idioms like:
Then we want to diagnose:
as an error. However, this is inconsistent with other range loops and also not what the #61405 text said. Probably we should change it. Starting a separate proposal for that discussion.
The text was updated successfully, but these errors were encountered: