世界を動かす技術を、日本語で。

Go: ジェネリックメソッドのサポート

概要

  • Go言語の メソッド に対する ジェネリック(型パラメータ) 導入提案
  • インターフェースメソッド には影響せず、 具体型メソッド のみが対象
  • シンタックス変更 は最小限で、既存コードと 後方互換性 を維持
  • 呼び出し・型推論・メソッド式 も期待通り動作
  • 実装・仕様変更 は比較的シンプルだが、エクスポートフォーマット更新が必要

Goにおけるジェネリックメソッド導入提案

  • 具体型メソッド (receiver付き関数)に 型パラメータ 導入を提案
  • 関数宣言 と同様に、メソッド宣言でも [TypeParameters] を許可
  • 例:func (s *S) m[P any](x P) { ... } のような宣言が可能
  • インターフェースメソッド には型パラメータを導入しない方針
    • 理由:Goのインターフェース実装は動的であり、どの型パラメータのインスタンスが必要かコンパイル時に決定できないため
  • ジェネリックメソッドインターフェースの実装要件 を満たさない

ジェネリックメソッドの意義と利点

  • メソッド はインターフェース実装以外にも、型に紐づく関数として コード整理可読性向上 に有用
  • x.a().b().c() のような メソッドチェーン による直感的な記述が可能
  • 関数型プログラミング 的なc(b(a(x)))よりも自然な記述
  • Goユーザーからの要望 が強く、既に多くの支持を集めている

提案される文法変更

  • メソッド宣言 のシンタックスを以下のように変更
    • 旧:func Receiver MethodName Signature [FunctionBody]
    • 新:func Receiver MethodName [TypeParameters] Signature [FunctionBody]
  • 例:func (r *Reader) Read[E any]([]E) (int, error) { ... }
  • 型パラメータのスコープ はメソッド名以降~メソッド本体終了まで
  • メソッド式・メソッド値 もジェネリック関数として扱う
  • 呼び出し時 は型パラメータを明示または推論で指定可能

実装例

  • 具体型S に対しジェネリックメソッドmを定義
    • type S struct { ... }
    • func (*S) m[P any](x P) { ... }
  • 呼び出し例
    • s.m[int](42) // 明示的型指定
    • s.m(x) // 引数xから型推論
  • ジェネリック型G[P] にもメソッド宣言可能
    • func (*G[P]) m[Q any](x Q) { ... }
  • インターフェースI の実装例
    • type I interface { m(string) }
    • type G[P any] struct{ ... }
    • func (G[P]) m(P) { ... }
    • var g G[string]
    • var _ I = g // G[string]による実装は有効
    • type H struct{ ... }
    • func (H) m[P any](P) { ... }
    • var _ I = h // Hは実装不可(型パラメータが一致しないため)

仕様・リフレクション・制限

  • インターフェースメソッド は非対応
  • リフレクション(reflectパッケージ) 経由で未インスタンス化のジェネリックメソッドにはアクセス不可
  • 既存のメソッド呼び出し・メソッド式 との互換性維持

実装・コンパイラ・仕様変更

  • パーサ は既にジェネリックメソッド構文を許容(現状はエラーを出すだけ)
  • 型チェッカー は制限解除などの調整が必要
  • コンパイラバックエンド は、非インターフェースreceiver経由のメソッド呼び出しは静的に解決可能
    • メソッド呼び出しを関数呼び出しへ変換
    • ジェネリックメソッド呼び出しも対応
  • エクスポート・インポートデータ形式 の更新が最も影響大
    • 多数のツール・リポジトリで同期的な対応が必要

まとめ

  • Go言語のメソッド文法拡張 による ジェネリックメソッド 導入提案
  • インターフェースとの分離 で実装・仕様の複雑化を回避
  • ユーザー利便性向上後方互換性維持 を両立
  • 実装コスト は限定的だが、エコシステム全体の調整が必要

Hackerたちの意見

彼らが必要ないって言ってたことを、少しずつ実装していってるね。

自分が間違ってるかもしれないって気づいて、変わろうとするのは悪いことじゃないよね。

もちろん、最初のGo発表を見返すと、どうやってやるかが分かったらジェネリクスが必要になるって言ってたよね。そして、最初のバージョンのジェネリクスが出た時も、どうやってやるかが分かったら、ジェネリックメソッドが後で追加されるって言われてた。だから、ここでは当てはまらないよ。必要性はずっと認識されてたんだ。

彼らはジェネリクスをやりたくないって言ったわけじゃなくて、時間をかけてちゃんとやりたいって言ってたんだ。どれだけ「正しかった」かは議論の余地があるけど、これで少し近づいたと思う。彼らが避けたかった方法で「間違ってた」わけじゃないと思うし(Javaのジェネリクスに関する問題を前例として挙げてたけど、詳細は忘れちゃった)。

「彼ら」が「私たち」にジェネリクスは必要ないと言ったのはどこ?それって悪意のある解釈か誤解、あるいはストローマンみたいだね。誰かが指摘したように、彼らはユースケースを理解するまでジェネリクスを延期したんだ。別の言語(例えばJava)のジェネリクス実装は、仕様と実装の半分を占めるから、Goはそんなものを望んでなかった。

無料で貰ったものに文句言うのってさ。

Goの開発を見ていると、Javaの開発を再体験しているような感じだね(最初はジェネリックがなかったし)。でも、数年じゃなくて数十年かかってるけど。2030年代にはGoがエラーハンドリングシステムを実装するのが待ちきれないよ!

これいいね。データアクセス方法に役立ちそう!批判してる人たちもいるけど、最初のジェネリクス提案の時から「今じゃない」って言われてたけど、「決してやらない」ってわけじゃなかった。実装に関する疑問もあったし、彼らは大きなチームじゃないから、少しずつやって、ちゃんと仕上げようとしてるんだ。

ゴファーは普通かなり速いけど、年寄りのカメの方がマスコットにはいいかもね?

批判者たちについて、最初のジェネリクス提案の時点で「今はダメ」と言われていたが、「決してダメ」とは言われていない。え?その投稿はGoのFAQを引用して、「Goがジェネリックメソッドを追加することはないと予想していない」と言ってる。元のジェネリクス提案についても「それなら、なぜメソッドが必要なのかがずっと不明確になる」といった議論があったよ。(いくつかの文脈を省略してるけど、意味は変わらないと思う。)それは「今はダメ」よりも「決してダメ」に近い気がする。投稿のサブタイトルは「見解の変化」でもあるし。

Goでジェネリクスを使おうとした時、ジェネリックメソッドがないのには本当に驚いた。実際に実装されてるのを見るのは嬉しいね。

これらのメソッドがインターフェースを実装してないことに気づいたときの驚きに置き換えられるね。それでも、個人的には、機能の半分がないよりはマシだと思う。

これは、他の言語からGoに来る人たちにとって、ジェネリクスの大きなギャップを埋めるものだと思うから、この方向性には完全に賛成だよ。どこでも使えってわけじゃないけど、使う必要があるなら、モジュールレベルのジェネリック関数を呼ぶよりは、構造体に持っておいた方がいいよね。

言語の機能とユーザーの期待の間にあるギャップを追いかけるのが、Goのリーダーシップにおける最大の誤りだったし、今もそうだね。

しつこい命令には、アイデンティティの降伏よりも強い反応が必要だね。

Goにとって悲しい日だ。博士たちが勝った、シンプルさは死んだ。

100%同意! https://itnext.io/go-evolves-in-the-wrong-direction-7dfda8a1...

自分のコードベースで実際に使わない限り、それは死んだとは言えないよ。どんな機能でも、使うかどうかは選べるからね。

Goのコードの可読性が確実に下がるね。

OMG。ライブラリを再コードしなきゃ。

今日はめっちゃ嬉しいサプライズ!変なパッケージAPIを使わなきゃいけなかった回数は数えきれないよ。

これで、ずっと夢見てたモナドライブラリがやっと作れる!怖がっていいよ。

家にはもうモナドがあるからね(return X, err)。

Goのモナドライブラリには、ほんとに一つの名前しかないよね…

エクセプションモナドって作れないかな?友達のために聞いてるんだけど。

残念ながら、この変更でモナドを表現することはまだできないね。ジェネリックメソッドがインターフェースを実装できないから。多分、こんな感じのが欲しいんだろうね:type Monad[T any] interface { Bind[U any](func(T) Monad[U]) } でも、これにはBindメソッドがジェネリックである必要があって、それはインターフェースではまだ許可されてないんだよね。

Goは、そういうジェネリックインターフェースメソッドをサポートしてないんだ。なぜなら、それをどう実装するか(呼び出し)わからないから、少なくとも効率的に実装する方法がわからないんだ。正直、この議論はよくわからない。リンクされている[1]のディスカッションを読んだけど、モノモーフィゼーションのアプローチ(コンパイル時、リンク時、またはJITでの実行時)は明らかに難しいか不可能だろうけど、実行時リフレクションを使わない理由は主に遅いからだよね。でも、実行時リフレクションこそが今のところの回避策なんだ。アイデンティティの例で言うと、インターフェースを基本的にIdentity(any) anyにコンパイルして、呼び出し時に戻り値の型をTにキャストするっていうのはどうかな?プリミティブな非ポインタ型はちょっと厄介だけど、もしジェネリックメソッドがポインタ型に制限されてたとしても、それは何もないよりはマシだよね。そして、その型の数は比較的少ないから、実装がコンパイルされるときに、適用可能な全てのプリミティブ型のメソッド実装をインスタンス化して、リンク時に必要ないものは削除することもできるかもしれない。もちろん、見落としている詳細がある可能性もあるけど。