kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

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

先日自分がGoについてつぶやいたものが、id:methaneさんに捕捉されていて、それに対する反論記事

methane.hatenablog.jp

があがっていたので、それに対する所感を書いてみました。(2017/09/22 追記):cocoatomoさんから指摘があったのですが、引用元は全て id:methane さんの、上記ブログエントリの文章です。雑多な感想に関して反応しているものではないです。

前置き

Goや言語批判に関するスタンスを誤解されると嬉しくないので、最初に書いておきます。

  • Goの言語仕様はあまり好きではないけど、Goユーザーを見下したり、馬鹿だとかそういう風には思っていない

言語仕様批判とユーザー批判は別の話ですが、しばしばいっしょくたにされて、糞言語Xを使うユーザは糞だ、みたいな主張がされることがあるので、 そういう意図はないという表明です。

  • Goユーザーの中で、ジェネリクスがなくても構わないと主張するユーザーへの批判はしたけど、Goユーザー全てがそうだと思っているわけではない

  • Goユーザーの中でジェネリクス不要論を唱えているユーザーへの批判はしたけど、そういうユーザーを馬鹿にしているわけではない

これは掛け値なしの本音なのですが、そう思ってもらえるかどうかについては読み手にお任せするしかありません。

Goはマイクロサービス的なものを開発するための言語か?

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

とのことですが、まずこの前提がおかしいのではないかと思います。実際の利用シーンを視ても、コマンドラインツールやWebサービスGCありのC的な使い方、などなど、かなり様々な用途で使われている言語だと思いますし、Goが出たときのドキュメントにも、利用シーンをそれほど限定するような文言はなかったように思います。

ただ、その上でいうと、HTTPとかJSONとかを扱うとき、主にスキーマがないものを扱うため、ジェネリクスがあってもそんなに便利にはならないことが多い、ということはその通りだと思います。DBについては、RDBなら、ジェネリクスを使って、(スキーマの整合性がとれていれば)型安全であることを謳うフレームワークは少なからずあります。

ソート関数とジェネリクスの関係

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

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

これは、部分的には正しいのですが、比較関数として、無名関数を使うケース(Java 8以降)だと、ジェネリクスがあるかないかで、比較関数の書きやすさが全く違います。

たとえば、Java 8以降で、整数のリストを降順ソートしたいとき、

Collections.sort(xs, (x, y) -> y.compareTo(x));

と書けば済みますが、ジェネリクスがないと

Collections.sort(xs, (Object x, Object y) -> ((Integer)y).compareTo((Integer)x));

となって面倒さが大幅にあがります。

余談ですが、比較関数を渡すのも面倒だという話はあって、そういう場合に使える言語機構として、Haskellなどにある「型クラス」があります。

Haskellは普段かかないので、Scalaで書くと、次のようになります。

val sortedList = List(3, 2, 4, 1, 5).sorted // 整数同士を比較するオブジェクトが暗黙の内に渡される

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

Javaレベルでのジェネリクスでも上記のように十分うれしいと思いますが、それはともかく、私が言ってないことを勝手に補完してそれに反論されるのはちょっと困惑です。ここに関しては引用がないので、一般論としての反論なのかもしれませんが。

ジェネリクスを使う側と定義する側の嬉しさの混同

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

これは、汎用的な型を「定義」できることに関するメリットを見落としているように思います。Goは確かに動的配列もマップ(連想配列)も組み込み型なので、Javaとは事情が多少違いますが、それ以外のコレクションが欲しくなったときに、Goの組み込み型だけでは対応できません。代表的な例として、集合を表す SetJavaではジェネリックなコレクションとして提供されていますが、現状のGoでそれを実現しようとすると、必然的に要素型は interface{} になってしまって非常に嬉しくありません。

また、コレクションに対して map したり filter したりしてコレクション操作を楽に書けるようになているのが、今時の言語だと一般的だと思いますが(自分もそのように書くのに慣れてしまって、ループでコレクション処理を書かなければいけないのは苦痛です)、このようなジェネリックmap メソッドや filter メソッドを定義しようと思うと、ジェネリックな関数を定義する機能が必要になります。

さらに、標準のコレクションでは性能要求を満たせない場合や、不変コレクションが欲しい場合など、Javaではサードパーティのコレクション(有名なものだとEclipse Collection)を使うという選択肢があって、そのようなサードパーティのコレクションも標準と同じ様な使い勝手を提供できますが、Goではそのようなものを提供しようとすると、要素型がやはり interface{] になってしまいます。

他にも汎用的なグラフライブラリを提供するとき(JGraphT)など、頂点や辺の型は型パラメタとして定義されています。これによって、ユーザ定義のクラスを型安全に、頂点や辺の型として使えます。Goではやはり interface{} になってしまうのも言うまでもないことです。

Goの利用シーンを限定するなら、そのような欠点は大した問題にはならないかもしれませんが、一つの機能がないことで利用シーンを大幅に狭めるのはあまりいいことだとは思えません。

その他

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

イディオム以前に、後者だと間違ったpop操作をする可能性がないのに対して、前者は間違った操作をする可能性があるので、単に面倒以上のコストを払っていると思います。本来なら必要なかった、コードのコピペというコストを払っている、といってもいいです。

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

これは私に対する反論ではなく、mizchiさんへの反論ですが、正直この辺は非常に(開発チームの言っていることには)懐疑的です。そもそも、Goが公式に初登場した時期(2009年頃?)でも、ジェネリクスを入れることについては検討中といってたわけで、それから8年経った今でも検討中と言われても一体いつ入れるつもりなんだというのは疑問に思います(その間、いくつものGoにジェネリクスを入れるプロポーザルが登場しているにもかかわらず)。

逆に言語にこだわらない、Goの広い分野への布教に熱心でない人は、ジェネリクスが無いことが不便な場面ではわざわざGoを選ばないでC++C#JavaやRustなどを使ってるので、 本当にジェネリクスが無いことで困ってない。

これはソースが欲しいです。現状、Goの利用シーンはかなり広がっているのに、ジェネリクスが無いことで不便な場面で他の言語にスイッチするというコストを多くのユーザーは払っているのでしょうか?(便利ライブラリやフレームワークの有無が原因ならわかりますが)

全体として、利用シーンをあえて限定した上で、その範囲でならジェネリクスが無くてもあまり困らない、という主張にみえて、Goをそういう言語としてとらえるなら間違ってはいないと思いますが、私はGoをもっと汎用的な言語だと思っているので、その点が噛み合っていないのかなという気がしました。また、上の方でも書きましたが、ジェネリクスによって得られる柔軟性やメリットについてはちょっと過小評価気味ではないかなという気がします。