Skip to content

proposal: Go 2: add a new iterator syntax, package, interfaces #54047

Closed
@earthboundkid

Description

@earthboundkid

Author background

  • Would you consider yourself a novice, intermediate, or experienced Go programmer?

Experience Go programmer.

  • What other languages do you have experience with?

JavaScript, Python.

Related proposals

  • Has this idea, or one like it, been proposed before?
    • If so, how does this proposal differ?

#20733 and #24282 address improving for loops. #40605, #43557, and #50112 address iterators. #17747, #34544, and other have added vet methods to ensure erors are checked on close. This proposal differs by using the range keyword to make the change more clear than reusing for, differentiating values, key values, and results, and by having a mandatory close mechanism that forces error handling, making the vet check less necessary.

  • Does this affect error handling?

No.

  • Is this about generics?

It uses the implemented design and makes various future patterns possible.

Proposal

  • What is the proposed change?

The following types would be added to the standard library in an iterators package (🚲 iters?).

type Value[T any] interface {
    Next() bool
    Value() T
    Close()
}

type KeyValue[K, V any] interface {
    Next() bool
    Value() (K, V)
    Close()
}

type Result[T any] interface {
    Next() bool
    Value() T
    Close() error
}

type KeyValueResult[K, V any] interface {
    Next() bool
    Value() (K, V)
    Close() error
}

type ValueIterable[T any] interface {
    Iter() Value[T]
}

type KeyValueIterable[K, V any] interface {
    Iter() KeyValue[K, V]
}

type ResultIterable[T any] interface {
    Iter() Result[T]
}

type KeyValueResultIterable[K, V any] interface {
    Iter() KeyValueResult[K, V]
}

The language spec would be changed to allow for loop statements of the following type:

// Suppose bufio.Scanner has been updated to have an Iter() iterators.Result[[]byte] method

s := bufio.NewScanner(os.Stdin)
range s : b : err {
    fmt.Println("line:", string(b))
    if bytes.Contains(b, []byte("done")) {
        break // scanner.Close() is automatically called here
    }
} // scanner.Close() would also be automatically called here
// err is now a equal to s.Err()
if err != nil { /* ... */ }

// Suppose there is an iterators.Slice function
s := getsomeslice()
range iterators.Slice(s) : row {
    go use(row) // row is closure safe
}

// Suppose there is an iterators.Enumerate 
// that converts ValueIterable to KeyValue iterables
s := getsomeslice()
range iterators.Enumerate(iterators.Slice(s)) : i, row {
    go use(i, row) // i and row are both closure safe
}

// One can also imagine helpers to promote the other types 
// to a KeyValueResult iterator with the key being i and err being nil.

// interval could be in iters package or written by hand
range interval(0, n): i {
    go doSomething(i)
}

Because all four iterator types have Value and Close methods, they are mutually exclusive for a type. As per #20733, the range block reassigns Value() on each loop. The Result and KeyValueResult types take an extra argument separated by a colon which only comes into scope after the loop is exited. (Open question: does Close() run after a panic?)

  • Who does this proposal help, and why?

It makes a wide variety of iterator patterns easier to use. It is compatible with existing designs in the standard library. It solves the problem of users accidentally closing over for loop variable or forgetting to check the final error in a result iterator type. It allows for the creation of iterator libraries that iterate over chunks, subsets, permutations, etc.

  • Please describe as precisely as possible the change to the language.

See above.

  • What would change in the language spec?

See above. Subject to further discussion.

  • Please also describe the change informally, as in a class teaching Go.

Go has a way of writing your own types that can be iterated over. First add method Iter() iterators.Value[myStruct] to our struct that just returns itself. Now we can use myStruct like this:

s := NewMyStruct()
range s : row {
   use(row)
}
  • Is this change backward compatible?

I believe so. That's why it uses range instead of for. I'm not committed to this color of the bikeshed if someone can make it work with for but still be clearly readable.

  • Show example code before and after the change.
    • Before
    • After

N/A because this is a new feature. Open question: should existing iterable types be magically opted into working with range statements? I think probably not.

  • Orthogonality: how does this change interact or overlap with existing features?

It adds a new way to iterate, which is unforunate. On the other hand, it fixes two long standing sources of bugs: not realizing a loop reuses variables and not calling .Err() after scanning a row from bufio or sql.

  • Is the goal of this change a performance improvement?

No.

Costs

  • Would this change make Go easier or harder to learn, and why?

Harder because there is another form of iteration. But it makes it easier to write bug free code that uses an iterator.

  • What is the cost of this proposal? (Every language change has a cost).

Largely the cost is the introduction of a new concept.

  • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?

They would need to know the new loop syntax.

  • What is the compile time cost?

Low. There would need to be a type analysis to ensure that the right type of iterator is used, e.g. you can't use a Result as a Value, etc.

  • What is the run time cost?

Low.

  • Can you describe a possible implementation?

TBD.

  • Do you have a prototype? (This is not required.)

No.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions