Skip to content

spec: add range over int, range over func #61405

Closed
@rsc

Description

@rsc

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 through n-1 are produced, with the same type as n. If n <= 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 calling f with a synthesized yield function that invokes the body of the loop. The values produced correspond to the arguments in successive calls to yield. As with range over other types, it is permitted to declare fewer iteration variables
than there are iteration values. The return value from the yield function reports whether f should continue iterating. For example, if the loop body executes a break statement, the corresponding call to yield returns false. The return value from f is ignored by range but conventionally returns false when yield has returned false, to allow composing a sequence of such functions. The use of a synthesized yield function does not change the semantics of the loop body. In particular, break, continue, defer, goto, and return 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.)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions