Go 1.18 Betaでジェネリクスを使ったFilter/Mapを作ってみる

こちらは Voicy Advent Calendar 2021 20日目の記事です

先週の12/14にGo 1.18 Betaがリリースされました!注目はなんといっても去年のアドベントカレンダーでも書いたジェネリクスです!

さっそくインストール

こちらからインストーラーをダウンロードできますが、1.18出たぞわーいってノリで仕事で使ってるMacでpkgファイルを実行すると、既存のgoコマンドを上書きして

$ go version
go version go1.18beta1 darwin/amd64

となってしまい、仕事に支障がでるのでやめましょう、、

ダウンロードページにも書いてありますが、その場合は

$ go install golang.org/dl/go1.18beta1@latest

と打つと~/go/bin/go1.18beta1がインストールされるので、これを実行するとさらに

$ go1.18beta1 
go1.18beta1: not downloaded. Run 'go1.18beta1 download' to install to /Users/[user name]/sdk/go1.18beta1

と出るので、言われるがままにgo1.18beta1 downloadを実行すると無事go1.18beta1という名前でコマンドがインストールされました!わーい!

$ go1.18beta1 version 
go version go1.18beta1 darwin/amd64

インストールができない場合はThe Go PlaygroundGo dev branchモードでも動作を確認することができます。

Filter/Mapを作ってみよう

ジェネリクスができたら個人的にぜひ欲しいと思ってたのが、配列に対して行うfiltermapといった処理です。将来的には実装されるんじゃないかと思ってますが(もしくは1.18ですでに追加されていたら教えてください)、せっかくなので自前で作ってみようと思います。

Filterを作る

呼び出し方の理想としては

array := []int{100, 200, 300, 400, 500}
filtered := array.filter(func(num int) bool {
    return num >= 300
})

みたいに書けて、なんなら関数の部分も省略して

array := []int{100, 200, 300, 400, 500}
filtered := array.filter(num => num >= 300)

とか書けると良かったのですが、そうもいかず、、
結果的にはFilter関数を作成して以下のように書くことにしました。とはいえ書き方はappendとかと似てるのでそこまで違和感はありませんが。

array := []int{100, 200, 300, 400, 500}
filtered := Filter(array, func(num int) bool {
    return num >= 300
})

というわけでジェネリクスを使用したFilter関数はこうなりました。

func Filter[T any](array []T, f func(T) bool) (result []T) {
    for _, value := range array {
        if f(value) {
            result = append(result, value)
        }
    }
    return
}

まぁそうだよねという感じで、特に難しいことはないと思います。

Mapを作る

同じ感じでMap関数も作ってみます

呼び出し方はFilterと同じくこんな感じ。

array := []int{100, 300, 500, 700, 900}
mapped := Map(array, func(num int) string {
    return "V" + strconv.Itoa(num)
})

で、Map関数はこうなりました。こちらはもっとシンプルですね。

func Map[T , T2 any](array []T, f func(T) T2) (result []T2) {
    for _, value := range array {
        result = append(result, f(value))
    }
    return
}

Filter/Mapを連続して呼び出したい

とはいえ、filterやmapってこんな感じで繋げて呼び出したいじゃないですか。

array := []int{100, 300, 500, 700, 900}
newArray := Filter(・・・).Map(・・・)

でもこの関数の作り方だとこうやって個別に呼びだして、都度変数に入れる必要があるんですよね。

array := []int{100, 300, 500, 700, 900}
filtered := Filter(array, ・・・)
mapped := Map(filtered, ・・・)

めんどくさい、、なによりカッコ悪い、、

なので、繋げて呼び出せるようにJavaのStreamを参考にstructを作って、それにFilter/Map関数を持たせるようにしたらどうだろうと思い試してみました。

呼び出し方はこんなイメージです。

array := []int{100, 300, 500, 700, 900}
result := NewStream(array).
    Filter(・・・).
    Map(・・・).
    Collect()

そしてStreamstructはこんなイメージ。

func NewStream[T any](array []T) Stream[T, T2] {
    return Stream[T, T2]{array}
}

type Stream[T, T2 any] struct {
    array []T
}

func (s Stream[T, T2]) Filter(f func(T) bool) Stream[T, T2] {・・・}
func (s Stream[T, T2]) Map(f func(T) T2) Stream[T, T2] {・・・}
func (s Stream[T, T2]) Collect() []T {
    return s.array
}

だったのですが・・・結論としてはダメでした、、

原因はMapのところで戻り値に指定する必要のあるT2です。本来はNewStreamでStreamを作る時にはT2の型が確定していなければなりません。しかしT2の型はMapメソッドを呼び出す時の引数で決まるので、Streamオブジェクトの作成時点では確定ができず、結果的にStreamの作成ができませんでした。

試しにStreamを作る時はTのみを定義し、Mapを呼び出す時に初めてT2が出てくるように

type Stream[T any] struct {
    array []T
}
func (s Stream[T]) Map[T2 any](f func(T) T2) Stream[T2] {・・・}

としてみたのですが、

methods cannot have type parameters

と言われてしまいました。メソッド毎に新しく型を追加することはできないようです、、

まとめ

他にも試行錯誤してみたのですが、結果的に良い感じのFilter/Mapを作成することができませんでした、、とはいえやっぱりあると便利なので、今後配列に標準でfiltermapが使えるようになることを期待しています。

ジェネリクス以外にも1.18のアップデートはこちらに載っているので、興味のある方はぜひチェックしてみてください。

最後に

Voicyでは一緒に働くエンジニアを大募集中です!Goはもちろん、インフラ、アプリ、Web等さまざまな職種で応募をお待ちしております!詳しくは以下のページをご覧ください! https://hrmos.co/pages/voicy/jobs/0000121