注意: 最新版のドキュメントをご覧ください。この第1版ドキュメントは古くなっており、最新情報が反映されていません。リンク先のドキュメントが現在の Rust の最新のドキュメントです。
ループの話をしましょう。
Rustの for
ループを覚えていますか?以下が例です。
for x in 0..10 { println!("{}", x); }
あなたはもうRustに詳しいので、私たちはどのようにこれが動作しているのか詳しく話すことができます。
レンジ(ranges、ここでは 0..10
)は「イテレータ」(iterators)です。イテレータは .next()
メソッドを繰り返し呼び出すことができ、その都度順番に値を返すものです。
こんな風に:
fn main() { let mut range = 0..10; loop { match range.next() { Some(x) => { println!("{}", x); }, None => { break } } } }let mut range = 0..10; loop { match range.next() { Some(x) => { println!("{}", x); }, None => { break } } }
始めに変数rangeへミュータブルな束縛を行っていますが、これがイテレータです。その次には中に match
が入った loop
があります。この match
は range.next()
を呼び出し、イテレータから得た次の値への参照を使用しています。 next
は Option<i32>
を返します。このケースでは、次の値があればその値は Some(i32)
であり、返ってくる値が無くなれば None
が返ってきます。もし Some(i32)
であればそれを表示し、 None
であれば break
によりループから脱出しています。
このコードは、基本的に for
ループバージョンと同じ動作です。 for
ループはこの loop
/ match
/ break
で構成された処理を手軽に書ける方法というわけです。
しかしながら for
ループだけがイテレータを使う訳ではありません。自作のイテレータを書く時は Iterator
トレイトを実装する必要があります。それは本書の範囲外ですが、Rustは多様な反復処理を実現するために便利なイテレータを幾つか提供しています。ただその前に、少しばかりレンジの限界について言及しておきましょう。
レンジはとても素朴な機能ですから、度々別のより良い手段を用いることもあります。ここであるRustのアンチパターンについて考えてみましょう。それはレンジをC言語ライクな for
ループの再現に用いることです。例えばベクタの中身をイテレートする必要があったとしましょう。あなたはこう書きたくなるかもしれません。
let nums = vec![1, 2, 3]; for i in 0..nums.len() { println!("{}", nums[i]); }
これは実際のイテレータの使い方からすれば全く正しくありません。あなたはベクタを直接反復処理できるのですから、こう書くべきです。
fn main() { let nums = vec![1, 2, 3]; for num in &nums { println!("{}", num); } }let nums = vec![1, 2, 3]; for num in &nums { println!("{}", num); }
これには2つの理由があります。第一に、この方が書き手の意図をはっきり表せています。私たちはベクタのインデックスを作成してからその要素を繰り返し参照したいのではなく、ベクタ自体を反復処理したいのです。第二に、このバージョンのほうがより効率的です。1つ目の例では num[i]
というようにインデックスを介し参照しているため、余計な境界チェックが発生します。しかし、イテレータが順番にベクタの各要素への参照を生成していくため、2つ目の例では境界チェックが発生しません。これはイテレータにとってごく一般的な性質です。不要な境界チェックを省くことができ、それでもなお安全なままなのです。
ここにはもう1つ、println!
の動作という詳細が100%はっきりしていない処理があります。 num
は実際には &i32
型です。これは i32
の参照であり、 i32
それ自体ではありません。 println!
は上手い具合に参照外し(dereferencing)をしてくれますから、これ以上深追いはしないことにします。以下のコードも正しく動作します。
let nums = vec![1, 2, 3]; for num in &nums { println!("{}", *num); }
今、私たちは明示的に num
の参照外しを行いました。なぜ &nums
は私たちに参照を渡すのでしょうか?第一に、&
を用いて私たちが明示的に要求したからです。第二に、もしデータそれ自体を渡す場合、私たちはデータの所有者でなければならないため、データの複製と、それを私たちに渡す操作が伴います。参照を使えば、データへの参照を借用して渡すだけで済み、ムーブを行う必要がなくなります。
ここまでで、多くの場合レンジはあなたの欲する物ではないとわかりましたから、あなたが実際に欲しているものについて話しましょう。
それは大きく分けてイテレータ、 イテレータアダプタ (iterator adaptors)、そして コンシューマ (consumers)の3つです。以下が定義となります。
既にイテレータとレンジについて見てきましたから、初めにコンシューマについて話しましょう。
コンシューマ とはイテレータに作用し、何らかの値を返すものです。最も一般的なコンシューマは collect()
です。このコードは全くコンパイルできませんが、意図するところは伝わるでしょう。
let one_to_one_hundred = (1..101).collect();
ご覧のとおり、ここではイテレータの collect()
を呼び出しています。 collect()
はイテレータが渡す沢山の値を全て受け取り、その結果をコレクションとして返します。それならなぜこのコードはコンパイルできないのでしょうか?Rustはあなたが集めたい値の型を判断することができないため、あなたが欲しい型を指定する必要があります。以下のバージョンはコンパイルできます。
let one_to_one_hundred = (1..101).collect::<Vec<i32>>();
もしあなたが覚えているなら、 ::<>
構文で型ヒント(type hint)を与え、整数型のベクタが欲しいと伝えることができます。かといって常に型をまるごとを書く必要はありません。 _
を用いることで部分的に推論してくれます。
let one_to_one_hundred = (1..101).collect::<Vec<_>>();
これは「値を Vec<T>
の中に集めて下さい、しかし T
は私のために推論して下さい」という意味です。このため _
は「型プレースホルダ」(type placeholder)と呼ばれることもあります。
collect()
は最も一般的なコンシューマですが、他にもあります。 find()
はそのひとつです。
let greater_than_forty_two = (0..100) .find(|x| *x > 42); match greater_than_forty_two { Some(_) => println!("Found a match!"), None => println!("No match found :("), }
find
はクロージャを引数にとり、イテレータの各要素の参照に対して処理を行います。ある要素が私たちの期待するものであれば、このクロージャは true
を返し、そうでなければ false
を返します。マッチングする要素が無いかもしれないので、 find
は要素それ自体ではなく Option
を返します。
もう一つの重要なコンシューマは fold
です。こんな風になります。
let sum = (1..4).fold(0, |sum, x| sum + x);
fold()
は fold(base, |accumulator, element| ...)
というシグネチャのコンシューマで、2つの引数を取ります。第1引数は base (基底)と呼ばれます。第2引数は2つ引数を受け取るクロージャです。クロージャの第1引数は accumulator (累積値)と呼ばれており、第2引数は element (要素)です。各反復毎にクロージャが呼び出され、その結果が次の反復のaccumulatorの値となります。反復処理の開始時は、baseがaccumulatorの値となります。
ええ、ちょっとややこしいですね。ではこのイテレータを以下の値で試してみましょう。
base | accumulator | element | クロージャの結果 |
---|---|---|---|
0 | 0 | 1 | 1 |
0 | 1 | 2 | 3 |
0 | 3 | 3 | 6 |
これらの引数で fold()
を呼び出してみました。
.fold(0, |sum, x| sum + x);
というわけで、 0
がbaseで、 sum
がaccumulatorで、xがelementです。1度目の反復では、私たちはsumに0をセットし、 nums
の1つ目の要素 1
が x
になります。その後 sum
と x
を足し、 0 + 1 = 1
を計算します。2度目の反復では前回の sum
がaccumulatorになり、elementは値の列の2番目の要素 2
になります。 1 + 2 = 3
の結果は最後の反復処理におけるaccumulatorの値になります。最後の反復処理において、 x
は最後の要素 3
であり、 3 + 3 = 6
が最終的な結果となります。 1 + 2 + 3 = 6
、これが得られる結果となります。
ふぅ、ようやく説明し終わりました。 fold
は初めのうちこそ少し奇妙に見えるかもしれませんが、一度理解すればあらゆる場面で使えるでしょう。何らかのリストを持っていて、そこから1つの結果を求めたい時ならいつでも、 fold
は適切な処理です。
イテレータにはまだ話していないもう1つの性質、遅延性があり、コンシューマはそれに関連して重要な役割を担っています。それではもっと詳しくイテレータについて話していきましょう、そうすればなぜコンシューマが重要なのか理解できるはずです。
前に言ったように、イテレータは .next()
メソッドを繰り返し呼び出すことができ、その都度順番に値を返すものです。メソッドを繰り返し呼ぶ必要があることから、イテレータは lazy であり、前もって全ての値を生成できないことがわかります。このコードでは、例えば 1-99
の値は実際には生成されておらず、代わりにただシーケンスを表すだけの値を生成しています。
let nums = 1..100;
私たちはレンジを何にも使っていないため、値を生成しません。コンシューマを追加してみましょう。
fn main() { let nums = (1..100).collect::<Vec<i32>>(); }let nums = (1..100).collect::<Vec<i32>>();
collect()
は幾つかの数値を渡してくれるレンジを要求し、シーケンスを生成する作業を行います。
レンジは基本的な2つのイテレータのうちの1つです。もう片方は iter()
です。 iter()
はベクタを順に各要素を渡すシンプルなイテレータへ変換できます。
let nums = vec![1, 2, 3]; for num in nums.iter() { println!("{}", num); }
これら2つの基本的なイテレータはあなたの役に立つはずです。無限を扱えるものも含め、より応用的なイテレータも幾つか用意されています。
これでイテレータについては十分でしょう。私たちがイテレータに関して最後に話しておくべき概念がイテレータアダプタです。それでは説明しましょう!
イテレータアダプタ はイテレータを受け取って何らかの方法で加工し、新たなイテレータを生成します。 map
はその中でも最も単純なものです。
(1..100).map(|x| x + 1);
map
は別のイテレータについて呼び出され、各要素の参照をクロージャに引数として与えた結果を新しいイテレータとして生成します。つまりこのコードは私たちに 2-100
の値を返してくれるでしょう。えーっと、厳密には少し違います!もしこの例をコンパイルすると、こんな警告が出るはずです。
warning: unused result which must be used: iterator adaptors are lazy and
do nothing unless consumed, #[warn(unused_must_use)] on by default
(1..100).map(|x| x + 1);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
また遅延性にぶつかりました!このクロージャは実行されませんね。例えば以下の例は何の数字も出力されません。
fn main() { (1..100).map(|x| println!("{}", x)); }(1..100).map(|x| println!("{}", x));
もし副作用のためにイテレータに対してクロージャの実行を試みるのであれば、代わりに for
を使いましょう。
他にも面白いイテレータアダプタは山ほどあります。 take(n)
は元のイテレータの n
要素目までを実行するイテレータを返します。先程言及していた無限を扱えるイテレータで試してみましょう。
for i in (1..).take(5) { println!("{}", i); }
これの出力は、
1
2
3
4
5
filter()
は引数としてクロージャをとるアダプタです。このクロージャは true
か false
を返します。 filter()
が生成する新たなイテレータはそのクロージャが true
を返した要素のみとなります。
for i in (1..100).filter(|&x| x % 2 == 0) { println!("{}", i); }
これは1から100の間の偶数を全て出力します。(反復処理中の要素を filter
で消費させないために、各要素の参照が渡されることに注目して下さい。そのためfilterの述語に &x
パターンを用いて整数自体を抽出しています。)
訳注: クロージャで用いられている
&x
パターンは パターン の章では紹介されていません。簡単に解説すると、何らかの参照&T
から 内容のみを取り出してコピー するのが&x
パターンです。参照をそのまま受け取るref x
パターンとは異なりますので注意して下さい。
あなたはここまでに説明された3つの概念を全て繋げることができます。イテレータから始まり、アダプタを幾つか繋ぎ、結果を消費するといった感じです。これを見て下さい。
fn main() { (1..) .filter(|&x| x % 2 == 0) .filter(|&x| x % 3 == 0) .take(5) .collect::<Vec<i32>>(); }(1..) .filter(|&x| x % 2 == 0) .filter(|&x| x % 3 == 0) .take(5) .collect::<Vec<i32>>();
これは 6, 12, 18, 24,
、そして 30
が入ったベクタがあなたに渡されます。
イテレータ、イテレータアダプタ、そしてコンシューマがあなたの助けになることをほんの少しだけ体験できました。本当に便利なイテレータが幾つも用意されていますし、あなたがイテレータを自作することもできます。イテレータは全ての種類のリストに対し効率的な処理方法と安全性を提供します。これらは初めこそ珍しいかもしれませんが、もし使えばあなたは夢中になることでしょう。全てのイテレータとコンシューマのリストは イテレータモジュールのドキュメンテーション を参照して下さい。