はじめに
- この記事のコードは
rustc 1.91.0-nightlyで動作確認しています - この記事で紹介している解決方法は,近い将来に不要となる可能性があります
- もっといい解決方法があればおしえてください
やりたいこと
例として以下のようなコードを考えます.
fn f(
a: impl Iterator<Item=usize>,
b: impl Iterator<Item=usize>
) -> impl Iterator<Item=usize> {
let n = b.sum();
a.take(n)
}
fn g() -> impl Iterator<Item=usize> {
let v = vec![2, 3];
f(0..10, v.iter().cloned())
}
関数 f の実装では戻り値に b を含んでいないので,b のライフタイムは短くても良さそうですが,このコードはコンパイルエラーになります.
64 | f(0..10, v.iter().cloned())
| ---------^-----------------
| | |
| | borrowed value does not live long enough
| argument requires that `v` is borrowed for `'static`
この関数 f の定義は,外から見ると戻り値のライフタイムが引数 a と b の両方に依存していると解釈されてしまい(overcaptureing と呼ばれる問題),関数 g で定義している変数 v のライフタイムが不十分だと言われています.
適切に関数 f を定義して,戻り値のライフタイムが引数 b に依存していないことを明示したいというのが,この記事でのやりたいことになります.
※ 例があまり良くなく,実はこのコードは v.into_iter() にすればコンパイルできるのですが,本記事の趣旨は関数 f をどう定義するかなので,この解決方法は考えないことにします.
状況によっては現実的ではない解決方法
以下のように,引数と戻り値のどちらかを具体的な型とすることでコンパイルエラーを回避することが可能です.
戻り値を impl Trait にしない
fn f<T>(
a: T,
b: impl Iterator<Item=usize>
) -> std::iter::Take<T>
where
T: Iterator<Item=usize>,
{
let n = b.sum();
a.take(n)
}
引数を impl Trait にしない
fn f<'a, T>(
a: T,
b: std::iter::Cloned<std::slice::Iter<'a, usize>>
) -> impl Iterator<Item=usize> + use<T>
where
T: Iterator<Item=usize>,
{
let n = b.sum();
a.take(n)
}
ただし,引数・戻り値の型が複雑すぎる状況では impl Trait とせざるを得ないため現実的ではありません.また,ライブラリの戻り値が impl Trati である場合などには,そもそもプログラマーには実際の型がわからないのでこの解決方法を採用することができません.
impl_trait_in_assoc_type を使った解決方法
トレイトを定義し,関数をトレイトのメソッドに,戻り値を関連型にすることで,引数と戻り値を impl Trait としたまま,戻り値のライフタイムが引数 b に依存しないことを明示できます.
関連型を impl Trait にする機能は rustc 1.91.0-nightly の時点では Unstable なので,コードの先頭で #![feature(impl_trait_in_assoc_type)] という宣言が必要になります.
#![feature(impl_trait_in_assoc_type)]
trait Trait<T>
where
T: Iterator<Item = usize>,
{
type Output;
fn f(a: T, b: impl Iterator<Item = usize>) -> Self::Output;
}
struct F{}
impl<T> Trait<T> for F
where
T: Iterator<Item = usize>,
{
type Output = impl Iterator<Item=usize>; // ここで型を定義すると b に依存していないことがコンパイラに伝わるらしい
fn f(a: T, b: impl Iterator<Item = usize>) -> Self::Output
{
let n = b.sum();
a.take(n)
}
}
fn g() -> impl Iterator<Item=usize> {
let v = vec![2, 3];
F::f(0..10, v.iter().cloned()) // OK
}
fn main() {
for k in g() {
println!("{}", k);
}
}
ただ関数を定義するためにトレイトと構造体が必要で,非常に面倒ではありますが,やりたかったことを実現できました.
RFC3617
この問題はすでに課題として認識されているようで,近い将来には,より簡単な記述で解決できるようになるのかもしれません.
コード
main.rs
#![feature(impl_trait_in_assoc_type)]
// この定義では戻り値のライフタイムが b に依存してしまう
fn f1(a: impl Iterator<Item=usize>, b: impl Iterator<Item=usize>) -> impl Iterator<Item=usize> {
let n = b.sum();
a.take(n)
}
// 戻り値に impl Trait を使わない例
fn f2<T>(a: T, b: impl Iterator<Item=usize>) -> std::iter::Take<T>
where
T: Iterator<Item=usize>,
{
let n = b.sum();
a.take(n)
}
// 引数に impl Trait を使わない例
fn f3<'a, T>(a: T, b: std::iter::Cloned<std::slice::Iter<'a, usize>>) -> impl Iterator<Item=usize> + use<T>
where
T: Iterator<Item=usize>,
{
let n = b.sum();
a.take(n)
}
// trait の関連型を使う例
trait Trait<T>
where
T: Iterator<Item = usize>,
{
type Output;
fn f(a: T, b: impl Iterator<Item = usize>) -> Self::Output;
}
struct F{}
impl<T> Trait<T> for F
where
T: Iterator<Item = usize>,
{
type Output = impl Iterator<Item=usize>;
fn f(a: T, b: impl Iterator<Item = usize>) -> Self::Output {
let n = b.sum();
a.take(n)
}
}
fn g() -> impl Iterator<Item=usize> {
let v = vec![2, 3];
// f1(0..10, v.iter().cloned()) // NG
// f1(0..10, v.into_iter()) // OK
// f2(0..10, v.iter().cloned()) // OK
// f3(0..10, v.iter().cloned()) // OK
F::f(0..10, v.iter().cloned()) // OK
}
fn main() {
for k in g() {
println!("{}", k);
}
}