51
43

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

イテレータを返す関数を書きたいんですけど? → やめとけ。 死ぬぞ。

Last updated at Posted at 2015-02-24

!!この記事の内容はもう古いです。 詳しくはコメント参照!!

こんにちは。 Rust記事です。

Rust、比較的新しい言語じゃないですか。 頑張ってチュートリアル読んで、じゃあなんかちょっとしたアルゴリズムでも書いてみるか、ってなるじゃないですか。 で、大体のちょっとしたアルゴリズムって、まぁ普通は配列とかマップとか、そういうデータ集合のデータ構造が出てきますよね。 もちろんそんなデータ構造を関数間で受け渡ししたくなるじゃないですか。 で、『Vec作って返すよりもイテレータの状態で返したほうがパフォーマンスいいんじゃない?』って場面はまぁあると思うんですよ。

やめとけ、死ぬぞ。

っていうのが今回の主旨です。

イテレータを返す関数って、どんな定義になると思いますか?
ここでは適当に、Vec<i32>を2つ受け取って、それらを連結した上で3の倍数の要素のみを返したい、とか考えましょう。
たぶんRust初心者は、こんなのを想像すると思うんです。 私も今朝しました。

fn the_function(v1: &Vec<i32>, v2: &Vec<i32>) -> Iterator<i32> {
    v1.iter().chain(v2.iter()).filter(|x| x % 3 == 0)
}

本当はこうなります。

fn the_function<'a>(v1: &'a Vec<i32>, v2: &'a Vec<i32>) -> std::iter::Filter<std::iter::Chain<std::slice::Iter<'a, i32>, std::slice::Iter<'a, i32>>, fn(&&i32) -> bool> {
    fn only_multiples_3(x: &&i32) -> bool { *x % 3 == 0 };
    v1.iter().chain(v2.iter()).filter(only_multiples_3)
}

ちなみに今私はこの関数をコンパイル通すために1時間かけました。
まさかの関数本体より長い戻り値型。 どうしてこうなった。

なぜなら、関数の戻り型の所には「型」を書かなければいけないのです。 Iterator<i32>はあくまでも trait であって、型ではありません。 Iterator<i32>を impl した何かの型をここには書いてやる必要があるのです。

Rust言語のイテレータはちゃんとできていて、例えばfilter(...)メソッドは、イテレータが実際に使われるまではフィルタリングや、フィルタリング元のイテレータの呼び出しを実際に行うことはありません。 つまり、イテレータは使われるまで呼び出し元の型だとかフィルタリングに使う関数オブジェクトの型だとか、そういうのを全部記憶しておく必要があるのです。 「この関数の戻り値はなんかIteratorっぽい挙動をする型を返すよ」ではだめなのです。「この関数は、Iteratorを impl して、Vec<i32>のイテレータを2つ連結した後にfn(&&i32) -> bool型の関数オブジェクトを使ってフィルタリングしたものをイテレータとした型を返すよ」と言ってやらないとだめなのです。 Rust言語では、これらの情報をぜんぶ型名として記憶するのです。

「でも、JavaのIterator<E>だとそんな面倒なこと必要ないじゃん」と思うでしょう。 その通りです。 Java言語では、これらの情報の記憶・解決を仮想関数テーブルを用いて実行時に行なっています。 このためイテレータは全部ヒープ上に確保されます。 でも、Rust言語の開発者たちはこう思ったようです。
「イテレータをヒープ上に確保したり、仮想関数テーブル使ったりするのって無駄じゃね? 十分な型推論があれば、全部コンパイル時に解決できるっしょ?」
十分な型推論があればってところがミソです。 実際、このイテレータのくっそ複雑な型は、イテレータを戻り値の型(ここだけは必ず型を書かなければならない)にしない限りほぼお目にかかりません。 Rustでは変数の宣言時に型は不要ですし、引数はジェネリック関数を使えば型の代わりに trait で記述することができます。 たとえばこんな感じに。

fn print_iter<'a, I: Iterator<Item=&'a i32>>(iter: I) {  // ここでは引数の"型"は明示せず、traitだけでいい
    for v in iter {
        println!("The iterator's value is {}", v);
    }
}

fn main() {
    let v1 = vec![1, 3, 5];
    let v2 = vec![2, 4, 6];
    // ここではiterの型は勝手に推論される
    let iter = v1.iter().chain(v2.iter()).filter(|x: &&i32| *x % 3 == 0);
    print_iter(iter);
}

まだ前の関数よりはなんぼか読みやすくなったのではないでしょうか。 これでもまだ'aとか&&とか出てきてようわからんという皆さん、これがRustだ。 この辺は普通に出るので頑張ってコンパイラと対話を試みてください。

一応、Java的な解決策、つまり型に複雑な情報を持たせるのを放棄して、全部仮想関数テーブルにやらせて表記を簡潔化するという手段がとれる場合もあります。 Box<T>型のパラメータTは trait でもいいことを利用して、Box<Iterator<Item=T>>型を返すような関数が書ける場合もあります
あるいは、そもそもIteratorを返すんじゃなくてVec<T>とかを返すようにするとプログラムを書くのが非常に楽になります。 もちろんこれはイテレータの遅延実行のメリットを捨ててたりとかVec<T>を確保する手間とかが余計にかかっているのですが、大抵の状況ではそんなもんどうでもいい誤差レベルのデメリットです。
まぁ、そういう誤差レベルのデメリットが気にならないならJavaとかで書けよって話ですが……。

一応これは言語開発者の皆さんもわりと喫緊の課題と認識しているようなので、近いうちにいい感じの解決策が提示されるかもしれません。 ただし1.0には間に合わない。 解決策を後で実装しても現バージョンのコードはそのまま動く(後方互換性を保てる)ので、やや後回しになったようです。

参考: RFC: Add unboxed, abstract return types #105

51
43
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
43

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?