Description
The proposal is to allow any defined type to be iterated on using the for
with range
clause if that type loosely implements the following interface. Its Do
function is called with the body of the for
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.
type Rangeable interface {
Do(f func(...) bool)
}
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 the for
block.
This would allow user-defined types to be iterated with a for
block. The most relevant and useful example is the container/set
type currently being discussed in #47331:
package sets
type Set[Elem] struct {
m map[Elem]struct{}
}
func (s Set) Do(f func(e Elem) bool) {
for k := range s.m {
stop := f(k)
if stop {
return
}
}
}
func main() {
s := sets.Set{}
// ...
for e := range s {
// ...
}
}
Another trivial example is types such as a numerical range or interval:
package rng
type Interval struct {
Start int
Finish int
Step int
}
func (i Interval) Do(f func(v int) bool) {
for j := i.Start; j < i.Finish; j+=i.Step {
stop := f(j)
if stop {
return
}
}
}
func main() {
r := &rng.Interval{Start: 0, Finish: 100; Step: 2}
for i := range r {
// ...
}
}
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 the containers/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:
func _() (Elem, bool) {
set := sets.Set{}
// ...
found := false
var foundE Elem
set.Do(func(e Elem) bool {
if condition {
found = true
foundE = e
return false
}
return true
})
return foundE, found
}
That code is significantly harder to understand than if the Set type supported iteration using this proposal:
func _() (Elem, bool) {
set := sets.Set{}
// ...
for e := range set {
if condition {
return e, true
}
}
return Elem{}, false
}
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 use for
range
.
That said, if we were going to allow the use of range syntax on user-defined types, the best way I can see to do it would be to compile the syntax into a call to the Do function.
-- @rsc #47331 (reply in thread)
AUI such an implementation would rewrite
for v := range x { if cond1 { break } if cond2 { continue } if cond3 { return a } }into something akin to
var ( _tmp_ret bool _tmp_a A ) 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 _tmp_ret { return _tmp_a }I think you'll find that this construct could do everything a normal
range
loop could do.
-- @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 a Do
function with one func
parameter discussed below, and a single bool
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 one func
argument of the Do
function is called for each iteration. The body of the for
with range
block becomes the func
argument given for the one func
parameter of the Do
function.
In the same way that a for
with range
may have zero or more iteration variables up to the number of iteration values for the built-in types, a for
with range
of a defined type may also have zero or more iteration variables up to the number of parameters that the Do
function's signature defines in its one func
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 the Do
function signals if iteration should continue. break
s or return
s in a for
with range
block are translated into returning false inside the function passed to the Do
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:- proposal: Go 2: iterators #40605 - Provides a
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. - proposal: Go 2: function values as iterators #43557 - Allow functions to be a range expression and used for iteration, however if a type is iterable, it seems clearer and easier to understand if the type itself can be passed to the range expression rather than a function generated by the type.
- proposal: Go 2: iterators #40605 - Provides a
-
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- Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit.
-
Show example code before and after the change.
See above. -
What is the cost of this proposal? (Every language change has a cost).
- How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
I'm not sure. - What is the compile time cost?
Little. - What is the run time cost?
None.
- How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
-
Can you describe a possible implementation?
See above.- Do you have a prototype? (This is not required.)
No.
- Do you have a prototype? (This is not required.)
-
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 ascontainer/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.- If so, what quantifiable improvement should we expect?
- How would we measure it?
-
Does this affect error handling?
No.- If so, how does this differ from previous error handling proposals?
-
Is this about generics?
No. But related, because generics appear to be driving general data structures, and that's raising the issue of iteration.- If so, how does this differ from the the current design draft and the previous generics proposals?