Description
Following discussion on #56413, I propose to add two new types that a for-range statement can range over: integers and functions.
In the spec, the table that begins the section would have a few more rows added:
Range expression 1st value 2nd value
array or slice a [n]E, *[n]E, or []E index i int a[i] E
string s string type index i int see below rune
map m map[K]V key k K m[k] V
channel c chan E, <-chan E element e E
integer n integer type index i int
function, 0 values f func(func()bool) bool
function, 1 value f func(func(V)bool) bool value v V
function, 2 values f func(func(K, V)bool) bool key k K v V
Range over integer
If n is an integer type, then for x := range n { ... }
would be completely equivalent to for x := T(0); x < n; x++ { ... }
, where T
is the type of n
(assuming x is not modified in the loop body).
The additional spec text for range over integer is:
For an integer
n
, the index iteration values 0 throughn-1
are produced, with the same type asn
. Ifn
<= 0, the loop does not run any iterations.
In the Go code I have examined from the main repo, approximately half of existing 3-clause for loops can be converted to range over integer. Loops that don't care about the index variable become even shorter. For example, the canonical benchmark iteration becomes:
for range b.N {
do the thing being benchmarked
}
3-clause for loops certainly have their place in Go programs, but something as simple as counting to n
should be easier. The combination of range over integer and range over functions should make range the for loop form of choice for almost all iteration. When you see a 3-clause loop, you'll know it is doing something unusual and know to examine it more carefully.
Range over function
If f
is a function type of the form func(yield func(T1, T2)bool) bool
, then for x, y := range f { ... }
is similar to f(func(x T1, y T2) bool { ... })
, where the loop body has been moved into the function literal, which is passed to f
as yield
. The boolean result from yield
indicates to f
whether to keep iterating. The boolean result from f
itself is ignored in this usage but present to allow easier composition of iterators.
I say "similar to" and not "completely equivalent to" because all the control flow statements in the loop body continue to have their original meaning. In particular, break, continue, defer, goto, and return all do exactly what they would do in range over a non-function.
Both T1 and T2 are optional: func(yield func(T1)bool) bool
and func(yield func()bool) bool
can both be iterated over as well. Just like any other range loops, it is permitted to omit unwanted variables. For example if f has type func(yield func(T1, T2) bool) bool
any of these are valid:
for x, y := range f { ... }
for x, _ := range f { ... }
for _, y := range f { ... }
for x := range f { ... }
for range f { ... }
The additional spec text for range over function is:
For a function
f
, the iteration proceeds by callingf
with a synthesizedyield
function that invokes the body of the loop. The values produced correspond to the arguments in successive calls toyield
. As with range over other types, it is permitted to declare fewer iteration variables
than there are iteration values. The return value from theyield
function reports whetherf
should continue iterating. For example, if the loop body executes abreak
statement, the corresponding call toyield
returns false. The return value fromf
is ignored byrange
but conventionally returns false whenyield
has returned false, to allow composing a sequence of such functions. The use of a synthesizedyield
function does not change the semantics of the loop body. In particular,break
,continue
,defer
,goto
, andreturn
statements all behave in a loop body ranging over a function exactly as they do in a loop body ranging over other types.
Being able to range over a function enables writing range over user-specified iteration logic,
which should simplify many programs. It should also help make use of custom collection types nicer.
Not all iteration a program wants to do fits into a single for loop. To convert one of these functions to a "next-based" or "pull-based" iterator, we plan to add a function to the standard library that runs the yield-based ("push-based") iterator in a coroutine, along the lines of the coro.Pull
function in my recent blog post. The specific name of those functions will be decided in a future proposal; this proposal is limited to language changes, not library changes.
One of the problems identified in the discussion of #56413 was having too many different possible functions to range over. For that reason, this proposal drops the possibility of funcs that do not return bool as well as funcs of the form func() (T, bool)
("pull-iterators"). Push iterators are the standard form with language support and that packages should provide, and something like coro.Pull
will allow conversion to pull iterators in code that needs that form.
Implementation
I have implemented this proposal so that we can explore it and understand it better. To run that prototype implementation:
go install golang.org/dl/gotip@latest
gotip download 510541 # download and build CL 510541
gotip version # should say "(w/ rangefunc)"
gotip run foo.go
and then use the Go toolchain you just built. If you already have Go checked out and use the codereview plugin, you can:
git change 510541
cd src
./make.bash
(Or git codereview change if you don't have the standard aliases set up.)