💊

Go Generics 最低限知っておくべきこと

2022/01/28に公開

Go1.18 でついに導入されるGenerics。

その使い方を紹介する記事も出始めていますが、ここさえ押さえておけば最低限 Generics の読み書きができる と言えるような内容をまとめた記事はあまり見かけなかったので、本記事でまとめてみようと思います。この記事が Generics を使い始める際の道しるべとなれば幸いです。

チュートリアル

・・・と言いながら、いきなり突き放すようで申し訳ないのですが、まずはとにもかくにも公式から出ているチュートリアルをこなしましょう。

https://go.dev/doc/tutorial/generics

Generics が動くバージョンの Go をインストールするところから始まり、Generics を使わない関数を書き、それを Generics を使って書き直す、といった内容になっています。

非ネイティブでも分かりやすい英語で書かれているので、まだご覧になっていない方は一通りこなしてみてください。

この後のセクションでは、このチュートリアルに載っていないが、筆者がここだけは押さえておくべきと思うポイントをご紹介していきます。

新しい interface

Generics 登場以前は interface を構成する要素(interface element)はメソッドを制約するためのメソッドシグネチャの集合(method element)だけでした。これからは型も制約するためにチュートリアルで見てきた通り "|" で区切られた型の集合も加わります。この型の集合は spec によると type element と呼ぶそうです。

新しいオペレータ

チュートリアルでは登場しませんでしたが、Go の Generics を使う上で重要となってくる新たなオペレータが導入されます。"~" です。

この "~" は、~T の形で上記の type elementtype constraint で使われます。

"~" もとい ~T の意味については Nobishii さんの以下の記事が詳しいのですが、簡単に説明すると「T を underlying type とする型の集合」です。

https://zenn.dev/nobishii/articles/99a2b55e2d3e50#approximation-element

underlying type 自体は Generics に限らない話なのでここでは詳しく述べませんが、「良く分からないよ!」という方は DQNEO さんのスライドが参考になるのでご覧ください。

https://speakerdeck.com/dqneo/go-language-underlying-type

ちなみに ~T は approximation element と呼ばれ、これ自体が underlying type というわけではありません。

\text{approximation element(~T) = ~ + underlying type(T)}

新しい事前宣言された識別子

新しい事前宣言された識別子(predeclared identifier)としては、チュートリアルでも登場した any に加え、comparable が追加されます。

any は空のインターフェース interface{} のエイリアスとして同じように扱うことができます。

comparable==!=で比較できる型の集合、すなわち比較可能性を表すインターフェースです。type constraint としてのみ使用が可能です。比較可能性の考え方は意外と複雑なので(特にインターフェース)、気になる方は spec をチェックしてみてください。

新しいパッケージ

新しい Generics 関連の標準パッケージとして、よく使われる type constraint をまとめた constrains, map の操作を行う maps, slice の操作を行う slices が Go1.18 で導入される予定でした。

しかし、これらのパッケージを導入することに対し未だ議論が収束していないとのことから、 Go1.18 で導入するのは見送り、Go1.19 や Go1.20 で再度検討する運びとなりました。

https://github.com/golang/go/issues/50792

なお、導入予定だったパッケージは実験的なリポジトリである exp 配下に移されたため、お試しで使うことはできます。

https://github.com/golang/exp/blob/master/constraints
https://github.com/golang/exp/blob/master/maps
https://github.com/golang/exp/tree/master/slices

型パラメータの推論とその手順

チュートリアルでもサラッと出てきましたが、parameterize された関数を呼ぶ際に type argument で type parameter の型を明示しなくてもコンパイラが型推論してくれます。

推論は 2-pass algorithm と呼ばれる次の2段階の手順で行われます。

  1. 引数の中で型が決まっていない定数が渡された type parameter は推論をスキップし、型の決まった type parameter はその型を採用します。
  2. 推論がスキップされた type parameter は、その type parameter が他の引数で推論に成功している場合はその型でキャストし、キャストに失敗すればコンパイルエラーとします。それでもなお型推論ができていない type parameter は定数のデフォルト型が採用されますが、異なる引数かつ同じ type parameter でデフォルト型が違う場合もコンパイルエラーとなります。

詳しくは以下の proposal をご覧いただければなと思います。

https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md#function-argument-type-inference

なお、paramerize された struct は、初期化する際にメンバ変数によって type parameter の型は推論されない(コンパイルエラーになる)ので、型を明示してあげましょう。

また、型推論に成功したからと言って、通常の関数と同様に引数や戻り値の型が関数内の処理に適していない場合もコンパイルエラーになることに注意しましょう。

さらに、推論可能性と代入可能性は別のお話なので注意してください。

https://twitter.com/cia_rana/status/1482310638793682944

おわりに

Generics 導入に伴い多くの機能が追加されることが見て取れたと思います。それでも本記事で述べてきたことはほんの基礎の部分であり、応用的な使い方については読者に委ねることになります。また、執筆時点では go1.18beta1 が最新であり、Go1.18 リリース後や Go1.19 以降で仕様が追加・変更されることも大いにありえるため、常に最新の情報を得るようにしましょう。

Let's Enjoy Generics Life!

Discussion