虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Goの標準パッケージに追加されたイテレータ操作関数のご紹介

こんにちは。 虎の穴ラボでFantiaの開発を担当している Y.Kです。

先日、8月14日にGo言語のバージョン1.23がリリースされました。

バージョン1.23の目玉としてはやはり、バージョン1.22で試験的に入っていた「range-over func」が正式に導入されたことかなと思います。

「range-over func」とは「for-range構文」のrange句において以下の関数を受け取ることができるようにする変更です。

func(func() bool)
func(func(K) bool)
func(func(K, V) bool)

この変更によって「for-range構文」を用いたシンプルな形でイテレータ処理の実装ができるようになります。
実装方法など詳細については公式Wiki公式ブログを参照して頂ければと思いますが、 例えば以下のようなコードを書くことができるようになります。

func filter(l []int, f func(int) bool) []int {
    var res []int
    for _, v := range l {
        if f(v) {
            res = append(res, v)
        }
    }
    return res
}

func mysort(l []int) []int {
    if len(l) < 2 {
        return l
    }

    pivotIdx := rand.Int() % len(l)

    left := mysort(filter(l, func(v int) bool { return v < l[pivotIdx] }))
    right := mysort(filter(l, func(v int) bool { return v > l[pivotIdx] }))

    return slices.Concat(left, []int{l[pivotIdx]}, right)
}

// mysortでソートしたint型のスライスを対象に処理を順々に実行できる
func sortedIter(s []int) func(func(int) bool) {
    sorted := mysort(s)
    return func(yield func(int) bool) {
        for _, v := range sorted {
            if !yield(v) {
                return
            }
        }
    }
}

func main() {
    i := []int{3, 2, 1, 4, 5}

    // func(func(K) bool)をrangeに渡す例
    for v := range sortedIter(i) {
        fmt.Println(v)
    }

    // 出力
    // 1
    // 2
    // 3
    // 4
    // 5
}

この変更で得られるメリットはいくつかあるのですが、個人的に期待しているのはこの仕組みを用いた標準パッケージの拡充です。 バージョン1.23においてもいくつかの標準パッケージに対する関数の追加は行われており、今後もこの流れは続くものと思われます。

今回はイテレータを操作する関数が追加されたパッケージや実際に追加された関数について、いくつか紹介していきたいと思います。

iterパッケージ

バージョン1.23で新たに追加された、イテレータに関する実装や定義が含まれているパッケージになります。

pkg.go.dev

range句が新たに受け取ることができるようになったfunc(func(K) bool)func(func(K, V) bool)といった関数がそれぞれiter.Seq[V]型やiter.Seq2[int, V]型のように定義されていたり、任意のタイミングでイテレータの要素を順に取り出せるようにするためのPullPull2といった関数が定義されています。

slicesパッケージ

名前の通りスライスに関する操作を行うための関数が定義されているパッケージです。
バージョン1.23のリリースに合わせて、イテレータの操作を行う9個の関数が追加されました。

例えば、インデックスと値を順に取り出すAllやイテレータからスライスを作成するCollect、スライスをn個ずつ区切ったイテレータとして返すChunkなどがあります。

pkg.go.dev

mapsパッケージ

こちらも名前の通りmapに関する操作を行うための関数が定義されているパッケージです。
バージョン1.23のリリースに合わせて、イテレータの操作を行う5個の関数が追加されました。

例えば、キーのみをイテレータとして返すKeysや、mapとイテレータを受け取ってイテレータの内容をmapに追加するInsertなどがあります。

pkg.go.dev

stringsパッケージ

UTF-8でエンコードされた文字列を操作するための関数が定義されているパッケージです。
こちらのパッケージはバージョン1.24で5つの関数が追加される予定です。

例えば、改行区切りで文字列を分割し順に取り出すイテレータを返すLinesや、任意の文字列で分割し順に取り出すイテレータを返すSplitSeqなどがあります。

pkg.go.dev

それぞれ追加されたパッケージの利用例

「range-over func」の実装例として書いてみたスライスのソート処理は以下のようにスライスの値を順に取り出すイテレータを返すslices.Values関数とイテレータを受け取ってソートしたスライスを返却するslices.Sorted関数を組み合わせることで実現できます。

func main() {
    i := []int{3, 2, 1, 4, 5}
    sorted := slices.Sorted(slices.Values(i))
    for num := range sorted {
        fmt.Println(num)
    }
    fmt.Println(i)

    // 出力
    // 0
    // 1
    // 2
    // 3
    // 4
    // [3 2 1 4 5]
}

形式が同じ2つのマップを結合する場合は、例として紹介したmaps.Insertとmapのキーと値を順に取り出すイテレータを返すmaps.Allを組み合わせることで実現できます。

func main() {
    map1 := map[string]string{"key1": "a", "key2": "b"}
    map2 := map[string]string{"key3": "c"}

    fmt.Println(map1)
    maps.Insert(map1, maps.All(map2))
    fmt.Println(map1)

    // 出力
    // map[key1:a key2:b]
    // map[key1:a key2:b key3:c]
}

mapのキーをスライスに変換する場合は、マップのキーを順に取り出すイテレータを返すmaps.Keysとイテレータを受け取ってスライスを返却するslices.Collectを組み合わせることで実現できます。

func main() {
    m := map[string]string{"key1": "a", "key2": "b", "key3": "c", "key4": "d"}
    k := slices.Collect(maps.Keys(m))
    fmt.Println(k)

    // 出力
    // [key1 key2 key3 key4]
}

既に世の中に沢山の紹介記事がありますが、iterパッケージで定義されているPullを用いることで任意のタイミングで値を取り出すことができるようになります。

func main() {
    i := []int{3, 2, 1, 4, 5}
    s := []string{"a", "b", "c", "d"}

    allIter1 := slices.All(i)
    // 値を順に取り出すnext()と、処理の停止を伝えるstop()が返ってくる
    next1, stop1 := iter.Pull2(allIter1)

    allIter2 := slices.All(s)
    next2, stop2 := iter.Pull2(allIter2)

    defer stop1()
    defer stop2()
    for {
        // それぞれ値を順に取り出す。
        // 戻り値はindex, 値, 値が有効かのflag
        // 最後まで行くと, 「ゼロ値, ゼロ値, false」が返ってくる
        _, num, ok1 := next1()
        _, str, ok2 := next2()

        if !ok1 && !ok2 {
            break
        } else if !ok1 {
            fmt.Printf("i: なし, s: %s\n", str)
        } else if !ok2 {
            fmt.Printf("i: %d, s: なし\n", num)
        } else {
            fmt.Printf("i: %d, s: %s\n", num, str)
        }

    }

}
// 結果
// $ go run main.go
// i: 3, s: a
// i: 2, s: b
// i: 1, s: c
// i: 4, s: d
// i: 5, s: なし

この様に新たに追加された関数群を用いることで、これまでと比べより簡潔に様々な処理を実装することができるようになりました。 今回紹介したもの以外で、こう使うと便利だよ!といった物があればコメントなどで教えていただけると嬉しいです。

補足

1.23で追加されたイテレータの操作に関する関数は1.23リリースノート、1.24で追加される内容については1.24リリースノートにまとまっているため、今回紹介しなかった関数など一覧が気になる方はそちらを参照していただければと思います。

終わりに

ループ処理の拡張は個人的に待ち望んでいた変更なので本当に嬉しく思っています。
「range-over func」がExperimentalとして導入されてから何度か触ってみてはいるものの、まだまだ書き慣れていないというのが正直なところです。
ただ、今回の正式リリースに合わせて標準パッケージに関数が追加されたことで誰でも簡単に恩恵を受けることができるようになりました。

最近はあまりGoのコードを書く機会がなかったのですが、これを機にまた機会を増やしていきたいと思います。

採用情報

虎の穴ラボでは現在、一緒にFantiaを開発していく仲間を積極募集中です!
多くのユーザーに使っていただけるtoCサービスの開発をやってみたい方は、ぜひ弊社の採用情報をご覧ください。
toranoana-lab.co.jp