その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

Go Genericsを使ってgo-optionalを書いた / Go Generics感想

Go Genericsがどんなもんか試してみたかったので、これを使ってOptionの実装を書いてみました。

github.com

基本的な使い方としてはSynopsisを読んでもらえばわかると思いますが、ユーティリティとしては

  • IsSome()
  • IsNone()
  • Take()
  • TakeOr()
  • TakeOrElse()
  • Filter()
  • Map()
  • MapOr()
  • Zip()
  • ZipWith()
  • Unzip()
  • UnzipWith()

あたりを取り揃えております。examplesも併せてご覧いただくとおおよその使い方の雰囲気が掴めると思います。
利用のためにはまだunstableな最新版 (go1.18) を使う必要があるので、gotipとかを使って新しい処理系を引っぱってくる必要があります。

で、GoのGenericsを使ってみた感想としてdefault valueとかconstraintとかどうするんだよと悪戦苦闘していたのですが、id:codehexさんの書かれた記事にほぼほぼ書かれていることを一通り終わった後に気付きました。

zenn.dev

で、概ね同じような所感を持ったのですが、それだけだと芸が無いので僕個人としてのGenericsを使ったときの感想について記しておきます。

メソッドに型パラメータが持てない

例えばこのようなメソッドについて考えてみましょう。

type Something struct {
}

func (s *Something) Echo[V any](v V) V {
	return v
}

なんとなく構文的にはvalidなように見えますが、これをコンパイルすると

./main.go:6:25: methods cannot have type parameters
./main.go:6:26: invalid AST: method must have no type parameters

というエラーが吐かれます。メソッド (つまりレシーバがある) 時にはそのメソッドに対して型パラメータが付けられないということのようですね。
例えば以下のようにメソッドにすることをやめるとコンパイルは通るようになります。

func Echo[V any](s *Something, v V) V {
	return v
}

こういった実挙動のため、go-optionalでは Map() 等の関数実行時の型パラメータに依存するユーティリティについてはメソッドではなく関数として実装しています。
ref: https://github.com/moznion/go-optional/blob/b0ada9baa88672d2f0fc11a1487842da170b5f92/option.go#L83

なんとかジェネリクスのconstraintをtype assertionで分岐させられないか

例えば、go-optionalでは「Option[T]の値が、Tの或る値を持っているか」を確認するメソッドである Contains(v T) を実装しようと思ったのですが

type Option[T any] struct {
	value T
	exists *struct{}
}

func (o Option[T]) Contains(v T) bool {
	if o.IsNone() {
		return false
	}
	return v == o.value
}

というふうに素朴に実装すると ./option.go:20: invalid operation: cannot compare v == o.value (operator == not defined on T) という風なコンパイルエラーが出てきます。そりゃそうだ。

これについては上のcodehexさんの記事にもありましたが、現状あるconstraintを満足させるためには別の型 (型パラメータ) を付けて実装する必要があります。
たとえばこれだと通る。

type ComparableOption[T comparable] struct {
	value T
	exists *struct{}
}

func (o ComparableOption[T]) Contains(v T) bool {
	if o.IsNone() {
		return false
	}
	return v == o.value
}

あくまで例えばの話ですが、anyについてtype assertionのようにconstraintのチェックができれば便利だよな〜と思い、例えば以下のように……

// o is a value of Option[T any]
v, ok := o.(Option[T comparable])

とはいえまーこれあんま筋良くない気がするな、コンパイル時のtype erasureとかもあるだろうし……

型パラメータにタプルを持たせられない

例えばこういう書き方はできません。

func Zip[T, U any](opt1 Option[T], opt2 Option[U]) Option[(T, U)] {
	...
}

まあそういうもんですよ、と言われたらその通りなのでgo-optionalでは Pair という構造体を用意して Option[Pair[T, U]] を返却するようにしました。

ref: https://github.com/moznion/go-optional/blob/b0ada9baa88672d2f0fc11a1487842da170b5f92/option.go#L108

pkg.go.dev がgodocを読み込んでくれないっぽい?

https://pkg.go.dev/github.com/moznion/go-optional

stableなruntime versionではまだ未定義の構文が多く含まれているからか、godocが解釈できないっぽい? pkg.go.dev に一向にドキュメントが出てこなくて地味に困っています……

[追記] バイナリのフットプリントについて

Genericsを使ったときと使わないときでビルドされたバイナリのフットプリントに差が出るのだろうか、という素朴な疑問があったので雑に比較。
環境は go version devel go1.18-4083a6f Wed Nov 17 14:10:29 2021 +0000 darwin/amd64

genericsなし:

package main

import "fmt"

type SomethingStr struct {
        v string
}

type SomethingInt struct {
        v int
}

func main() {
        s := SomethingStr{
                v: "foo",
        }
        i := SomethingInt{
                v: 123,
        }

        fmt.Println(s)
        fmt.Println(i)
}

genericsあり:

package main

import "fmt"

type Something[T any] struct {
        v T
}

func main() {
        s := Something[string] {
                v: "foo",
        }
        i := Something[int] {
                v: 123,
        }

        fmt.Println(s)
        fmt.Println(i)
}

結果:

-rwxr-xr-x   1 user  user  1846368 11 18 11:11 generics*
-rwxr-xr-x   1 user  user  1846368 11 18 11:27 no_generics*

1バイトも変わらぬ結果に。面白いですね。とはいえこれは最適化戦略やコードに依存する気がするのでもっと複雑なコードを書くと変わってくる可能性がある気が……あんま信用しないでください。



だいたいそんな感じでした。手習い的にGoのGenericsを使ってみましたが、けっこうシンプルかつ現時点でも普通に使えるな、という肌触りです。
ここは型の推論が効くだろ (他の言語だと効きそうに見える)、と思って型パラメータの記述をサボると「型パラメータを明示しろや」とコンパイラに怒られたりもするわけですが、まあそこはgoらしさ・シンプルさを優先したということなのでしょう。機能と実装複雑性・コンパイルタイムのトレードオフという見方をしました。