methaneのブログ

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

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

kmizu.hatenablog.com

Twitterである程度レスをしたのですが、やはり繰り返される話題なので残る形で書いておきたいと思います。

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

私の前の記事は、まさに前者の批判に対する返答です。私はGoにジェネリクスを追加することに賛成ですが、別にそうならなかったとしても失望しない程度に「なくても構わない」人です。

一方で後者は、もしGoに限らず一般論としてのジェネリクス不要論だとすれば、批判にも値しないと思いますよ。話題にするつもりはありません。

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

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

マイクロサービスという言い方は良くなかったかも知れません。もちろんWebサービスも含むのですが、Webサービスの裏側で動くサーバーだったり、Webサービスと言ってもファイルのダウンロードやJSON APIだけとかのWebアプリではなかったりとか、そういったクラウド上で動くいろんなサーバー全般のことです。

Go の開発された背景やゴールは繰り返し紹介されてきました。

Goが解決しようとしている問題は大きくわけると2つの「スケール」です。1つはハードのスケール、つまりたくさんのコア、たくさんのサーバー、クラウドへの対応です。もう一つはGooglerの開発のスケール、ビルド時間短縮であったり、疎結合な開発チームへの対応だったり、先週までJavaC++Pythonを書いていた人が素早く生産的になれることでした。

もちろん、これらを目標にしたからと言って、用途がこれらに限定される訳ではありません。GCやコードフォーマッタ付きのC言語と見た時、OpenSSLへの依存とかしなくても https のクライアントが使えますし、autotools などのたくさんの周辺ツールを覚えなくても (Windows を含む!) クロスプラットフォーム対応できるし、クラウドへのデプロイを助けるシングルバイナリやクロスコンパイルはCLIツールのバイナリ配布も助けます。

開発のスケールについては、Googleの開発体制はほとんどそのままOSSの開発体制にも繋がります。週末にしかGoを使わない人が、世界中の異なるタイムゾーンで、フェイス・トゥ・フェイスのコミュニケーションは殆どない状況で協力しているのですから。

3つの記事から幾つか引用します。(ですができれば勝手にGoの役割を思い込みで決めつける前に記事を全部読んで下さいね)

Go, Open Source, Community

The first goal is to make a better language to meet the challenges of scalable concurrency. By scalable concurrency I mean software that deals with many concerns simultaneously, such as coordinating a thousand back end servers by sending network traffic back and forth.

Today, that kind of software has a shorter name: we call it cloud software. It’s fair to say that Go was designed for the cloud before clouds ran software.

Go at Google: Language Design in the Service of Software Engineering

The problems introduced by multicore processors, networked systems, massive computation clusters, and the web programming model were being worked around rather than addressed head-on.

When Go launched, some claimed it was missing particular features or methodologies that were regarded as de rigueur for a modern language. How could Go be worthwhile in the absence of these facilities? Our answer to that is that the properties Go does have address the issues that make large-scale software development difficult. These issues include:

  • slow builds
  • uncontrolled dependencies
  • each programmer using a different subset of the language
  • poor program understanding (code hard to read, poorly documented, and so on)
  • duplication of effort
  • cost of updates
  • version skew
  • difficulty of writing automatic tools
  • cross-language builds

Individual features of a language don’t address these issues. A larger view of software engineering is required, and in the design of Go we tried to focus on solutions to these problems.

Toward Go 2

We want to make programmers more effective at managing two kinds of scale: production scale, especially concurrent systems interacting with many other servers, exemplified today by cloud software; and development scale, especially large codebases worked on by many engineers coordinating only loosely, exemplified today by modern open-source development.


たとえば、Java 8以降で、整数のリストを降順ソートしたいとき、 Collections.sort(xs, (x, y) -> y.compareTo(x)); と書けば済みますが、ジェネリクスがないと Collections.sort(xs, (Object x, Object y) -> ((Integer)y).compareTo((Integer)x)); となって面倒さが大幅にあがります。

Twitter でも返信しましたが、最近のGoはもう少し楽な書き方ができています。

   // https://golang.org/pkg/sort/#Slice
    sort.Slice(people, func(i, j int) bool { return people[i].Name < people[j].Name })

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

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

はい、これは kmizu さんへの反論ではなく、この話題になるといっつも湧いて出てくるJavaで型を語るなマウントしてくる人に向けた牽制でした。 Javaでも持ってるマウント→Javaで語るなマウントの連携は、本当に本当にうんざりです。 でも、まぁそういった人は内容読んでくれませんもんね。ブコメにまた湧いていました。

 それ以外のコレクションが欲しくなったときに、Goの組み込み型だけでは対応できません。代表的な例として、集合を表す Set が Javaではジェネリックなコレクションとして提供されていますが、現状のGoでそれを実現しようとすると、必然的に要素型は interface{} になってしまって非常に嬉しくありません。

GoではX型の集合は map[X]bool で書きます。

if m[x] { ... } // 要素が含まれているかのテスト
m[x] = true // 要素の追加
delete(m, x) // 要素の削除

で、もちろんそれ以外に組込型が対応していないデータ構造もあるのですが、(Pythonphpプログラマーなら同意してくれると思うのですが)それらを使う頻度はとてもとても少ないです。

さらに、文字列や整数に関するデータ構造だったらGenerics要らなくてstringや(効率は少し犠牲にして)int64に特化して実装してしまうこともできます。 逆に大量のデータを扱いたいならそもそもGo上の struct 型を直接入れるのではなくシリアライズしてオンメモリDBに入れることで、メモリ効率が良くなるだけでなく、(GoのGCは精密でバイト配列の中身はスキャンしないので)GCのオーバーヘッドも格段に減らしたりします。

それでもジェネリックなデータ構造がほしいなら、たしかに interface{} 経由になったり、コードジェネレータに頼ったりする事になるのですが、本当にごくごく稀で、また他の理由で interface{} に頼ったりコードジェネレータに頼ったりすることだってあります。 (protobufとか、Swagger等JSON APIスキーマ言語からの生成とか)

そういった理由で、実際にGoで開発している人は、ジェネリックなデータ構造が無いことを大きな不便とは感じにくいのです。

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

これは、ジェネリック以前の文化的なところからのすれ違いです。

もし、Goが filter, map, reduce を多用するスタイルを推奨する言語であれば、ユーザー定義関数のジェネリクスを待たずとも、組み込み関数に追加することができますが、Goはそれをしていません。末尾最適化も実装していません。高階関数は多用するものの、短い関数を省略して書けるラムダ式のようなものは持っていません。Goは関数型スタイルの書き方を推奨しておらず、単に for ループを書こうという言語なのです。

これについては直接的なソースが見つからなかったのですが、間接的なものであれば。

なぜGo言語 (golang) はよい言語なのか・Goでプログラムを書くべき理由

「知的でないプログラマ」のためにデザインされているというのは指摘自体は正しいと思います。実際Rob PikeもFrom Parallel to Concurrentの20:40~で次のように述べています
The key point here is our programmers are Googlers, they’re not researchers. They’re typically, fairly young, fresh out of school, probably learned Java, maybe learned C or C++, probably learned Python. They’re not capable of understanding a brilliant language but we want to use them to build good software. So, the language that we give them has to be easy for them to understand and easy to adopt.
要するにGoogle社員のような、研究者ではなく大学を出たばかりでC++, Java, Pythonの経験しかない「素晴らしいプログラミング言語」を理解する能力が欠けているエンジニアでも簡単に理解し、大きなシステムを構築するのに使えるようにGo言語はデザインされているのです。

GitHub - robpike/filter: Simple apply/filter/reduce package.

Having written it a couple of years ago, I haven’t had occasion to use it once. Instead, I just use “for” loops. You shouldn’t use it either.

私も、JavaのStream APIC#LINQは確かにモダンでクールな書き方だと思いますが、単にループを書く書き方に比べて劇的に生産性があがるとは思えません。「Goに入ってはGoに従え」で、Goらしい書き方になれることをおすすめします。

たとえばコードベースの一部をScalaからGoへ移植したMovioの記事でも、

movio.co

Simpler code is more readable code

No map, no flatMap, no fold, no generics, no inheritance… Do we miss them? Perhaps we did, for about two weeks.

It’s hard to explain why it’s preferable to obtain expressiveness without actually ‘Go’ing through the experience yourself - pun intended. However, Russ Cox, Golang’s Tech Lead, does a good job of it in the “Go Balance” section of his 2015 keynote at GopherCon.

のように書かれています。


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

どれくらい「大幅に狭め」ているかどうかはさておき、もともとGoは他の汎用言語が解決しない問題を解決するために生まれた言語であり、例えばランタイムがマルチスレッド上に goroutine を動かす前提なのでLinux上でsetuidできないなど、特徴的と言うか色々割り切った言語であって、すべての用途をカバーしようという言語ではありません。それがたまたま(ジェネリクスを多用しないなど)いくつかの条件で git-lfs などのツールの開発にもフィットしたというだけです。

なので、Goが今使われていない分野ではなく、Goが今使われている分野での問題を解決することを考えるべきだと思います。 実際にGoの開発者は、今後の言語機能の拡張を考えるにあたって、Goのユーザーからの体験レポートを求めています。

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

はい、「面倒」といってたのはむしろそっちの意味です。多少のタイプ数なんて最初から気にしていません。 Pop() くらいならまだマシなのですが、スライスの途中への要素の追加削除など、もうちょっと込み入ったイディオムは、組み込み関数を用意するほどではないけれどもスライス操作のジェネリックなライブラリがあると嬉しいと思います。

一方で、ほんの2,3行で書けるコードをライブラリに頼るのが常に善かというと、そうでもありません。

イディオムなら動作を完全に理解している基本操作の組み合わせなのに対し、ライブラリだと微妙に期待と違うことがあります。

標準ライブラリにピッタリのものがない場合、サードパーティライブラリに頼るなら、コードを確認し、メンテナの実績を確認し、さらにバージョンアップのたびにそのライブラリが悪意あるユーザーに乗っ取られている危険があるので差分を確認し、、、と、イディオムなら30秒で読み書きできたコードが、ライブラリだと数時間から長期的には数日ものメンテナンスコストにつながることだってあります。

Go Proverbs

A little copying is better than a little dependency.


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

「頑なに要らないと言っている」と「いつ入れるつもりなんだ」には相当の乖離があると思うのですが。。。

「懐疑的」というのが「本当は入れるつもり無いんだろ」という意味であれば、「そもそも入れる必要があるかどうかはこれから考える」段階です。

上でも紹介した “Toward Go 2” を、 ymotongpoo さんが日本語訳してくださったのでそちらを参照してください。

www.ymotongpoo.com

Go 1がリリースされた後、私たちはGoが設計された本来の目的である、本番環境での利用に時間を費やす必要があることを認識していました。それから言語の変更そのものからあえて離れ、私たちのプロジェクトでのGoの利用や実装の改善へと注力する対象を移しました。私たちはGoをさまざまな新しいシステムにポートし、Goがより効率的に実行するようにパフォーマンスに致命的なあらゆる箇所を再実装しました。また、競合条件検出ツール(race detector)といった新しい重要なツールも追加しました。 いま、私たちはGoを大規模の本番環境に耐える品質のシステムで使う経験を5年重ねてきました。私たちはどのような機能がそのような環境に適合し、またどのような機能がそうでないかの知見をためてきました。 いまこそ、Goの進化と成長の次なる一歩をすすめ、Goの未来を計画するときです。
すべてのGoへの大きな変更は、Goが今日どのように人々に利用されているか、なぜ十分にうまくいっていないかを記載した1つ以上の体験レポートによって動機づけられるべきです。 疑う余地もなく大きな変更に対して、そのような体験レポートを見ることは多くなく、その中でも実例を伴ったものはほとんどありません。
たとえば、最近はジェネリクスについて調査しているのですが、私にはGoユーザーがジェネリクスを使って解決する必要がある詳細で具体的な問題がいまいちうまく想像できないのです。 結果として、ジェネリクスメソッド、つまりレシーバーとは区別されてパラメータ化されたメソッドをサポートするかといった設計に関する質問に答えることができません。 もし実世界での利用例が多くあれば、深刻なものを調査することによってこのような問題に答えることができるでしょう。

私は Go 1.2 から使っていますが、その頃からほとんど言語には手は加わっておらず、一方でツールチェインやランタイムの進化は凄まじいものがあります。

半年ごとのリリースが、いつも待ち遠しく、しかも一度バージョンを上げるともう1つ前のバージョンには戻りたくありません。 コンパイラが賢くなったり、プロファイラが使いやすくなったり、スタックトレースが読みやすくなったり、ランタイムやライブラリがより多くのCPUコアにうまくスケールするようになったり、GCの停止時間が数桁小さくなる事が、Goユーザーの実際の問題を大きく解決してきたのです。

Googleジェネリクスを検討するよりも優先したそれらの改善は、私にとっても確かにジェネリクスよりも優先する価値のあるものに感じられます。

Goを大して実用していないのに、Goユーザーにとって何が重要かを勝手に決めつけて、それを優先して実装しないことに文句を言うの、相当に失礼ですよ。もうやめませんか?

逆に言語にこだわらない、Goの広い分野への布教に熱心でない人は、ジェネリクスが無いことが不便な場面ではわざわざGoを選ばないでC++C#JavaやRustなどを使ってるので、 本当にジェネリクスが無いことで困ってない。
これはソースが欲しいです。現状、Goの利用シーンはかなり広がっているのに、ジェネリクスが無いことで不便な場面で他の言語にスイッチするというコストを多くのユーザーは払っているのでしょうか?(便利ライブラリやフレームワークの有無が原因ならわかりますが)

データがないので完全に印象論ですが、PythonRubyPHPJavaに比べると、Goは第n言語 (n>1) とされることが多いと思います。

私はもちろん、日本の著名なGopherの方たち、大抵Goより先にいくつも言語を使い込んでいる方たちばかりです。そしてその方たちからも、早くジェネリクスを!という声より、別に無くても、という意見を目にすることが多いです。 (日本のGoユーザーからジェネリクスを望む声があまり聞こえないことには同意いただけますよね?)

そしてGoから他の言語へのスイッチの事例は少ないですが、Goが面倒な場面ではそもそも最初からGo以外を選ぶのではないでしょうか?

僕は性能が要らない手元で動かすスクリプトPythonで書きますし、Python自体を開発するときはCを使います。Unity関連でC#のコードを読むことはありますが、ジェネリクスを多用する必要がある静的言語のプログラムを書く必要に迫られることは最近無いですね。昔はプログラミングコンテストではC++使っていましたが、そういった分野ではGoはあまり良い選択肢ではないと思います。実際にGoはISUCONに優勝したことはあっても、ICFPCで優勝したことはありません。


最後になりますが、「Goが大好きでもっとGoをいろんな人に布教したくて、そのためにはジェネリクスがないのがネックになっている」ならまだ分からなくもないのですが、「Goを大して実用もしてないしGoのゴールも理解してない部外者が勝手にGoユーザーに必要なものを決めつけて、Goユーザーがそれを対して重要じゃないということに対して批判する」のは、ナンセンスで、気分悪く、本当にうんざりです。

自分の問題を解決するのに適した言語を選びたいのではなく、自分の好きな言語機能を普及しているすべての言語に押し付けたいのですか?自宅を訪問してくる宗教勧誘並に迷惑ですよ。


(追記)

一つの機能がないことで利用シーンを大幅に狭めるのはあまりいいことだとは思えません。

に関連して、この考えかたがGoの設計思想と正反対だということを示すいくつかの資料を教えていただいたので紹介しておきます。

Simplicity is Complicated

Rob Pike 氏のスライドです。前半だけで良いので読んで下さい。

多くの言語が似たような機能をお互いに追加して近づいているのに対して、 Rob Pike 氏は異なる問題には異なるツールがあるのがいいと考えています。

  • Go is different.
  • Go does not try to be like the other languages.
  • Go does not compete on features.

GopherCon 2015: Russ Cox - Keynote - YouTube

Go Balance が取捨選択の基準の参考になります。

https://talks.golang.org/2014/hellogophers.slide#64

  • focus on the original goals

Goはゴールを設定しそこにフォーカスしている言語です。なのでそれ以外のユーザーや用途を取り込むために、フォーカスしている領域で重要とされない機能を取り込むことは、Goにとっては良いことではありません。