methaneのブログ

このブログに乗せているサンプルコードはすべてNYSLです。

Go にジェネリクスがなくても構わない人たちに対する批判について

なんども繰り返される話でうんざりなんだけど、繰り返されるたびに反論するのもアレなので、URL貼れるように記事にしておく。

頑なに要らないと言ってる人が具体的にどの発言のことを差してるのか分からないけど、コア開発者たちはツールチェインやランタイムの進化を優先していただけで頑なに拒否してたりはしません。今はツールチェインやランタイムが大分進化したから、Goの適用範囲を広げるためにジェネリクスを含めて機能追加も検討し始めようかっていうフェーズです。

あとどの言語にもちょっと公平的な見方ができなくなった痛いファンはいるもので、そういった人たちをいちいちあげつらってこういう言い方で失笑するのは、別に止めはしないけど自分の格を下げるだけだと思う。

逆に言語にこだわらない、Goの広い分野への布教に熱心でない人は、ジェネリクスが無いことが不便な場面ではわざわざGoを選ばないでC++C#JavaやRustなどを使ってるので、本当にジェネリクスが無いことで困ってない。そういった意味で「別にGoにはジェネリクス要らない」「ジェネリクスより先にXXX欲しい」人たちを失笑するのは、やはり笑ってる方がバカっぽい。RubyC++も使える人が「別にRubyに静的型要らない」って言ってるのに対して「こいつ静的型の便利さ理解してないよwww」と言ってるのと同じ。

Goは特に今で言うマイクロサービス的なものを(色んな意味で)効率よく開発するために作られた言語で、DBとかJSONとかRedisとか扱ってHTTP API提供する簡単なサービスを書いてみたら分かると思うんだけど、本当にジェネリクスがなくて不便な場面ってメチャクチャ少ない。 interface {} (Javaでいう object) が出て来る場面って、ジェネリクスが無いせいで出て来ることは本当に稀。

JSONRDB使っていてコードジェネレータを使わない場面では interface{} がたくさん出て来るけど、それはジェネリクス持ってる言語もジェネリクスじゃなくてリフレクションで解決しててGoと同じ不便さを持ってるだろう。

log.Printf("%v %v %v", a, b,c) みたいな場合、可変長引数が interface {} だけど、それもあんまりジェネリクス関係ないし、しかも静解析ツールがフォーマット文字列と引数の整合性チェックしてくれるのでやっぱり困ってる実感ない。

唯一面倒、あるいは不格好だなぁと思うのはソートだ。ジェネリックなソート関数を使うのが面倒で、便利さのために sort.Ints() とか sort.Strings() とかを提供している*1のが不格好なんだけど、この面倒臭さも単純にジェネリクスが無いせいとはいうわけでもない。

例えば Javaジェネリクスがプリミティブ型にそのまま対応したとしても、プリミティブ型が .compareTo() を持ってないし演算子オーバーロードとか「この演算子を定義している型」を宣言する方法を持ってないから、ジェネリック関数に渡すには「値の比較方法」「値の交換方法」を教えてあげないといけなくて面倒だよね。

今のGoのソートの面倒さはそれの方に近くて、比較方法と交換方法をいちいち教えないといけない部分が面倒くさい。もちろんC++のテンプレートみたいなのもあるわけで、Goに上手くフィットするジェネリクスならこの問題も解決するはずだ。

でも、Goがジェネリクス持ってないことを非難するときにはJavaを「持ってる」側に分類しておいて、Javaレベルのジェネリクスはあんまり嬉しくない*2ことを説明すると「ジェネリクスJavaだけの機能じゃない。Javaと比較すんな」みたいな反応されるのにはうんざり。

これは2割くらいしか同意できない。

昔にJavaジェネリクスが無いことを擁護していたユーザーは、きっとC++のテンプレートしか知らなくて、しかも当時のC++コンパイルエラーは読めたものじゃなかったのだろう。あのコンパイルエラーと戦うよりは、 int x = (int)vec.get(i) と書くほうが生産性が高く感じる人が多くても不思議ではない。

一方で今のGoユーザーは、 int v = (int)v.get(i) なんてコードはほぼ書かない。昔のJavaと違って、動的配列もマッピング型もジェネリックな組み込み型だから。たまに書く場合も、JSONmap[string]interface{} で扱うとか、どんな型でも値として入れられる Context から値を取り出すとか、ジェネリックがあっても便利になるとは思えないケースがほとんどだ。

なので、アプリ開発言語としてメジャーなのがC++Javaしか無かった当時のJavaユーザーと、そもそもジェネリクスがなくて困るケースにあまり遭遇しない、そういうケースは別にGo以外でも他のメジャーな言語使えばいい今のGoユーザーでは、「ジェネリクスいらない」の意味が全く異なる。どちらにしろ、両者とも「横目により良いものを見ながらやせ我慢」してたわけじゃないので「酸っぱいぶどう」というのは違うと思う。

逆に2割の同意できる部分としては、今ジェネリクス要らないと思ってるGoユーザーも、(Javaほどではないとはいえ)実際に追加されたら便利だと思うだろう。

さっきも書いたとおり、Goユーザーがジェネリクスが無い不便さを実感しにくいのは int v = (int)v.get(i) のような、ジェネリクスがあればダウンキャストが要らなくなるコードにほとんど遭遇しないからだ。

だけど、実際にはジェネリクスなしで型安全を手に入れるために、「イディオム」というコストを払っている。例えばFIFOキューから値を取り出すなら、今は v := q[0]; q = q[1:] なのが、ジェネリクスがあれば v, ok := q.Pop() になるだろう。後者に慣れたら、きっと前者は面倒に感じるはずだ。

なので、Goにジェネリクスを足す理由が適用範囲を広げるためだとしても、今Goがフィットしてる分野にもちゃんとメリットはあると思う。

*1:ちゃんと確認してないので、単に便利さのためだけじゃなくて最適化のために専用のアルゴリズムが使われているかもしれない

*2:いちいちboxingするコスト、Javaユーザーは許容するかもしれないけど、高性能サーバーを手早く書きたいGoユーザーは許容しないだろう