Blog

Go 1.26 の自己参照型制約って結局なんのために使うの?

こんにちは。ニフティ株式会社の statiolake です。

最近プロダクトに Go を採用したこともあって、なんだかチーム内で Go に対する関心が高まっています。最近 Go 1.26 がリリースされたということで、チームで新機能を眺める会をしていました。

さて、今回のリリースにはいくつかのコア言語機能の変更が含まれているのですが、その中の一つに「ジェネリック型がその型パラメータリスト中で自分自身を参照できるようになった」という変更があります。

一見すると型としての表現力上がったように思える追加機能です。がしかし、考えれば考えるほど「本当にこれが必要な場面ってある?」「公式の例、自己参照なしでも同じことできそうじゃない?」という議論になりました。 そこでもう少し詳しく調べてみたところ、なかなか面白い内容だと感じたので記事にしてみます。

忙しい人向け

以下が、今のところの私の理解です。

  • Go 1.26 から、ジェネリック型の定義の中で自分自身を参照できるようになった
  • ただし公式リリースノートの Adder の例は自己参照なし (単なる A any) でも問題にならない
  • 実際に自己参照が必要になるのは、ジェネリックなインターフェースとカスタム型が相互に参照し合う場合
  • それも型システムとして本質的に必要だったというよりは、Go のメソッドの制約をインターフェース側で回避するための workaround として機能する

…ただ、あらかじめ逃げ道を作っておくのですが、私は Go や型理論のことをよく知っているわけではないので、完全に正しく理解できている自信はありません。

定義の中で自分自身を参照するとは

まず、新機能を確認しましょう。Go 1.26 のリリースノートには次のような例があります。

ここで重要なのは type Adder[A Adder[A]] interface { ... } という部分です。Adder というインターフェースの定義がまだ完了していない段階で、その型パラメータの制約として Adder 自身が登場しています。Go 1.25 以前では、このようにインターフェースの定義途中で自分自身を型パラメータの制約に使うことはコンパイルエラーになっていました。これが 1.26 で解禁された、というのが今回の変更です。

もやもや: 公式の Adder 例、自己参照なしでも書けないか?

正直に言います。最初に見たとき、「インターフェース側は A any にして、使う側で制約をかければ済むのでは?」と思いました。

試してみると、これで普通に動きます。Go 1.25 でもバッチリです。

ではどういうときに本当に必要になるのでしょうか?

必要なのはどういうときか?

それなりに考えましたが、自力では思いつけなかったため、調べました。するとそもそも実装されるきっかけになったと思しき Go の issue #68162 が見つかり、ここに答えがありました。

結論だけ簡単にお伝えすると、まず型の定義側に型制約を書かざるを得ないケースが存在して、さらにその型制約が相互再帰的に自分自身に返ってくる場合に必要になるということでした。難しい。

まず、型の定義側に型制約を書かざるを得ないケースとはどういうことでしょうか?

通常 Go のメソッドはジェネリックになることができませんが、型変数を一切扱えないとなると、ジェネリックな構造体に対してメソッドを実装できないということになってしまい、困ります。そのためレシーバだけはジェネリックになれるようです。しかし、関数と違ってその場で型制約を表現することができません。そのため、メソッド内でレシーバのジェネリクスに対して何らかの要求をしたい場合には、構造体の定義時点でその制約を課しておく必要があるということでした。1

次に、その型制約が相互再帰的に自分自身に返ってくるとはどういう時でしょうか?

こちらについては、次の節から具体例で見ていきたいと思います。

必要なセットアップ

まず、「比較可能な要素 (Element) のペア」を表す ElementPair という構造体を考えてみましょう。

ここで、FirstIsSmaller() の中で ep.First.Less(...) を呼んでいます。すると EElement である必要があります。ところが FirstIsSmaller がメソッドであるため、レシーバーである ElementPair[E]E に対して E Element[E] という制約を直接課すことはできません

ここでさきほどの議論の 1 つ目の前提が満たされ、ElementPair には型定義の時点で E Element[E] という制約がつくことになりました。

ただし、ここまでは新機能の出番はありません。

相互再帰をすると問題が起きる

問題が起きるのは、Element のメソッドが ElementPair を返したい場合です。

Zip の戻り値として ElementPair[E] を書くためには、ElementPair の型パラメータ制約である E Element[E] を満たす必要があります。そのため Element のインターフェース定義の時点で EElement[E] を満たしていなければならない。

ここでさきほどの前提の 2 つ目が満たされます。結果、type Element[E Element[E]] という自己参照制約が現れてしまいました。これが Go 1.26 で初めて書けるようになったものです。

相互参照の流れを整理するとこうなります。

  1. ElementPair[E]E Element[E] を要求する
  2. Element の中で ElementPair[E] を参照したい → E Element[E] が要求される
  3. type Element[E Element[E]] という自己参照が発生する

関数なら不要

ちなみに、メソッドではなく関数として書くのであれば、Go 1.25 でも問題ありませんでした。その場で制約が書けないのはメソッドに特有の制限だからです。

制約の解決は関数の型パラメータ [E Element[E]] で行えるため、双方 any でよく、相互参照の問題が起きないのです。

まとめ

  • Go 1.26 から、ジェネリック型の定義の中で自分自身を参照できるようになった
  • ただし公式リリースノートの Adder の例は自己参照なし (単なる A any) でも問題にならない
  • 実際に自己参照が必要になるのは、ジェネリックなインターフェースとカスタム型が相互に参照し合う場合
  • それも型システムとして本質的に必要だったというよりは、Go のメソッドの制約をインターフェース側で回避するための workaround として機能する

普通のアプリケーションコードを書く分には出会わないまま過ごすかもしれませんが、Go のジェネリクスの仕様を深く理解する上では面白い題材でした。
まだまだわからないことが多いので、引き続き勉強していこうと思います。

  1. なお、その制限が文法上の問題なのか言語の複雑性を増やさないための選択なのかは調べきれませんでした。公式 FAQ のこちらを見る分には文法上の制約っぽい説明がなされていますが、それだけかはわかりません。 ↩︎

ニフティでは、
さまざまなプロダクトへ挑戦する
エンジニアを絶賛募集中です!
ご興味のある方は以下の採用サイトより
お気軽にご連絡ください!

ニフティに興味をお持ちの方は
キャリア登録をぜひお願いいたします!

connpassでニフティグループに
参加いただくと
イベントの
お知らせが届きます!