ジェネリクスはなぜ必要か
Why Generics? by Ian Lance Taylor
はじめに
本記事は、先週開催されたGophercon 2019での私の講演をブログ記事にしたものです。(講演動画)
この記事は、Goにジェネリクスを追加するとはどういうことか、そしてなぜそれをすべきだと私が考えているかについて書いたものです。また、Goにジェネリクスを追加するための設計案の最新の状況についても触れます。
Goは2009年11月10日にリリースされました。それから24時間も経たないうちに、ジェネリクスについての最初のコメントが投稿されました。(そのコメントは例外についても触れていましたが、これは2010年初頭に panic と recover という形で言語に追加されました。)
3年間にわたるGoのアンケート調査で、ジェネリクスの欠如は常に言語における修正すべき問題のトップ3に挙げられてきました。
なぜジェネリクスなのか
しかし、ジェネリクスを追加するとはどういうことで、なぜそれを望むのでしょうか。
Jazayeriらの言葉を借りれば、ジェネリックプログラミングとは、型を外に括りだして、関数やデータ構造を汎用的な形で表現できるようにすることです。
これはどういう意味でしょうか。
簡単な例として、スライスの要素を逆順にしたいとします。多くのプログラムで必要になる処理ではありませんが、そう珍しいものでもありません。
まずはintのスライスだとしましょう。
func ReverseInts(s []int) {
first := 0
last := len(s)
for first < last {
s[first], s[last] = s[last], s[first]
first++
last--
}
}
とても単純ですが、こんな単純な関数であってもテストケースはいくつか書きたくなるものです。実際に書いてみたところ、バグを見つけました。すでに気づいた読者も多いことでしょう。
func ReverseInts(s []int) {
first := 0
last := len(s) - 1
for first < last {
s[first], s[last] = s[last], s[first]
first++
last--
}
}
変数 last を設定するときに1を引く必要があります。
次に、stringのスライスを逆順にしてみましょう。
func ReverseStrings(s []string) {
first := 0
last := len(s) - 1
for first < last {
s[first], s[last] = s[last], s[first]
first++
last--
}
}
ReverseInts と ReverseStrings を比べてみると、この2つの関数はパラメータの型を除いてまったく同じであることがわかります。これに驚く読者はいないでしょう。
Goを始めたばかりの人が驚くのは、あらゆる型のスライスに対して動作する単純な Reverse 関数を書く方法がないということです。
他の多くの言語ではこの種の関数を書けます。
PythonやJavaScriptのような動的型付け言語では、要素の型をわざわざ指定しなくても関数を書くだけで済みます。しかしGoは静的型付け言語であり、スライスの正確な型とその要素の型を書き下す必要があるため、このやり方はうまくいきません。
C++、Java、Rust、Swiftなど、他の多くの静的型付け言語は、まさにこの種の問題に対処するためにジェネリクスをサポートしています。
現在のGoにおけるジェネリックプログラミング
では、Goの人々はこの種のコードをどのように書いているのでしょうか。
Goでは、インターフェース型を使い、渡したいスライス型にメソッドを定義することで、異なるスライス型に対して動作する単一の関数を書けます。標準ライブラリの sort.Sort 関数はこのように動作しています。
言い換えると、Goのインターフェース型はジェネリックプログラミングの一形態です。インターフェースは異なる型に共通する側面を捉えてメソッドとして表現することを可能にします。そうすればそれらのインターフェース型を使う関数を書け、そのメソッドを実装するあらゆる型に対してその関数が動作するようになります。
しかしこのアプローチは私たちが望むものには届きません。インターフェースを使う場合、メソッドは自分で書かなければなりません。スライスを逆順にするためだけに、いくつかのメソッドを持つ名前付きの型を定義しなければならないのは不格好です。しかも書くメソッドはどのスライス型に対してもまったく同じものになるため、ある意味では重複したコードを移動して凝縮しただけであり、なくしたわけではありません。インターフェースはジェネリクスの一形態ではありますが、私たちがジェネリクスに求めるものすべてを与えてはくれないのです。
自分でメソッドを書く必要をなくす別のインターフェースの使い方として、言語自体がある種の型に対してメソッドを定義するという方法も考えられます。これは現在の言語ではサポートされていませんが、たとえば、あらゆるスライス型が要素を返す Index メソッドを持つと定義することもできるでしょう。しかし、実際にそのメソッドを使うには空のインターフェース型を返す必要があり、そうすると静的型付けの恩恵をすべて失ってしまいます。さらに厄介なことに、同じ要素型を持つ2つの異なるスライスを受け取る、あるいはある要素型のマップを受け取ってその同じ要素型のスライスを返すようなジェネリック関数を定義する方法もなくなってしまいます。Goが静的型付け言語であるのは、それによって大規模なプログラムを書きやすくなるからです。ジェネリクスの恩恵を得るために静的型付けの恩恵を失いたくはありません。
別のアプローチとして reflect パッケージを使ってジェネリックな Reverse 関数を書くという方法もありますが、これは書くのが面倒で実行も遅いため、実際にそうする人はほとんどいません。このアプローチでは明示的な型アサーションも必要で、静的な型検査もありません。
あるいは、型を受け取ってそのスライス用の Reverse 関数を生成するコードジェネレータを書くという方法もあります。実際にそうしたことを行うコードジェネレータはいくつも存在します。しかしこれは Reverse を必要とするすべてのパッケージに新たな手順を追加することになり、異なるコピーをすべてコンパイルしなければならないためビルドが複雑になり、また元のソースのバグを修正するにはすべてのインスタンスを再生成する必要があります。そのインスタンスの一部はまったく別のプロジェクトにあることもあります。
これらのアプローチはどれも十分に不格好なので、Goでスライスを逆順にする必要がある人のほとんどは、必要な特定のスライス型のための関数を単に書いているだけだと思います。そしてその関数のテストケースを書いて、最初に私がしたような単純なミスをしていないか確認する必要があり、そのテストを日常的に実行する必要もあります。
どのようなやり方をするにせよ、要素の型以外はまったく同じに見える関数のために多くの余分な作業が必要になるということです。それができないという話ではありません。明らかに実現可能ですし、Goのプログラマはすでにそうしています。ただ、もっと良い方法があってしかるべきだ、というだけの話です。
Goのような静的型付け言語にとって、そのより良い方法とはジェネリクスです。先に書いたように、ジェネリックプログラミングとは型を外に括りだして関数やデータ構造を汎用的な形で表現できるようにすることです。これはまさに私たちがここで求めているものです。
ジェネリクスがGoにもたらすもの
Goのジェネリクスにまず、そして最も重要なものとして求めるのは、スライスの要素型を気にせずに Reverse のような関数を書けるようにすることです。要素の型を外に括りだしたいのです。そうすれば関数を一度だけ書き、テストも一度だけ書いて、go-gettableなパッケージに収め、必要なときにいつでも呼び出せます。
さらに言えば、これはオープンソースの世界なので、誰か他の人が Reverse を一度書いてくれれば、私たちはその実装を使わせてもらうこともできます。
ここで断っておきたいのですが、「ジェネリクス」という言葉は多くの異なる意味を持ちえます。本記事で「ジェネリクス」と言うとき、私が意味しているのはたった今説明した内容です。特に、C++言語にあるようなテンプレートのことは意味していません。テンプレートは、ここに書いたことよりもかなり多くのことをサポートしています。
Reverse については詳しく見てきましたが、ジェネリックに書けそうな関数は他にもたくさんあります。たとえば次のようなものです。
- スライス内の最小/最大の要素を見つける
- スライスの平均/標準偏差を求める
- マップの和集合/積集合を計算する
- ノードとエッジからなるグラフの最短経路を見つける
- スライスやマップに変換関数を適用し、新しいスライスやマップを返す
これらの例は他の多くの言語で利用可能です。実のところ、このリストはC++の標準テンプレートライブラリを眺めながら書いたものです。
Goの強力な並行処理サポートに特有の例もあります。
- タイムアウト付きでチャネルから読み込む
- 2つのチャネルを1つのチャネルに結合する
- 関数のリストを並行に呼び出し、結果のスライスを返す
- Contextを使って関数のリストを呼び出し、最初に完了した関数の結果を返しつつ、残りのゴルーチンをキャンセルして後始末する
これらの関数はすべて、異なる型に対して何度も書かれているのを見てきました。Goでそれらを書くこと自体は難しくありません。しかし、あらゆる値の型に対して動作する、効率的でデバッグ済みの実装を再利用できたら素晴らしいことです。
念のため言っておくと、これらは単なる例に過ぎません。ジェネリクスを使うことでより簡単かつ安全に書ける汎用的な関数は他にもたくさんあります。
また、先に書いた通り、対象は関数だけではありません。データ構造もそうです。
Goには言語に組み込まれた2つの汎用的なジェネリックデータ構造、スライスとマップがあります。スライスとマップはどんなデータ型の値でも保持でき、格納・取得される値には静的な型検査が働きます。値はそのままの形で格納され、インターフェース型に変換されて格納されるわけではありません。つまり []int があるとき、そのスライスはintに変換されたインターフェースではなく、int自体を直接保持しています。
スライスとマップは最も便利なジェネリックデータ構造ですが、それだけではありません。他にもいくつか例を挙げます。
- 集合(Set)
- 効率的な挿入とソート順での走査ができる、自己平衡木
- 同じキーを複数持てるマルチマップ
- 単一のロックを使わずに並行した挿入と検索をサポートする並行ハッシュマップ
ジェネリックな型を書けるようになれば、スライスやマップと同じ型検査の利点、つまりコンパイラが保持する値の型を静的に型検査でき、値をインターフェース型ではなくそのままの形で格納できるという利点を持つ、こうした新しいデータ構造を定義できるようになります。
また、先に挙げたようなアルゴリズムをジェネリックなデータ構造に適用することもできるようになるはずです。
これらの例はすべて Reverse と同じように、パッケージ内に一度だけ書かれたジェネリックな関数やデータ構造として、必要になるたびに再利用されるべきものです。これらはスライスやマップと同じように、空のインターフェース型の値を格納するのではなく、特定の型を格納し、その型がコンパイル時に検査されるべきです。
以上が、Goがジェネリクスから得られるものです。ジェネリクスは、コードを共有し、プログラムをより簡単に構築できるようにする強力な構成要素を与えてくれます。
これがなぜ検討する価値のあることなのか、説明できていれば幸いです。
利点とコスト
しかし、ジェネリクスは、レモネードの泉に毎日太陽が降り注ぐ理想郷「ビッグ・ロック・キャンディ・マウンテン」からやって来るわけではありません。言語へのあらゆる変更にはコストが伴います。Goにジェネリクスを追加すれば言語がより複雑になることは間違いありません。言語へのどんな変更についても言えることですが、利点を最大化しコストを最小化することについて議論する必要があります。
Goでは、自由に組み合わせられる独立した直交性のある言語機能によって複雑さを抑えることを目指してきました。個々の機能をシンプルにすることで複雑さを減らし、それらを自由に組み合わせられるようにすることで機能の利点を最大化しています。ジェネリクスについても同じことをしたいと考えています。
これをより具体的にするために、私たちが従うべきいくつかの指針を挙げます。
新しい概念を最小限にする
言語に追加する新しい概念はできる限り少なくすべきです。つまり、新しい構文や新しいキーワード・名前は最小限にとどめるということです。
書き手が負う複雑さ
できる限り、複雑さはジェネリックなパッケージを書くプログラマが負うべきです。パッケージの利用者にジェネリクスのことを気にしてほしくありません。これは、ジェネリック関数を自然な形で呼び出せるべきだということ、そしてジェネリックなパッケージの使い方に誤りがあった場合、それが理解しやすく修正しやすい形で報告されるべきだということを意味します。ジェネリックなコードへの呼び出しをデバッグしやすいことも重要です。
書き手と使い手の独立性
同様に、ジェネリックなコードの書き手と使い手の関心事を切り分けやすくし、それぞれが独立してコードを開発できるようにすべきです。異なるパッケージにある通常の関数の書き手と呼び出し側が互いの都合を気にする必要がないのと同じように、両者は互いが何をしているかを気にする必要がないようにすべきです。当たり前のことのように聞こえますが、他のあらゆるプログラミング言語のジェネリクスにこれが当てはまるわけではありません。
短いビルド時間と速い実行速度
当然ながら、できる限り、Goが今日私たちに与えてくれている短いビルド時間と速い実行速度を維持したいと考えています。ジェネリクスは往々にして、ビルドの速さと実行の速さの間にトレードオフを持ち込みがちです。できる限り両方とも実現したいのです。
Goの明快さとシンプルさを保つ
最も重要なこととして、今日のGoはシンプルな言語です。Goのプログラムは通常、明快で理解しやすいものです。この領域を模索してきた長いプロセスの大部分は、その明快さとシンプルさを保ちながらどのようにジェネリクスを追加するかを理解しようとすることに費やされてきました。既存の言語をまったく別の何かに変えてしまうことなく、その言語にうまく収まる仕組みを見つけ出す必要があります。
これらの指針は、Goにおけるジェネリクスのどのような実装にも当てはまるべきものです。今日皆さんに伝えたい最も重要なメッセージはこれです。ジェネリクスは言語に大きな利点をもたらしうるが、それをする価値があるのは、Goが依然としてGoらしさを保っている場合に限られる。
設計のドラフト
幸いなことに、それは実現可能だと私は考えています。この記事を締めくくるにあたり、なぜジェネリクスが欲しいのか、それに求められる要件は何かという議論から、どのようにすればそれを言語に追加できると私たちが考えているかという設計の話に、簡単に移りたいと思います。
2022年1月追記 本記事は2019年に書かれたもので、最終的に採用されたジェネリクスの仕様とは異なる内容を説明しています。最新の情報については、言語仕様の型パラメータに関する説明と、ジェネリクスの設計ドキュメントを参照してください。
今年のGopherconで、Robert Griesemerと私はGoにジェネリクスを追加するための設計ドラフトを公開しました。詳細はそのドラフトを参照してください。ここでは主な点についていくつか説明します。
この設計におけるジェネリックな Reverse 関数は次のようになります。
func Reverse (type Element) (s []Element) {
first := 0
last := len(s) - 1
for first < last {
s[first], s[last] = s[last], s[first]
first++
last--
}
}
関数の本体はまったく変わっていないことに気づくでしょう。変わったのはシグネチャだけです。
スライスの要素型が外に括りだされました。それは今や Element という名前が付き、私たちが 型パラメータ と呼ぶものになっています。スライスパラメータの型の一部であったものが、別個の追加の型パラメータになったのです。
型パラメータを持つ関数を呼び出すには、一般的な場合、型引数を渡します。これは型であるという点を除けば他の引数と同じです。
func ReverseAndPrint(s []int) {
Reverse(int)(s)
fmt.Println(s)
}
この例で Reverse の後に見える (int) がそれです。
幸い、この例も含め多くの場合において、コンパイラは通常の引数の型から型引数を推論できるため、型引数についてまったく言及する必要はありません。
ジェネリック関数を呼び出すことは、他のどんな関数を呼び出すのとも変わらないように見えます。
func ReverseAndPrint(s []int) {
Reverse(s)
fmt.Println(s)
}
言い換えると、ジェネリックな Reverse 関数は ReverseInts や ReverseStrings よりも多少複雑になってはいますが、その複雑さは呼び出し側ではなく関数の書き手が負うことになります。
コントラクト
Goは静的型付け言語であるため、型パラメータの型について話す必要があります。この メタ型 は、ジェネリック関数を呼び出すときにどのような型引数が許されるか、そしてジェネリック関数が型パラメータの値に対してどのような操作を行えるかをコンパイラに伝えます。
Reverse 関数はどんな型のスライスに対しても動作します。Element 型の値に対して行っている唯一の操作は代入であり、これはGoのどんな型に対しても機能します。このような、非常によくあるジェネリック関数の場合、型パラメータについて特別なことを言う必要はありません。
別の関数を簡単に見てみましょう。
func IndexByte (type T Sequence) (s T, b byte) int {
for i := 0; i < len(s); i++ {
if s[i] == b {
return i
}
}
return -1
}
現在、標準ライブラリの bytes パッケージと strings パッケージの両方に IndexByte という関数があります。この関数は、シーケンス s の中の b のインデックスを返すもので、s は string または []byte のどちらかです。この単一のジェネリック関数を使えば、bytes と strings パッケージにある2つの関数を置き換えられます。実際にはそこまでしないかもしれませんが、これは有用な単純な例です。
ここでは、型パラメータ T が string や []byte のように振る舞うことを知る必要があります。T に対して len を呼び出せ、インデックスでアクセスでき、インデックス操作の結果をbyteの値と比較できるということです。
これをコンパイルできるようにするには、型パラメータ T 自体に型が必要です。それはメタ型ですが、複数の関連する型を記述する必要がある場合があること、そしてそれがジェネリック関数の実装とその呼び出し側との関係を記述するものであることから、私たちは T の型を実際には コントラクト と呼んでいます。ここではそのコントラクトは Sequence という名前になっています。これは型パラメータのリストの後に現れます。
この例における Sequence コントラクトは次のように定義されます。
contract Sequence(T) {
T string, []byte
}
これは単純な例なので、内容もかなり単純です。型パラメータ T は string か []byte のどちらかになれるということです。ここでの contract は新しいキーワードになるかもしれませんし、パッケージスコープで認識される特別な識別子になるかもしれません。詳細は設計ドラフトを参照してください。
Gophercon 2018で発表した設計を覚えている方なら、このコントラクトの書き方がずっとシンプルになっていることがわかるでしょう。以前の設計に対しては、コントラクトが複雑すぎるという多くのフィードバックをいただき、それを踏まえるよう努めました。新しいコントラクトは、書くのも読むのも理解するのも、はるかに簡単になっています。
コントラクトを使うと、型パラメータの基底型を指定したり、型パラメータが持つべきメソッドを列挙したりできます。また、異なる型パラメータ同士の関係を記述することもできます。
メソッドを伴うコントラクト
もう一つ単純な例を見てみましょう。String メソッドを使って、s のすべての要素の文字列表現からなる []string を返す関数です。
func ToStrings (type E Stringer) (s []E) []string {
r := make([]string, len(s))
for i, v := range s {
r[i] = v.String()
}
return r
}
とても単純です。スライスを走査し、各要素に対して String メソッドを呼び出し、その結果の文字列のスライスを返します。
この関数は、要素の型が String メソッドを実装していることを要求します。Stringer コントラクトがそれを保証します。
contract Stringer(T) {
T String() string
}
このコントラクトは単に、T が String メソッドを実装していなければならないと言っているだけです。
このコントラクトは fmt.Stringer インターフェースに似ていると気づくかもしれませんが、ToStrings 関数の引数は fmt.Stringer のスライスではないということは指摘しておく価値があります。それはある要素型のスライスであり、その要素型が fmt.Stringer を実装しているということです。要素型のスライスと fmt.Stringer のスライスとでは、通常メモリ上の表現が異なり、Goはそれらの間の直接的な変換をサポートしていません。だからこそ、fmt.Stringer が存在していても、この関数を書く価値があるのです。
複数の型を伴うコントラクト
複数の型パラメータを持つコントラクトの例を見てみましょう。
type Graph (type Node, Edge G) struct { ... }
contract G(Node, Edge) {
Node Edges() []Edge
Edge Nodes() (from Node, to Node)
}
func New (type Node, Edge G) (nodes []Node) *Graph(Node, Edge) {
...
}
func (g *Graph(Node, Edge)) ShortestPath(from, to Node) []Edge {
...
}
ここではノードとエッジから構築されるグラフを記述しています。グラフに特定のデータ構造を要求しているわけではありません。代わりに、Node 型はその Node に接続するエッジのリストを返す Edges メソッドを持たなければならないと言っています。そして Edge 型は、その Edge が接続する2つの Node を返す Nodes メソッドを持たなければなりません。
実装は省略していますが、これは Graph を返す New 関数のシグネチャと、Graph の ShortestPath メソッドのシグネチャを示しています。
ここで重要なのは、コントラクトが単一の型だけに関するものではないということです。コントラクトは2つ以上の型の間の関係を記述できます。
順序付け可能な型
意外と多いGoへの不満の一つに、Min 関数がないというものがあります。あるいは同様に Max 関数がないというのもあります。それは、有用な Min 関数はあらゆる順序付け可能な型に対して動作するべきであり、つまりそれはジェネリックでなければならないからです。
Min を自分で書くこと自体はごく簡単ですが、有用なジェネリクスの実装であれば、それを標準ライブラリに追加できるはずです。私たちの設計ではこのようになります。
func Min (type T Ordered) (a, b T) T {
if a < b {
return a
}
return b
}
Ordered コントラクトは、型 T が順序付け可能な型でなければならないと言っています。つまり、小なり、大なりといった演算子をサポートしているということです。
contract Ordered(T) {
T int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
string
}
Ordered コントラクトは、言語で定義されているすべての順序付け可能な型を単に列挙したものです。このコントラクトは、列挙された型のいずれか、あるいはそれらのいずれかを基底型とする名前付きの型を受け付けます。基本的には、小なり演算子を使えるあらゆる型ということです。
すべての演算子に対して機能する新しい記法を考案するよりも、小なり演算子をサポートする型を単に列挙するほうがずっと簡単だとわかりました。そもそもGoでは、組み込みの型だけが演算子をサポートしています。
この同じアプローチは、どの演算子に対しても使えますし、より一般的には、組み込みの型を扱うことを意図したジェネリック関数のコントラクトを書く際にも使えます。これによって、ジェネリック関数の書き手は、その関数が使われることを想定している型の集合を明確に指定できます。またジェネリック関数の呼び出し側は、使おうとしている型に対してその関数が適用可能かどうかを明確に把握できます。
実際には、このコントラクトはおそらく標準ライブラリに入ることになるでしょうから、実際の Min 関数(これもおそらく標準ライブラリのどこかに入ることになるでしょう)は次のようになります。ここでは contracts パッケージで定義された Ordered コントラクトを単に参照しています。
func Min (type T contracts.Ordered) (a, b T) T {
if a < b {
return a
}
return b
}
ジェネリックなデータ構造
最後に、単純なジェネリックデータ構造として二分木を見てみましょう。この例では木が比較関数を持つため、要素の型に対する要件はありません。
type Tree (type E) struct {
root *node(E)
compare func(E, E) int
}
type node (type E) struct {
val E
left, right *node(E)
}
新しい二分木を作る方法は次の通りです。比較関数は New 関数に渡します。
func New (type E) (cmp func(E, E) int) *Tree(E) {
return &Tree(E){compare: cmp}
}
非公開のメソッドは、v を保持しているスロットへのポインタ、あるいは v が入るべき木の中の位置へのポインタを返します。
func (t *Tree(E)) find(v E) **node(E) {
pn := &t.root
for *pn != nil {
switch cmp := t.compare(v, (*pn).val); {
case cmp < 0:
pn = &(*pn).left
case cmp > 0:
pn = &(*pn).right
default:
return pn
}
}
return pn
}
ここでの詳細はそれほど重要ではありません。特にこのコードはテストしていないのでなおさらです。単純なジェネリックデータ構造を書くとはどういうことかを示そうとしているだけです。
これは木がある値を含んでいるかどうかを調べるコードです。
func (t *Tree(E)) Contains(v E) bool {
return *t.find(e) != nil
}
これは新しい値を挿入するコードです。
func (t *Tree(E)) Insert(v E) bool {
pn := t.find(v)
if *pn != nil {
return false
}
*pn = &node(E){val: v}
return true
}
型 node が型引数 E を持っていることに注目してください。これがジェネリックデータ構造を書くということです。ご覧の通り、あちこちに型引数が散りばめられていることを除けば、普通のGoのコードを書くのと変わりません。
この木を使うのはとても簡単です。
var intTree = tree.New(func(a, b int) int { return a - b })
func InsertAndCheck(v int) {
intTree.Insert(v)
if !intTree.Contains(v) {
log.Fatalf("%d not found after insertion", v)
}
}
それがあるべき姿です。ジェネリックデータ構造を書くのは少し難しくなります。サポートする型のために型引数を明示的に書き出さなければならないことが多いからです。しかし、それを使う側から見れば、できる限り、普通の非ジェネリックなデータ構造を使うのと変わらないようにしています。
次のステップ
私たちはこの設計を試せるよう、実際の実装に取り組んでいます。設計を実際に試してみて、書きたい種類のプログラムを本当に書けるのかを確認できることが重要です。私たちが望んでいたほど速くは進んでいませんが、これらの実装が利用可能になり次第、さらに詳細をお伝えします。
Robert Griesemerは go/types パッケージを変更する予備的なCLを書きました。これにより、ジェネリクスとコントラクトを使ったコードが型検査を通るかどうかをテストできます。現時点では未完成ですが、単一のパッケージに対してはほぼ動作しており、引き続き作業を進めていきます。
この実装や今後の実装を使って皆さんにお願いしたいのは、実際にジェネリックなコードを書いて使ってみて、どうなるかを見てもらうことです。人々が必要なコードを書けること、そして期待通りにそれを使えることを確認したいのです。もちろん最初からすべてがうまくいくわけではなく、この領域を探求していく中で物事を変えなければならないこともあるでしょう。そして念のため言っておくと、構文の詳細についてよりも、セマンティクスについてのフィードバックのほうに私たちははるかに強い関心を持っています。
以前の設計にコメントをくださった皆さん、そしてGoにおけるジェネリクスがどのような形になりうるかを議論してくださった皆さんに感謝したいと思います。私たちはすべてのコメントに目を通しており、皆さんがこのために注いでくださった労力に大変感謝しています。その労力なくして、私たちが今日この場所にいることはなかったでしょう。
私たちの目標は、言語を使いにくいほど複雑にすることもなく、Goらしさを失わせることもなく、今日議論してきたような種類のジェネリックなコードを書けるようにする設計にたどり着くことです。この設計がその目標に向けた一歩になることを願っていますし、私たちや皆さんの経験から何がうまくいって何がうまくいかないのかを学びながら、引き続き調整を続けていくつもりです。もしその目標に到達できれば、それは今後のGoのバージョンに向けて提案できるものになるでしょう。
By Ian Lance Taylor