-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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
proposal: Go 2: spec: for range with defined types #47707
Comments
@Merovius Building on your example, could defers work something like this? Assuming the for v := range x {
if cond1 { break }
if cond2 { continue }
if cond3 { return a }
if cond4 { defer z.Push(v) }
} var (
_tmp_ret bool
_tmp_a A
_tmp_defers []?
)
x.Do(func(v B) bool {
if cond1 { return false }
if cond2 { return true }
if cond3 {
_tmp_a, _tmp_ret = a, true
return false
}
if cond4 {
_tmp_defers = append(_tmp_defers, ?z.Push(v)?)
}
})
if _tmp_ret {
return _tmp_a
}
for i := len(_tmp_defers)-1; i >= 0; i-- {
defer ?_tmp_defers[I]?
} |
Personally, I don't really see the need to make So, personally, I would rather if we focus on using iterators for a generic iteration interface, gain some experience with what methods are common and what we do need from them. And then, maybe, make a subset of them usable with That being said, I'm not strongly opposed to this proposal. One thing the proposal text doesn't answer is, what would happen in this code: type Ranger struct {
f func() bool
}
func (r *Ranger) Do(f func(v int) bool) {
r.f = f
return
}
func F() func() bool {
r := new(Ranger)
for _ = range r {
defer func() { fmt.Println("Hello world") } ()
}
return r.f
}
func main() {
f := F()
f() // ???
} This uses
I think for illustrative purposes, that is just |
We need to consider how to handle |
This doesn’t show what any iterators might look like, which is where the fault comes in. Here is an example of what an iterator would look like:
The issue with the Also, as I mentioned in #43557 (comment) it would be much better to have a github discussion for this instead of multiple competing proposals. |
This also only supports iterating structures, whereas I may want an iterator which is defined at the top-level, like:
It is also worth mentioning that @DeedleFake had already proposed (and I supported) a similar concept in #43557 (comment), which was a more-flexible version of this proposal, but hadn’t thought of an The main issue that I highlighted earlier had already been brought up in the other proposal (however I translated “issue with the proposal” to “issue when we forget to check the |
@Merovius I think this is a good point. Iterators are very flexible and they address the problem of how to control the current scope well during iteration. The sql.Rows example is a little different in that it is a scanning pattern which is not quite the same as iterating over known types and values, but that is definitely evidence of that patterns flexibility. The If generics mean that there will be a shift towards defined types, such as |
@Merovius I think in this case the rewritten function is captured which captures the scope of the function containing the for loop like it would if the user referenced variables in that scope inside the closure. That would be odd, but storing a function inside an iterator like you demonstrated is likely to have strange side-effects in any situation. Would you expect a function stored in that case to work differently? I think the main oddity is that defers could be lifted, and you could set state that affected If Go had a |
@ianlancetaylor I think the go-to case is workable by setting a temp bool and exiting early. For example: Label:
// ...
for v := range x {
if cond1 { break }
if cond2 { continue }
if cond3 { return a }
if cond4 { defer z.Push(v) }
if cond5 { goto Label }
} var (
_tmp_ret bool
_tmp_a A
_tmp_defers []?
_tmp_goto bool
)
x.Do(func(v B) bool {
if cond1 { return false }
if cond2 { return true }
if cond3 {
_tmp_a, _tmp_ret = a, true
return false
}
if cond4 {
_tmp_defers = append(_tmp_defers, ?z.Push(v)?)
}
if cond5 {
_tmp_goto = true
return false
}
})
for i := len(_tmp_defers)-1; i >= 0; i-- {
defer ?_tmp_defers[I]?
}
if _tmp_ret {
return _tmp_a
}
if _tmp_goto {
goto Label
} |
You seem to be ignoring the This wouldn't be a problem if @ianlancetaylor brought up func type Ranger struct {
f func() bool
}
func (r *Ranger) Do(f func(v int) bool) {
r.f = f
return
}
func F() func() bool {
r := new(Ranger)
for _ = range r {
goto Foo
}
Foo:
fmt.Println("Hello world")
return r.f
}
func main() {
f := F()
f()
} would the call to This isn't just an issue of closing over variables. |
From the first sentence of the first comment in this issue:
There are many possible designs to make this work. But this issue only explores using "Do", which has some issues. Would proposing alternatives be appropriate here or in another proposal? (For example, I'm thinking Ranger like this:
|
I think it's easier to discuss different ideas in different issues. We can still cross reference them. Could you open a new issue if you have an alternative proposal?
@wsc0 This interface only satisfies iterating with an index and an element. For supports iterating with other values, the spec refers to them as iteration variables and they can be different depending on the type being ranged over. For example, how would a type that operates like a channel work with it since it iterates with no index? |
I just took a look at #43557, which has a nice overview of different ideas, and made me realise having a separate Iter type is more verbose. I was indeed thinking of something similar to #43557 , so I'll not open another issue. Of course, the single value return would need to be treated differently -- that was just a sketch. Thanks for pointing it out nonetheless. |
Something else that this proposal seems to leave out is how to pass around iterators. For instance, how would I write the equivalent of |
type Collection[Elem any] interface {
Do(func(Elem) bool)
}
type mapper[T, U any] struct {
c Collection[T]
f func(T) U
}
func (m mapper[T, U]) Do(f func(U) bool) {
m.c.Do(func(t T) bool {
return f(m.f(t))
})
}
func Map[T, U any](c Collection[T], f func(T) U) Collection[U] {
return mapper{c, f}
} (I called it |
@Merovius My bad - the example I was thinking of where this would have trouble wasn't a map operation, but a zip operation. The issue wasn't being able to pass around the iterator, but being able to iterate over two things at the same time. I think that it would not be possible without goroutines.
Collection is still a bit weird considering that a mapper isn't really a Collection... Same thing for infinite "rangers" like sequences. In Java this concept is named Iterable. |
I agree that zipping seems hard without goroutines. |
As this was just brought to my attention again:
This doesn't work, no. Consider for x := range c {
defer fmt.Println("foo")
panic(nil)
} This should print "foo" for a non-empty container and then panic. But with that rewrite, it won't print anything, as it panics before the I know that I claimed that this kind of rewrite would work, but after some time and all this discussion, I really think it's at the very least extremely complex. I would go so far as to say I was mistaken to suggest it. |
If you add a defer prior to for v := range x {
if cond1 { break }
if cond2 { continue }
if cond3 { return a }
if cond4 { defer z.Push(v) }
if cond5 { goto Label }
} var (
_tmp_ret bool
_tmp_a A
_tmp_defers []func()
_tmp_goto bool
)
defer func() {
for fn := range _tmp_defers {
defer fn()
}
}()
x.Do(func(v B) bool {
if cond1 { return false }
if cond2 { return true }
if cond3 {
_tmp_a, _tmp_ret = a, true
return false
}
if cond4 {
_tmp_defers = append(_tmp_defers, func() { z.Push(v) })
}
if cond5 {
_tmp_goto = true
return false
}
})
if _tmp_ret {
return _tmp_a
}
if _tmp_goto {
goto Label
} Handling deferred functions in a deferred function ensures it always runs, regardless of panics/etc. Iterating over the slice normally and using defer ensures A) deferred calls are executed in the expected order (LIFO) and B) subsequent deferred calls are still executed if one call panics. Of course the runtime could potentially do some trickery to make it more efficient. |
I do not yet have a good understanding of how these suggestions interact with https://go.dev/ref/spec#Handling_panics . Specifically:
How are we handling the additional indirection created by Example to illustrate what I am getting at does: https://go.dev/play/p/Wa8VXAAW1ts |
I’m assuming the compiler and runtime would do something smarter than what I wrote. I’m not suggesting that the compiler literally generate what I wrote, but I find it easier to reason about valid Go instead of “insert compiler magic here”. |
Here is a crack at handling Need a slightly more complex example to illustrate.
This can be rewritten as:
I thought this would require continuations when I first saw thought of how to deal with defers, but maybe the save the panicing value and deferred functions is sufficient? |
The problem with deferring in the for-loop after I don't think there's any need to rewrite panic. A panic within the callback will work exactly as expected, as long as |
Fair point about x.Do panicking. My suggestion is not correct for that case. I am not sure I see a problem about the callback yet.
What I was trying to address with my suggestion is ordering the defer statements, such that recover can be handled correctly. Spec: "recover was not called directly by a deferred function." So it is not quite enough to re-execute the same deferred functions with the same arguments in the correct order. Which function stack matters too. This was why I was saving the deferred functions and executing them in the original function body. |
Here is a variant of the previous that uses a defer before x.Do, and within this it defers the _temp_defers and then recovers and re-raises a panic if type _tmp_defer_loc1 struct{ f: func(V); arg0: V }
type _tmp_defer_loc2 struct{ f: func() }
var (
// ...
_tmp_defers []any
)
defer func() {
for i := range _tmp_defers {
switch d := _tmp_defers[i].(type) {
case _tmp_defer_loc1:
defer d.f(d.arg0)
case _tmp_defer_loc2:
defer d.f()
}
}
if r := recover(); r != nil { // not quite right. will stop unwinding panic(nil) prematurely. may need help from runtime.
panic(r)
}
}()
x.Do(func(v B) bool {
// ... You can try playing around the idea of re-raising the panic in the defer https://go.dev/play/p/zmXP_MVAM5U. It seems to be needed to achieve the correct order. This feels close, but I don't think it is not quite right yet. The panicking unwinding order does not seem correct in all cases yet. |
Closing in favor of #43557. |
The proposal is to allow any defined type to be iterated on using the
for
withrange
clause if that type loosely implements the following interface. ItsDo
function is called with the body of thefor
block as the argument of the function parameter, where breaks and returns inside the for block are translated into returning false inside the function, and continues are translated into returning true.The parameters of
f
are not defined because they can be zero or more parameters of any types, and those parameters would map to being iteration variables of thefor
block.This would allow user-defined types to be iterated with a
for
block. The most relevant and useful example is thecontainer/set
type currently being discussed in #47331:Another trivial example is types such as a numerical range or interval:
Why
User-defined types cannot be iterated in a way that retains control of the current scope using the
return
,break
,continue
keywords. This results in two approaches to iteration being used for built-ins vs defined types. This is already evident in discussions about how thecontainers/set
package may be used and will become more relevant as folks write more collection types once generics are released.For example, to search a Set type for an element and exit early, code like the below would need to be written:
That code is significantly harder to understand than if the Set type supported iteration using this proposal:
Prior Discussion
I can't take credit for the ideas that form this proposal. As far as I know the ideas were first suggested by @rsc and further elaborated on by @Merovius. The ideas were shared in response to the problem of how do we make iteration of new generic data structure types, like
container/set
, as simple as built-in types that can usefor
range
.-- @rsc #47331 (reply in thread)
-- @Merovius #47331 (reply in thread)
Further Details
This proposal changes the Go spec's description of the
range expression
so that in addition to the range expression being allowed to be an array, pointer to an array, slice, string, map, or channel permitting receive operations, it may also be any defined type that has aDo
function with onefunc
parameter discussed below, and a singlebool
return value.If a value that is a defined type is provided as a range expression and does not have a
Do
function, it is a compiler error.If a value that is a defined type is provided as a range expression and it has a
Do
function, the onefunc
argument of theDo
function is called for each iteration. The body of thefor
withrange
block becomes thefunc
argument given for the onefunc
parameter of theDo
function.In the same way that a
for
withrange
may have zero or more iteration variables up to the number of iteration values for the built-in types, afor
withrange
of a defined type may also have zero or more iteration variables up to the number of parameters that theDo
function's signature defines in its onefunc
parameter.In the same way that iterations are defined by the built-in types to mean different things for slices, maps, channels, etc, a defined type also defines an iteration for itself the spec doesn't limit or constrain how a defined type might iterate. For example, a
container/set
-like type may iterate for each element of the set. Or, a type representing a numerical range or interval, may iterate for each interval in the range with some defined step.The
bool
return value of theDo
function signals if iteration should continue.break
s orreturn
s in afor
withrange
block are translated into returning false inside the function passed to theDo
function and setting necessary temporary variables to communicate back the intention to return. See @Merovius's example above.Proposal template
Would you consider yourself a novice, intermediate, or experienced Go programmer?
Experienced
What other languages do you have experience with?
Java, Ruby, C#, C, JavaScript
Would this change make Go easier or harder to learn, and why?
A little harder since there's one more thing to learn, that implementing the interface supports iteration.
Has this idea, or one like it, been proposed before? If so, how does this proposal differ?
I couldn't find a proposal issue that was identical. There are some proposals that attempt to do similar things:
Next
function that can be called repeatedly, but it is less adaptable to different types of iterables since it is limited to a single element/item being returned on each iteration.Who does this proposal help, and why?
Anyone building data structures that can be iterated, and anyone using them so that their iteration code is simple and much the same to iterating any built-in type.
What is the proposed change?
See above.
Please describe as precisely as possible the change to the language.
See above.
What would change in the language spec?
See above.
Please also describe the change informally, as in a class teaching Go.
See above.
Is this change backward compatible?
Yes
Show example code before and after the change.
See above.
What is the cost of this proposal? (Every language change has a cost).
I'm not sure.
Little.
None.
Can you describe a possible implementation?
See above.
No.
How would the language spec change?
See above.
Orthogonality: how does this change interact or overlap with existing features?
It builds on the existing for range semantics without changing existing semantics. It supports new data structures such as
container/set
such that they may be iterated in the same way as built in types, maintaining consistency for use.Is the goal of this change a performance improvement?
No.
Does this affect error handling?
No.
Is this about generics?
No. But related, because generics appear to be driving general data structures, and that's raising the issue of iteration.
The text was updated successfully, but these errors were encountered: