🔁

Goの1.22にGOEXPERIMENTガード下で導入されるrange over func proposalを試してみる

2023/12/04に公開

Goのrange over func proposal

https://github.com/golang/go/issues/61405

上記のproposalで述べられているrange over funcを試し見てみます。

対象読者

  • The Go Programming Languageに対してある程度の習熟している
  • ユーザーがカスタマイズできるfor range loopに興味があるけどこういうproposalが出てたのは知らなかった。もしくは
  • 上記のproposalのinterfaceの感触が気になってるけど自分で実装してみるほどじゃない

この記事のすること

  • 上記issueの筆者が気になったところを取り上げます
  • このproposalで述べられている新しい構文を試します。
    • sliceやmapからこのiterator funcを作る関数を書いてみます
    • iterator funcから得られる値を加工するadapter funcを書きます
      • e.g. filter, map

Proposalの概要

以下のように、

  • func(func()bool)
  • func(func(V)bool)
  • func(yield func(k K, v V) bool)

for-rangeで消費できるようにするというproposalです

package main

import (
	"fmt"

	"github.com/ngicks/go-iterator-helper/iteratorhelper"
)

func main() {
	var iter func(yield func(k int, v string) bool)
	iter = iteratorhelper.SliceIter([]string{"foo", "bar", "baz", "qux", "quux"})
	var adaptedIter func(yield func(k []int, v []string) bool)
	adaptedIter = iteratorhelper.Chunk(iter, 2)

	for k, v := range adaptedIter {
		fmt.Printf("k = %#v, v = %#v\n", k, v)
	}
}
/*
k = []int{0, 1}, v = []string{"foo", "bar"}
k = []int{2, 3}, v = []string{"baz", "qux"}
k = []int{4}, v = []string{"quux"}
*/

signature

上記リンクトップのコメントではspec textに以下を追加すると述べています。
実際にはリンク先にはfunc(func()bool) boolという感じでboolの返り値がついているんですが、gotipで動かせる現状版だと下記が正しいです。コメントでも、proposalからこのboolの返り値が取り除かれていないのは誤りであると述べられています。

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)
function, 1 value   f  func(func(V)bool)           value    v  V
function, 2 values  f  func(func(K, V)bool)        key      k  K      v          V

integer以下が追加行です。range over integerはこの記事ではスコープから外します。

3つのシグネチャがあります。おそらく下二つはslice, mapをrange overすることを念頭に置いたsignatureでしょう。

これらのiterator funcは、 for rangeで消費することができます。

for x, y := range f { ... }
for x, _ := range f { ... }
for _, y := range f { ... }
for x := range f { ... }
for range f { ... }

実際にはこれらは

for x, y := range f { ... }

f(func(x T1, y T2) bool { ... })

にrewriteすることで動作を実現します。

プロトタイプ実装

gotipを使うと実行できます。

go install golang.org/dl/gotip@latest
gotip download 510541
GOEXPERIMENT=range gotip run ...

Discussion Summary / FAQ の気になるところ(2023/07/23時点)

https://github.com/golang/go/issues/61405#issuecomment-1638896606

このissueコメントでモチベーション、その他もろもろが述べられていますが、そのうち筆者が気になったものだけピックアップします

このコメントは2023-07-23から現在(2023-12-04)まで更新されていません。

What if the iterator function ignores yield returning false? / What if the iterator function saves yield and calls it after returning?

  • 現在のプロトタイプ実装では特にチェックされない
    • チェックするとインライン化がうまくいかないため
  • 標準化の際にはパフォーマンスに過度な影響が出ないようにチェックする必要がある
    • 間違った使用法でpanicするように

What if the iterator function calls yield on a different goroutine?

  • まだ決まってない
    • Goはgoroutineを特定できないようにしてきたので、yieldだけ「iteratorが呼びだれたgoroutineから呼ばれなければならない(must be called)」としてしまうのは変
    • iteratorを呼び出したgoroutine以外でyieldを呼ぶとpanicするというのはさらに変だが、だとしてもそうする価値があるかもしれない。

What happens if the iterator function recovers a panic in the loop body?

  • まだ決まってない
    • 現在のプロトタイプはrecover後もう一度yieldを呼んで処理を続けることを許す
      • これはGoroutineの挙動とは似通っていない
      • もしかしたらこれは間違いかもしれないが、だとしても、効率的にただすことが難しい。
  • 継続的に検討する

Can range over a function perform as well as hand-written loops?

はい。

このループは

for i, x := range slices.Backward(x) {
	fmt.Println(i, x)
}

以下にダイレクトに変換され、

slices.Backward(s)(func(i int, x string) bool {
	fmt.Println(i, x)
	return true
})

slices.Backwardが小さいため以下のようにインライン化され

func(yield func(int, E) bool) bool {
	for i := len(s)-1; i >= 0; i-- {
		if !yield(i, s[i]) {
			return false
		}
	}
	return true
}(func(i int, x string) bool {
	fmt.Println(i, x)
	return true
})

関数リテラルが即座に呼ばれていることがわかるのでインライン化され、

{
	yield := func(i int, x string) bool {
		fmt.Println(i, x)
		return true
	}
	for i := len(s)-1; i >= 0; i-- {
		if !yield(i, s[i]) {
			goto End
		}
	}
End:
}

この場合yieldがdevirtualizeできる

{
	for i := len(s)-1; i >= 0; i-- {
		if !(func(i int, x string) bool {
			fmt.Println(i, x)
			return true
		})(i, s[i]) {
			goto End
		}
	}
End:
}

さらに関数リテラルがインライン化できる

{
	for i := len(s)-1; i >= 0; i-- {
		var ret bool
		{
			i := i
			x := s[i]
			fmt.Println(i, x)
			ret = true
		}
		if !ret {
			goto End
		}
	}
End:
}

この時点で、SSAバックエンドが不必要な変数などを判定し、上記のコードを以下と同等に扱う

for i := len(s)-1; i >= 0; i-- {
	fmt.Println(i, s[i])
}

この最適化は閾値以下の単純なloop bodyと単純なiteratorに対してのみおこなわれる。逆に複雑なloop bodyやiteratorに対しては関数呼び出しのオーバーヘッドは重大ではない。

現状

実装

実際にproposalの構文でiteratorやadapterを実装してみてgotipで実行してみましょう。

上記iterおよびx/exp/xiterのproposalで基本的なアダプター類の実装例は述べられています。以下の実装はそれらと重複するところがあります。

書いたコードはこのrepositoryに入っています

https://github.com/ngicks/go-iterator-helper

base

元となるデータソースからiteratorを生成します。

proposalが通過し、周辺ライブラリが追従するころにはslicesmapsパッケージでsliceやmapからiteratorを生成する関数は提供されるでしょうから自分で実装する必要はないと思いますが、実験として必要なので実装します。

ここは特にいうことはないですね。

func SliceIter[T ~[]E, E any](sl T) func(yield func(k int, v E) bool) {
	return func(yield func(k int, v E) bool) {
		for idx, v := range sl {
			if !yield(idx, v) {
				return
			}
		}
	}
}

func SliceIterSingle[T ~[]E, E any](sl T) func(yield func(v E) bool) {
	return func(yield func(v E) bool) {
		for _, v := range sl {
			if !yield(v) {
				return
			}
		}
	}
}

func MapIter[K comparable, V any](m map[K]V) func(yield func(k K, v V) bool) {
	return func(yield func(k K, v V) bool) {
		for k, v := range m {
			if !yield(k, v) {
				return
			}
		}
	}
}

func ChanIter[V any](ch <-chan V) func(yield func(V) bool) {
	return func(yield func(V) bool) {
		for v := range ch {
			if !yield(v) {
				return
			}
		}
	}
}

base for custom-container

sliceやmapは元からfor range loopでiterate可能でしたが、このproposalではほかのデータソースからもiteratorを作成できます。そのためのサンプルを以下に示します。

  • RangeIter: nからmの数値型を列挙する
    • 前述のproposalにはrange over intが含まれ、この構文で、0からnを列挙できます。0 to nは頻出で一般的であるとされる一方で、n to mは比較的一般的でないとみなされたようです。
  • OrderedMapIter: github.com/wk8/go-ordered-map/v2のOrderedMapの要素を古いものから新しいものに向けて列挙くする
    • このproposalの目的の一つに、「genericsの追加によってもたらされたcustom containerに統一的なiterate-overのinterfaceを与えること」のようなことが書いてあります。custom containerの例としてordered mapが上がっていますので、例として作っておきました。
  • Scan: io.Readerを読んでテキストとして解釈し、bufio.SplitFuncに応じて列挙する
    • 同じく、proposalにはstrings.Line()のようなものが追加できると述べています。bufio.Scannerをfunction iteratorに適合させたアダプタも作ってみます。
package iteratorhelper

import (
	"bufio"
	"io"

	orderedmap "github.com/wk8/go-ordered-map/v2"
)

type Numeric interface {
	~int | ~int8 | ~int16 | ~int32 | ~int64 |
		~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
		~float32 | ~float64
}

func RangeIter[T Numeric](start, end T) func(yield func(T) bool) {
	return func(yield func(T) bool) {
		for i := start; i < end; i++ {
			if !yield(i) {
				return
			}
		}
	}
}

func OrderedMapIter[K comparable, V any](o *orderedmap.OrderedMap[K, V]) func(yield func(k K, v V) bool) {
	return func(yield func(k K, v V) bool) {
		for pair := o.Oldest(); pair != nil; pair = pair.Next() {
			if !yield(pair.Key, pair.Value) {
				return
			}
		}
	}
}

func Scan(r io.Reader, split bufio.SplitFunc) func(yield func(text string, err error) bool) {
	scanner := bufio.NewScanner(r)
	if split != nil {
		scanner.Split(split)
	}

	return func(yield func(text string, err error) bool) {
		for scanner.Scan() {
			if !yield(scanner.Text(), nil) {
				return
			}
		}
		if scanner.Err() != nil {
			_ = yield("", scanner.Err())
			return
		}
	}
}

adapter

baseで作られたfuncを加工するadapter funcを作ります。ほかの言語で言うところのiterator methodのfilterとかmapとかですね。

iteratorに渡されるyield funcを即時関数として、自身に渡されるyieldの呼び出しの前後でデータの加工などを行うことになります。echoなどのmiddlewareと大体似たような形式で行いますね。

筆者がよく使うのは

  • Chain
  • Chunk
  • Enumerate
  • Filter
  • Map
  • Skip / Take
  • Window
  • Zip

あたりなので、これらを実装してみます。

Chain

渡されたiteratorを順番に一つずつiterateしていきます。yieldがfalseを返した後にyieldを呼ぶのはプロトコル違反なので適当なフラグが必要です。

func Chain[K, V any](
	iters ...(func(yield func(k K, v V) bool)),
) func(yield func(k K, v V) bool) {
	return func(yield func(k K, v V) bool) {
		stopped := false
		for _, iter := range iters {
			iter(func(k K, v V) bool {
				if !yield(k, v) {
					stopped = true
					return false
				}
				return true
			})
			if stopped {
				return
			}
		}
	}
}

Chunk

渡されたiterの中身をsize個ずつまとめて返します。yieldがfalseを返した後にさらにyieldを呼ぶのはプロトコル違反なので適当なフラグが必要です。

func Chunk[K, V any](iter func(yield func(k K, v V) bool), size uint) func(yield func(k []K, v []V) bool) {
	if size == 0 {
		panic("Chunk: size must not be zero.")
	}
	return func(yield func(k []K, v []V) bool) {
		stopped := false
		chunkedK, chunkedV := make([]K, size), make([]V, size)
		idx := uint(0)
		iter(func(k K, v V) bool {
			if idx < size {
				chunkedK[idx] = k
				chunkedV[idx] = v
				idx++
			}
			if idx >= size {
				idx = 0
				if !yield(append([]K{}, chunkedK...), append([]V{}, chunkedV...)) {
					stopped = true
					return false
				}
			}
			return true
		})
		if !stopped && idx != 0 {
			yield(append([]K{}, chunkedK[:idx]...), append([]V{}, chunkedV[:idx]...))
		}
	}
}

yieldに渡される[]Kおよび[]Vはクローンされたものが渡されていますが、これはiterate結果が保存されるケースは十分一般的だと予想したためです。例えば、以下のように

func CollectSlice[V any](iter func(yield func(v V) bool)) []V {
	var out []V
	for v := range iter {
		out = append(out, v)
	}
	return out
}

func CollectMap[K comparable, V any](iter func(yield func(k K, v V) bool)) map[K]V {
	out := make(map[K]V)
	for k, v := range iter {
		out[k] = v
	}
	return out
}

実際にはiterx/exp/xiterに定義されたものを使うことになると思うので深く考えてないです。

Enumerate

ほかの言語では筆者はまあまあな頻度で使います。1要素しか返さないiteratorを受け取って、何個目の要素かを表すint要素を足して2要素のiteratorに変換します。

func Enumerate[V any](iter func(yield func(v V) bool)) func(yield func(idx int, v V) bool) {
	return func(yield func(idx int, v V) bool) {
		var idx int
		iter(func(v V) bool {
			if !yield(idx, v) {
				return false
			}
			idx++
			return true
		})
	}
}

Filter

Select, Excludeです。

func FilterSelect[K, V any](
	iter func(yield func(k K, v V) bool),
	selector func(k K, v V) bool,
) func(yield func(k K, v V) bool) {
	return func(yield func(k K, v V) bool) {
		iter(func(k K, v V) bool {
			if selector(k, v) && !yield(k, v) {
				return false
			}
			return true
		})
	}
}

func FilterExclude[K, V any](
	iter func(yield func(k K, v V) bool),
	excluder func(k K, v V) bool,
) func(yield func(k K, v V) bool) {
	return func(yield func(k K, v V) bool) {
		iter(func(k K, v V) bool {
			if !excluder(k, v) && !yield(k, v) {
				return false
			}
			return true
		})
	}
}

Map

mapperによってiterの返す値と型を変換します。

func Map[K1, V1, K2, V2 any](
	iter func(yield func(k K1, v V1) bool),
	mapper func(k K1, v V1) (K2, V2),
) func(yield func(k K2, v V2) bool) {
	return func(yield func(k K2, v V2) bool) {
		iter(func(k K1, v V1) bool {
			if !yield(mapper(k, v)) {
				return false
			}
			return true
		})
	}
}

SkipWhile / TakeWhile

特定の条件が満たされる(間は|まで)iteratorの返す値を無視するようなadapterです。

func SkipWhile[K, V any](
	iter func(yield func(k K, v V) bool),
	predicate func(k K, v V) bool,
) func(yield func(k K, v V) bool) {
	return func(yield func(k K, v V) bool) {
		taking := false
		iter(func(k K, v V) bool {
			if !taking && !predicate(k, v) {
				taking = true
			}
			if taking && !yield(k, v) {
				return false
			}
			return true
		})
	}
}

func TakeWhile[K, V any](
	iter func(yield func(k K, v V) bool),
	predicate func(k K, v V) bool,
) func(yield func(k K, v V) bool) {
	return func(yield func(k K, v V) bool) {
		iter(func(k K, v V) bool {
			if !predicate(k, v) {
				return false
			}
			if !yield(k, v) {
				return false
			}
			return true
		})
	}
}

Window

iteratorの返す値をバッファーしてsize個のwindowを1つずつ移動しているかのような値を返すadapterです。例えば[1,2,3,4,5]のsliceを一要素ずつ返すiteratorがあり、size=3である場合、Windowを適用後は[1,2,3], [2,3,4], [3,4,5]が返されるようになります。

移動平均取るときとか、筆者はごくたまにしか使わないですが便利です。

サンプルなのでものすごくコピーが生じる実装です。実用的なものを作るならコピーを少なくするためにバッファーサイズをsizeの数倍にしてコピーの頻度を下げるとか、sliceから直接生成させるようにするとか、たぶんそういった工夫が必要ですね。

func Window[K, V any](iter func(yield func(k K, v V) bool), size uint) func(yield func(k []K, v []V) bool) {
	if size == 0 {
		panic("Window: size must not be zero.")
	}
	return func(yield func(k []K, v []V) bool) {
		bufK, bufV := make([]K, size), make([]V, size)
		idx := uint(0)
		ended := false
		iter(func(k K, v V) bool {
			if idx < size {
				bufK[idx] = k
				bufV[idx] = v
				idx++
			} else {
				copy(bufK, bufK[1:])
				copy(bufV, bufV[1:])
				bufK[len(bufK)-1] = k
				bufV[len(bufV)-1] = v
			}

			if idx == size {
				if !yield(append([]K{}, bufK...), append([]V{}, bufV...)) {
					ended = true
					return false
				}
			}

			return true
		})

		if !ended && idx != size {
			_ = yield(append([]K{}, bufK[:idx]...), append([]V{}, bufV[:idx]...))
		}
	}
}

Swap

2要素を返すiteratorの第1要素と第2要素を入れ替えるadapterです。たまに必要になりそうな気がする。

func Swap[K, V any](iter func(yield func(k K, v V) bool)) func(yield func(v V, k K) bool) {
	return func(yield func(v V, k K) bool) {
		iter(func(k K, v V) bool {
			if !yield(v, k) {
				return false
			}
			return true
		})
	}
}

Zip

zipは、二つのiteratorを受けとり、両方を同時にiterate overするというiteratorです。

今回述べられるproposalはいわゆるPush型のiteratorであり、明示的にPullするタイミングを制御できないため、1個要素取得したらコントロールを呼び出し側に戻す、というようなことはできません。
そのため二つのiteratorを一緒に進めるというのは不可能であり、pushをいかにしてかpullに変換する必要があります。

そこで、受け取った二つのiteratorをgoroutineの中で消費し、1つ要素を受けとるたびにチャネルを通して元の実行コンテキストに送信します。channelをunbufferedにしておけば、Pull型への変換が可能となるわけです。

func Zip[V1, V2 any](
	left func(yield func(v V1) bool),
	right func(yield func(v V2) bool),
) func(yield func(l V1, r V2) bool) {
	return func(yield func(l V1, r V2) bool) {
		iterResult := make(chan bool)
		leftCh := make(chan V1)
		rightCh := make(chan V2)

		go func() {
			left(func(v V1) bool {
				leftCh <- v
				return <-iterResult
			})
			close(leftCh)
		}()
		go func() {
			right(func(v V2) bool {
				rightCh <- v
				return <-iterResult
			})
			close(rightCh)
		}()

		defer func() {
			close(iterResult)
			for _ = range leftCh {
			}
			for _ = range rightCh {
			}
		}()

		for {
			l, okL := <-leftCh
			r, okR := <-rightCh
			if !okL || !okR {
				break
			}
			if !yield(l, r) {
				break
			}
			iterResult <- true
			iterResult <- true
		}
	}
}

Zip(iter.Pull版)

https://github.com/golang/go/issues/61897#issuecomment-1818871435

新しいCLでiter packageが使用できるようになったためiter.Pullを利用したZipを実装します。

iter.Pullおよびiter.Pull2は新しいcoroutineの中でiteratorを実行することでPush型iteratorをPull型に変換すると述べています。

go install golang.org/dl/gotip@latest
gotip download 543319
GOEXPERIMENT=rangefunc gotip test ./...
import (
	"iter"
)

func ZipPull[V1, V2 any](
	left func(yield func(v V1) bool),
	right func(yield func(v V2) bool),
) func(yield func(l V1, r V2) bool) {
	return func(yield func(l V1, r V2) bool) {
		nextL, stopL := iter.Pull(left)
		nextR, stopR := iter.Pull(right)
		defer stopL()
		defer stopR()

		for {
			l, lOk := nextL()
			r, rOk := nextR()

			if !lOk || !rOk {
				return
			}
			if !yield(l, r) {
				return
			}
		}
	}
}
coroの実装

えっcoroutine?となると思います。私はなりました。
Goはcoroutineをサポートしていませんし、追加したともテキスト中に書かれていませんので急にしれっと出てきました。

Wikipediaによればcoroutineは実行を一時中断、再開させれるコンピュータプログラムコンポーネントです。

どういうことなのかわからなかったのでCLを読んでみることにします。

CLによれば、coroutineは/src/runtime/coro.goで定義されています。

やはりこのCLでcoroutineを実装しているということで間違いないようです。

// A coro represents extra concurrency without extra parallelism,
// as would be needed for a coroutine implementation.
// The coro does not represent a specific coroutine, only the ability
// to do coroutine-style control transfers.
// It can be thought of as like a special channel that always has
// a goroutine blocked on it. If another goroutine calls coroswitch(c),
// the caller becomes the goroutine blocked in c, and the goroutine
// formerly blocked in c starts running.
// These switches continue until a call to coroexit(c),
// which ends the use of the coro by releasing the blocked
// goroutine in c and exiting the current goroutine.
//
// Coros are heap allocated and garbage collected, so that user code
// can hold a pointer to a coro without causing potential dangling
// pointer errors.

いきなりcoroutineじゃないって言ってますね。coroはcoroutineそのものではないが、既存のgoroutineの仕組みを利用して、上記Zip実装の中でやっていたようなことをするより効率的にcoroutine的なコントロールを実現するものと述べられていますね。

coroを定義することで、coroutineとして動作させるgoroutineと、coroutineの中で動作させる関数を記録します。

type coro struct {
	gp guintptr
	f  func(*coro)
}

g(goroutineのことですね)を拡張してcoroをトラックできるようにします。

type g struct {
	// 中略
+	coroexit     bool // argument to coroswitch_m
	// 中略
+	coroarg *coro // argument during coroutine transfers
	// 中略
}

getg()という関数(実際にはコンパイラがTLSから*gを取得するようにリライトするらしい)があるので、現在の*gは割とどこからでも取得可能です。
実装の中でfunc newproc1(fn *funcval, callergp *g, callerpc uintptr) *g(新しいgoroutineを作成する)やfunc mcall(fn func(*g))(*mg0のスタックでfnを実行する)を利用するため、自由に引数を渡せたりできません。そのため*gにcoroutine関連の情報をストアしておく必要があるようですね。

c := newcoro(f func(*coro))で、fを新しいgoroutineの中で実行、fは引数に渡される*coroを引数にcoroswitch(*coro)を呼び出すことが期待されているようです。newcoroを呼び出した側のgoroutineでcoroswitch(c)を呼ぶことで、fとその外側が交互に実行・ブロッキングが切り替えられます。

corostart(), coroswitch()によってnewproc1で作られた新しいgoroutineのスケジュール状態を手動で切り替えてしまうことで、重いスケジューラ部分をスキップする、それよによってcoroutine-likeなコントロールを効率的に実現するということのようです。

iter.Pullはこの挙動を利用することで、goroutineからparallelismをなくしてconcurrencyだけ得ることでPush型iteratorを効率的にPull型に変換するということらしいですね。

複数adapterの適用

func multipleAdapter() {
	base1 := iteratorhelper.SliceIter([]string{"foo", "bar", "baz", "qux", "quux"})
	base2 := iteratorhelper.SliceIter([]string{"corge", "grault", "garply"})
	adapted := iteratorhelper.FilterExclude(
		iteratorhelper.Map(
			iteratorhelper.Chain(base1, base2),
			func(_ int, v string) (time.Time, string) {
				return time.Now(), v + v
			},
		),
		func(k time.Time, v string) bool {
			return k.UnixNano()%3 == 0
		},
	)

	for k, v := range adapted {
		fmt.Printf("k = %#v, v = %#v\n", k, v)
	}
}
/*
k = time.Date(2023, time.November, 30, 14, 9, 58, 979296385, time.Local), v = "foofoo"
k = time.Date(2023, time.November, 30, 14, 9, 58, 979335361, time.Local), v = "barbar"
k = time.Date(2023, time.November, 30, 14, 9, 58, 979336663, time.Local), v = "quxqux"
k = time.Date(2023, time.November, 30, 14, 9, 58, 979337755, time.Local), v = "corgecorge"
k = time.Date(2023, time.November, 30, 14, 9, 58, 979343827, time.Local), v = "graultgrault"
k = time.Date(2023, time.November, 30, 14, 9, 58, 979344979, time.Local), v = "garplygarply"
*/

このように入れ子にiteratorにadapterを適用していきます。

method chainによるadapterの適用はできません。
#49085がないため、methodが新しいtype parameterを追加できないためです。このためMapのような型の変換を行うadapterはmethod chainで実装できません。
このproposal上のコメントでもある通りtype parameterのあるmethodがinterfaceを実装すべきなのかという問題があるため、実装される可能性も低いです。

感想

  • 3つシグネチャがあるので、少なくとも2つずつadapterを作る必要があるのでそこが少し手間に感じます。
  • どうやってFilterのようなアダプタ関数を定義するか一瞬わかりませんでした。
  • 書くのにずいぶん時間がかかりました。あとからちょこちょこいじっていますが、最初に大体のコードを書き上げるのに3,4時間ぐらいかかりました
    • 構文追加によってVS CodeのGo extensionなどエディター上での支援が得られなくなるためです。
      • 今思えばコンソールでwatch GOEXPERIMENT=range gotip vet ./...を実行しておけばよかったです。
    • やっぱり言語サーバーの支援は偉大です。
  • 前からiterator protocolをユーザーのコードに向けて公開してほしいと思っていたので、この変更はすごく歓迎です。
  • Goにとってあまりない構文の追加でありますので、for range loopを見て何かしらのcode generationを行っているようなコードは影響を受けるかもしれませんね。
  • いろいろ呼んでるとこの構文追加による利益は「シーケンスデータの生成および加工の標準的な方法が決まること」であって、シーケンスデータの加工さえしていればfor-rangeを使わないコードベースであっても同様に利益を得ますね。
  • iterおよびx/exp/xiterで普段つかうようなものは実装されると思いますので、基本的にはそれらのアダプタを組み合わせるアダプタを書くことになるかな・・・未来は明るいですね。

どんどん使いやすくなって助かりますね。このまま取り込まれるのかはわかりませんが楽しみです。

参考

参考にさせていただいた記事です。

GitHubで編集を提案

Discussion