RustのGeneric associated types(GATs)が1.65.0でstabilizeとなりました。
ということで本記事では、GATsについての各種解説等を読みながら、GATsの理解を少しでも進めることを目指します。
Rustのversionは1.69.0を利用しました。
The push for GATs stabilization
まずは、2021年8月Rust blogの記事The push for GATs stabilizationから読んでいきます。
そこでは、GATsの具体例として以下のcodeが挙げられています。
今までは、traitのassociated typeにはgenericsやlifetimeは書けなかったのですが、GATsによって書けるようになりました。
以前、Rustのlifetimeとvarianceでも書いたのですが、Vec<T>が、Vec<usize>のように具体的な型を与えられてtypeになるように、Bar<'a>も具体的なlifetimeがあたえられてtypeになると理解しています。
Associated typeのところにlifetimeが書けるとなにがうれしいかというと
こんなcodeが書けるようになります。以前ですと、&'a T;の'aは実装するstructに定義されている必要がありました。
ちなみに上記のcodeはcompileが通りません。
|
82 | type Bar<'a> = &'a T;
| ^^^^^- help: consider adding a where clause: `where T: 'a`
| |
| ...so that the reference type `&'a T` does not outlive the data it points at
compile通すには結果的に以下のようになりました。
Tは実際には&strかもしれないので、&str: 'aの制約が必要といわれるのはなんとなくわかります。
次にもう少し具体的な利用例が紹介されます。
ここではmut sliceのwindow(sub slice)を返すiteratorを実装したいというユースケースです。
具体的には[1,2,3,4,5]があったときに[1,2,3], [2,3,4], [3,4,5]を返すようなiteratorです。
このWindowsMutにIteratorを実装しようとすると
error: lifetime may not live long enough
--> src/lending.rs:21:9
|
15 | impl<'t, T> Iterator for WindowsMut<'t, T> {
| -- lifetime `'t` defined here
...
18 | fn next<'a>(&'a mut self) -> Option<Self::Item> {
| -- lifetime `'a` defined here
...
21 | Some(retval)
| ^^^^^^^^^^^^ method was supposed to return data with lifetime `'t` but it is returning data with lifetime `'a`
|
= help: consider adding the following bound: `'a: 't`
self.sliceの参照を返しているので、'a: 'tの制約が必要となり、compile errorとなります。
のようにwhere 'a: 't'をつけると今度は
error[E0195]: lifetime parameters or bounds on method `next` do not match the trait declaration
--> src/lending.rs:18:12
|
18 | fn next<'a>(&'a mut self) -> Option<Self::Item>
| ^^^^ lifetimes do not match method in trait
Iteraotr traitにそんな制約はないのでこれもcompile errorとなってしまいます。
そこで、GATsを利用した、LendingIteratorを定義します。
これはself.next()の戻り値を利用している間だけ有効なItemを返すIteratorと読めます。
実装はIteratorと同じですが、Itemのlifetimeに呼び出し時のlifetimeを渡せています。
// => [1, 2, 3]
// => [2, 3, 4]
// => [3, 4, 5]
無事、windowを返すことができました。
記事でも触れられていますが、type Item<'a> where Self: 'aがなぜ必要かというと
fn next<'static>(&'static mut self) -> Option<Self::Item<'static>> のようなcodeを書けなくするためみたいです。
この点はissueになっており将来的には暗黙的に仮定されるかもしれません。
Generic Associated Types Initiative
次はGeneric Associated Types Initiativeが提供してくれているbookをみていきます。 GATsに関する過去あった議論も整理されており、RFCやissueのコメントは長く全部読むのは大変なので助かりました。
例えば、GATsはRustの複雑度を上げすぎてしまう懸念の話は頷けるところ多かったです。
Iterable trait
次はIterable traitを通してGATsの使い所をみていきます。
まず以下のように今まで通りIteratorを実装してみます。
ここで次のように、ある処理の中で複数回iterateしたいので、iteratorを返せるcollectionを引数にした処理を書きたいとします。
これは以下のように2回目のfor loopでmoveした値を利用しているのでcompileが通りません。
28 | for elem in collection {
| ^^^^^^^^^^ value used here after move
|
ということで、Iteratorを返せることを抽象化したIterable traitを作ることにします。
IterがIteratorでItemを返します。IntoIteratorの&self版といった感じです。
&[T]は複数回Iteratorを返せるのでIterableを実装したいところですが..
ここで&'what lifetimeをどこからもってくるかで問題が生じます。GATsがないと今までは、impl<'a> Iterable for A<'a>のようにimplにlifetime書く必要がありましたが、今回の[T]にはlifetimeがでてきません。
というわけで、Iterable traitをGATsを使って書き直します。
ItemとIterにlifetimeを渡したいので、trait側で定義します。
実装はtrait側のlifetimeを使うだけです。
fn count_twice<I: Iterable + ?Sized>(collection: &I) { /* ... */} Sized制約をopt outすると無事compileが通りました。
要はGATsによって、呼び出し側が使っている間だけ有効なselfのdataの参照を返せるようになったと理解しています。
bookにはGATsを実際に利用しているcrateのlistも載っていたりしました。
また、IterableやLendingIterator以外にも、GATsのdesign patternとして、Many modeやgenerics scopesも取り上げられていました。
Many modeについてはわからないことが多く、今後の課題です。
まとめ
GATsの一利用例をみてきました。今後はlibrary等でも使われてくることが増えてくるのではと思っているのでうまい使い所を見つけていきたいです。