Go言語にはジェネリクスがない。
他言語から入ってきた方はかなりこの部分に戸惑うことが多い。
そこで、そういった方々になぜジェネリクスがないのかを説明するにあたって、自分の中でいい感じに言語化できたのでログとして残しておこうと思う。
なぜジェネリクスがないか Link to heading
ジェネリクスがない理由を説明するには以下の2つが必要だと思っている。
- 言語としてのスタンスの話
- ジェネリクスが効果を発揮する場面
それぞれ細かく説明していこうと思う。
言語としてのスタンスの話 Link to heading
何かが新しく作られるときには何かしら課題を解決しようとして生まれてくる。
Goが解決しようとした課題についてはGoの作者の生みの親の一人でもあるRob Pike氏が2012年の公演で話されている。
リンク先の講演の記事の中に、以下の点が既存の開発の辛い部分で、Goはそれらに対処しているという部分がある。
- 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 (言語間ビルド)
上記に挙げられている対処している課題を見ると、Go言語は「プログラム言語としての正しさ」という点ではなく「(言語仕様も含む)開発の簡単さ」に強くフォーカスを当てられている言語だとわかる。
そのため、新しい機能を言語に追加にあたり「プログラミング言語としての正しさ」が強くあったとしても、それを代替する手段がありかつ全体として「開発の簡単さ」が損なわれる場合は導入されない傾向がある。
ジェネリクスが効果を発揮する場面 Link to heading
ジェネリクスが便利な機能なのは間違いない。 型を値のように取り扱い、抽象化された処理を型安全に取り扱うことができる。
しかし、便利に使うためには1つ条件がある。
それは、「高度に抽象化された振る舞い」が定義できる場合になる。
それでは「高度に抽象化された振る舞い」とは何か。
例えばGUI開発におけるViewの振る舞いがある。
AndroidのListView
は「アイテムが一覧で並んでいる」という振る舞いを抽象化したViewクラスになる。
ListView
は内部で表示するものをAdapter経由で受け取るが、Adapterには型パラメータを与えることができるため、一覧の中に表示する項目を型安全に取り扱うことができる。
これは「一覧で並んでいる」という振る舞いを高度に抽象化できているため、内部で表示する項目はジェネリクスの型パラメータとして与えても違和感のない設計にできる。
GUIの開発では「羅列されたデータの中から1つ選択する」「テーブルとして表示する」の様に文字通り触る人の「振る舞い」を抽象化し、取り扱うデータを型パラメータを用いて安全に取り扱うとができるため、ジェネリクスとの相性はとてもよい。
もう1つの例としてはデータコンテナとしての振る舞いがある。
種類は色々あるが、ざっくりと以下の3つに大別できると思う。
- Collection
- Nullable
- 非同期
これらはコードを書く上で頻出する振舞いでありながら、データの構造そのものには依存しない。
そのため、振舞いを抽出して定義しつつ、ジェネリクスとして型パラメータを与えるようにすることで、抽象化された振舞いを型安全に行うことができる。
Goにおけるジェネリクス Link to heading
GoのユースケースとしてGUIを開発はあまりメジャーな対象になっていないのでデータコンテナだけの話になる。
先程ジェネリクスのメリットを説明しましたが、それらの機能を言語に組み込もうとするとどうしても言語として複雑になる(大小の話ではなく入れる入れないだと入れたほうが複雑になるという話)。
Goの言語のスタンスとしての部分で解説したとおり、Goは開発のしやすさに重点を置いており、そのしやすさは言語そのものも対象になっている。
とはいえ、同じような操作は発生するため何かしらの手段で解決はしなくてはいけない。
Goはそれらの操作に対して以下の様に対応している。
- Collection -> for文とif文
- Nullable -> if文
- 非同期 -> goroutineとchannel
CollectionとNullableに対する解決策は非常にシンプル。基本構文で対応できるならそれで良いじゃないかという解決策を取っている。
言語として機能が足りないように感じるかもしれないが、少なくともデータコンテナに対する操作としては過不足なく実現できる。
また、特にNullableのチェック漏れ等の問題には、GoはLinterを使って解決するという文化になっている。
言語仕様をシンプルに保つ理由の1つに静的解析ツールを作りやすくする目的も有るため、デファクトスタンダードのものを使いつつ、自分の目的にあった解析ツールを作ることもできる。
そういった文化により、言語の仕様自体はシンプルに保ちつつ、安全で漏れの無いコードを書くことを可能にしている。
非同期に対するgoroutineだが、これはGoの解決したかった課題というのもあり非常に高度な抽象化がされている。
使い方の詳細はここでは解説しませんが、goroutineはOSのThreadを完全に隠蔽しつつ、goroutine間でデータをやり取りするためのchannelには型をつけることができる。
ジェネリクスとしては実装していませんが、型安全に並行処理を取り扱えるようになっている。
上記の様にGoはジェネリクスがなかったとしても言語として十分に使うことができる機能を備えている。
また、一般的な開発で使うような機能は全て標準パッケージがinterfaceとして提供しているため、それらと組み合わせることで他のプログラミングでできることと過不足ない実装を実現できる。
ただ、最近は GoのFAQにも書いてあるとおり、コミュニティや言語が成熟してきたのでジェネリクスの議論も進んできている。
気になる方は是非こちらを見てどういった議論が行われているのかを確認してみると良いと思う。