Go の Generics に対応したライブラリを作ってみた

来月リリース予定のGo 1.18からGenericsが対応されることで、一つの関数定義で様々な型の処理を担うことができるようになります。
現在ベータ版の中でいろいろな人がいろいろな使い方を試していると思いますが、せっかくなのでこの機にGenerics対応のライブラリを作って(プレ)公開してみました。
そしてせっかくなのでちょっと宣伝ついでに、実際にGenerics環境を触ってみての感想とかを書きたいと思います。

### 注意事項

この記事は2022年1月時点での現況を元に記載しています。
2月には正式なGo1.18がリリースされることで、いくつか状況が変わっている可能性があります。

# 作ったライブラリ

https://github.com/meian/gcf

MapやFilterなど、他の言語でよくあるCollectionを操作する処理の詰め合わせです。
内部的にはIteratorな実装を合成することで、Collection ライクな処理の合成を(なるべく)簡単に行えるようにしたものです。
現状はスライスを使うかRepeat関数で同じ要素を並べるしかCollectionソースを作る方法がないですが、今後channelからソースを作ったり関数ベースで要素作ったりとか考えています。

どんな機能が使えるかとかはREADMEにざっくり書いてあるので見てみてください。
日本語版をメインで書いてるのでこちらの方がわかりやすいと思います。

https://github.com/meian/gcf/blob/main/README.ja.md

# なぜ作ったか

go は機能セットがシンプルなのはいいのですが、昔使っていた言語では次々に処理をチェーンして実装できたものがだいたいforとかifとかの基本構文を組み立てて書く必要があるんですよね。
go界隈としてはその書き方がお作法的になってるだろうしパフォーマンス稼ぎたい時はそうするべきだとは思いますが、ロジックにシビアなパフォーマンス要求がない時でもいつもそれで書くのがメンドイなぁとそこかしこで思うことがあったりしてます。(僕の場合)
以前よく使ってたC#ではLinqが代表例になると思いますが、それっぽい機能をgo1.17までで作るとしたら、`go generate` で型ごとに関数を用意するか要素を `interface{}` で取り扱うかの2択で、どちらもツラミが大きかったのかなと思います。
そこにGenericsである程度型が汎用的に使えるよと言う話が挙がって、何回かのバージョンアップで流れつつもようやくリリースされる目処が立って、ベータが出ましたという後はもうその場の勢いでワーっとコードを書いて作りました。

端的に言うとGenericsを触ってみたかったしこの機能が欲しかったから作りました(\*´ω`*)

# 簡単な使い方

docコメントとかExampleも書いていますが、以下にインラインの説明を入れながら簡単な使い方を。

```golang
import (
	fmt
	github.com/meian/gcf
)


// スライスからコレクション(Iterable)を作成する
// データソースの作成以外はこのコレクションを引き回して処理を組み立てることになる
itb := gcf.FromSlice([]string{"hoge", "foo", "bar"})

// 関数の条件に一致する要素のみを抽出
itb = gcf.Filter(itb, func(v string) bool {
	return len(v) == 3
})

// コレクションを2回繰り返す
itb = gcf.RepeatIterable(itb, 2)

// 処理した結果をスライスで取得する
s := gcf.ToSlice(itb)

fmt.Println(s)
// Output:
// [bar foo bar foo]
```

# 所感

※一応この記事のメインはここです
作ってみたり現状の開発環境だったりのあれこれについて。

## メソッドで型指定できないツラミ

Go 1.18のGenericsについてのツラミ。
これを感じてる人は結構いると思いますが、今回追加されるGenericsのtype parameterって、

- type による型・インターフェイスの指定(not alias)
- func によるトップレベル関数定義

のいずれかにしかつけられないんですよね。
で、今実装しているものだと `Map` だけなのですが、元のコレクションと処理後のコレクションの型が変わる場合、それらをsturct or interfaceのメソッドで定義することができないという。
(これがあるのでgcfではメソッドチェーンは早々に諦めて、全て関数で実装するようにしました)
あまりproposalとか追ってないですが、そのうちメソッドでもtype parameterがサポートされてほしいものですよね。

## 型推論が結構しっかり

Generics定義の処理を使う側の型推論について。
作り始める前は関数でどこまで暗黙に推論効くか、地味に不安があったんですよね。
特にMapとかは処理関数の結果型が元のコレクションに紐付かないからその辺りを拾ってくれるのか、とか。
ただ実際に作ってみれば、ライブラリ内は `[T]` を大量に書いてますが、使う側(testとかexample)ではほぼほぼ型の明示が不要で、引数の内容からしっかり型を判別してくれるんだなと感心したりしました。

## LSP(gopls)の補完

vscodeでのLanguage Serverの対応状況について。
vscodeで関数の引数に関数を渡す場合って、入力候補からfuncを選択すると引数まで含めて展開してくれますよね。
![intellisense-func.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75716/c0130d6a-7aa2-a4f0-abb3-56ed45becf9f.png)
これを展開するとこうなるんですよね。
![intellisense-typeparam.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75716/5eb1ca06-124d-de43-c4a6-cb3b15d0c810.png)
全部が型を推論して展開できないのはわかりますが、ここはすでに前の引数から解決してる型なのでintとして欲しかった。。。

## CIはまだ回せない

GitHubでの運用体制について。

https://github.com/actions/setup-go

あるかなぁ、まだないよなぁ、とダメ元でソースを確認しに行ったところ、setup-goの対応がまだ1.17.6までだったため、Github Actionsはまだ使えないことがわかりました。
厳密には自分でスクリプトで18-rcを落としてセットアップまでやればできなくはないけど、毎回ダウンロードしていると遅すぎるし、かと言ってsetup-goみたくインストール済キャッシュを独自に対応するのも不毛すぎるし、ということでここは大人しくGAを待とうかと思います。
それまではローカルで手動テストの手動カバレッジチェックですね\_(:3」∠)_

## [pkg.go.dev](https://pkg.go.dev/) の対応もまだ?

docコメントの公開について。
載っかるか試しにリクエストしてみたところ、not foundと言われました。。
同じアカウントで別のgoツールは(ほぼ何も書いてないけど)載っているので、まだGenerics対応のリポジトリを解析できていないのではないかなぁと。
ここも待つしかないようです。

ローカルでは `go tool doc` は1.18-rcのイメージ内で動くけど、godocはどうなんだろう？

# ライブラリの使い勝手

まだ自分でこれを使ってアプリを作ってない(ホントに勢いで作ったので)のではっきりとは言えませんが、多分以下は用意しないとgoでは辛いのではないかなと。
あと、CIない状況ではv0.1.0を切るのもアレな気がするので、早くGAしてほしい。

## errorの対応

goのエラー処理は `error` オブジェクトを発生元から処置先まで引き回すことで実現するので、関数やメソッドの戻り値として `(T, error)` にすることがしょっちゅうあります。
なので `Filter` とか `Map` とかの関数を介して処理する機能では、`FilterE` とか `MapE` とかのエラーも返せる形式を作らないと使える幅が少ない気がしています。
(というかおいおい作ろうかな)

# 最後に

せっかくなので機能要望とかこの作り方変とかフィードバックいただけるとありがたいです。
あと完全ソロプレイでレビュアいなかったりするのでお願いできる方とか、ここやったので取り込んでのPRしたい方とか、そういう人が捕まるとありがたいです(\*´ω`*)