動機: Rustについて学ぶと共に英語の翻訳をしてみよう思った(読むのは興味をもった順ですので悪しからず)
誤訳や改善案等、見つけた場合はご指摘の程宜しくお願いします。
- 元記事 : https://doc.rust-lang.org/book/iterators.html
- ライセンス : MIT license, Apache License 2.0.
- See LICENSE-APACHE, LICENSE-MIT, and COPYRIGHT for details.
イテレータ
ループの話をしましょう。
Rustのfor
ループを覚えていますか?こちらが例になります。
for x in 0..10 {
println!("{}", x);
}
今やあなたはRustに詳しいですから、私達はどのようにこの文が動いているのか詳しく話せます。
レンジ(ここでは0..10
)は'イテレータ'です。イテレータは.next()
メソッドを繰り返し呼び出すことができ、その都度順番に値を返すものです。こんな風に書いてみます。
let mut range = 0..10;
loop {
match range.next() {
Some(x) => {
println!("{}", x);
},
None => { break }
}
}
変数rangeにイテレータをミュータブル束縛しています。loop
の中のmatch
は、イテレータであるrange.next()
を呼び出すことで返ってくる次の値への参照を使用しています。next
にはOption<i32>
が返ってきます。このケースでは、イテレータの次の値が返ってくればその値はSome(i32)
であり、返ってくる値が無くなればNone
が返ってきます。もしSome(i32)
であればそれを表示し、None
であればbreak
によりループから脱出しています。
このコードは、基本的にfor
ループバージョンと同じ動作です。for
ループはこのloop
/match
/break
で構成された処理を手軽に書ける方法というわけです。
しかしながら、for
ループはイテレータのみに使うものではありません。Iterator
トレイトを実装し組み込むことで、あなた自作のイテレータを書くこともできます。このガイドの範囲外ですが、Rustは多様な反復処理を実現するために便利なイテレータを幾つか提供しています。ただ、それらについて話す前に、Rustのアンチパターンについて話しておかなければなりません。しかもアンチパターンというのは、先ほど書いたようにレンジを使うことなのです。
ええ、確かに私たちはたった今レンジが如何にクールなのか話しました。しかしレンジは古典的過ぎます。例えば、もしあなたがvectorの中身を繰り返し処理したいとき、こんな風に書きたくなるかもしれません。
let nums = vec![1, 2, 3];
for i in 0..nums.len() {
println!("{}", nums[i]);
}
これは実際のイテレータの使い方からすれば全く正しくありません。あなたはvectorを直接反復処理できるのですから、こう書くべきです。
let nums = vec![1, 2, 3];
for num in &nums {
println!("{}", num);
}
これには2つの理由があります。第一に、このほうが書き手の意味するところがはっきり表現できます。私たちはvectorのインデックスを作成してからその要素を繰り返し参照したいのではなく、vector自体を反復処理したいのです。第二に、このバージョンのほうがより効率的です。1つ目の例ではnum[i]
というようにインデックスを使っているため、余計な境界チェックが発生します。しかし、イテレータが順番にvectorの各要素の参照を生成していくため、2つ目の例では境界チェックが発生しません。これはイテレータにとってごく一般的な性質です。私たちは不要な境界チェックを取り除いてもなお安全であることが分かっていますから、より効率的な2つ目の例を使うべきです。
ここにはもう1つ、println!
の動作という詳細が100%はっきりしていない処理があります。num
は実際には&i32
型です。これはi32
の参照であり、i32
それ自体ではありません。println!
は私たちがそのことを理解していなくとも、参照からうまく値を取り出してくれます。このコードも正しく動作します。
let nums = vec![1, 2, 3];
for num in &nums {
println!("{}", *num);
}
今、私たちは明示的にnum
から参照を取り出しました。なぜ&nums
は私たちに参照を渡すのでしょうか?第一に、&
を用いて私たちが明示的に要求したからです。第二に、もしデータそれ自体を私たちに渡す場合、私たちはデータの所有者でなければならないため、データの複製と、それを私たちに渡す操作が伴います。参照を使えば、データの参照を借用し、参照を介するだけで、ムーブを行う必要がなくなります。
そういうわけで、今やレンジはあまりあなたが欲しいものではなくなりました。それではあなたが代わりに欲しいと思うものについて話しましょう。
それは大きく分けてイテレータ、イテレータアダプタ、そしてコンシューマの3つです。以下が定義となります。
- イテレータはあなたに値の列を渡します。
- イテレータアダプタはイテレータに作用し、出力の異なるイテレータを生成します。
- コンシューマはイテレータに作用し、幾つかの最終的な値の組を返します。
あなたは既にイテレータとレンジを見てきたのですから、最初にコンシューマについて話しましょう。
コンシューマ
コンシューマとはイテレータに作用し、1種類以上の値を返すものです。最も一般的なコンシューマはcollect()
です。このコードは全くコンパイルできませんが、意図するところは伝わるでしょう。
let one_to_one_hundred = (1..101).collect();
ご覧のとおり、ここでイテレータがcollect()
を呼び出しています。collect()
はイテレータが渡す沢山の値を受け取り、その結果をコレクションとして返します。それならなぜこのコードはコンパイルできないのでしょうか?Rustはあなたが集めたい値の型を判断することができないため、あなたが知っている型を指定する必要があります。こちらのバージョンはコンパイルできます。
let one_to_one_hundred = (1..101).collect::<Vec<i32>>();
もしあなたが覚えているなら、::<>
構文で型ヒントを与える事ができ、整数型のvectorが欲しいと伝えることができます。しかし、常に型をまるごとを書く必要はありません。_
を用いることで部分的に推論してくれます。
let one_to_one_hundred = (1..101).collect::<Vec<_>>();
これは"値をVec<T>
の中に集めて下さい、しかしT
は私のために推論して下さい"という意味です。このため_
は度々"型プレースホルダー"と呼ばれています。
collect()
は最も有名なコンシューマですが、他にもあります。find()
はそのひとつです。
let greater_than_forty_two = (0..100)
.find(|x| *x > 42);
match greater_than_forty_two {
Some(_) => println!("We got some numbers!"),
None => println!("No numbers 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()
を呼び出してみました。
(1..4).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
になるため、3 + 3 = 6
となり、これが最終的な結果となります。1 + 2 + 3 = 6
が、得られる結果となります。
ふぅ、ようやく説明し終わりました。fold
は初めのうちこそ少し奇妙に見えるかもしれませんが、一度理解すればあらゆる場面で使えるでしょう。何かのリストを持っていて、そこから1つの結果を求めたいときならいつでも、fold
は適切な処理です。
イテレータにはまだ話していないもう1つの性質、遅延性があり、コンシューマはそれに関連して重要な役割を担っています。それではもっと詳しくイテレータについて話していきましょう、そうすればなぜコンシューマが重要なのか理解できるはずです。
イテレータ
前に言ったように、イテレータは.next()
メソッドを繰り返し呼び出すことができ、その都度順番に値を返すものです。メソッドを繰り返し呼ぶ必要があることから、イテレータはlazy
であり、前もって全ての値を生成できないことがわかります。例えばこのコードでは、1-100
の値は実際には生成されておらず、単にその代わりとなる値を生成しています。
let nums = 1..100;
私たちはレンジを使っていないため、レンジは値を生成しません。コンシューマを追加してみましょう。
let nums = (1..100).collect::<Vec<i32>>();
今、collect()
は幾つかの値を渡してくれるレンジを要求し、値を生成する作業を行います。レンジは基本的な2つのイテレータのうちの1つです。もう片方はiter()
です。iter()
はvectorを順番に各要素を渡してくれる単純なイテレータに変換できます。
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);
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
また遅延性にぶつかりました!このクロージャは実行されないですね。例えばこれは何の数字も出力されません。
(1..100).map(|x| println!("{}", x));
もし副作用のためにイテレータに対してクロージャの実行を試みるのであれば、代わりにfor
を使いましょう。
興味深いイテレータアダプタは沢山あります。take(n)
は元のイテレータのn
回目までを実行するイテレータを返します。これは元のイテレータに対して副作用を及ぼさないことに注意して下さい。では以前言っていた無限のイテレータを試してみましょう。
#![feature(step_by)]
for i in (1..).step_by(5).take(5) {
println!("{}", i);
}
これの出力は、
1
6
11
16
21
filter()
は引数としてクロージャをとるアダプタです。このクロージャはtrue
かfalse
を返します。filter()
が生成する新たなイテレータはそのクロージャがtrue
を返した要素のみとなります。
for i in (1..100).filter(|&x| x % 2 == 0) {
println!("{}", i);
}
これは1から100の間の偶数を全て出力します。(filter
が反復処理をされている要素を消費1しないようにするために、述語に各要素の参照が渡される&x
パターンを使用して整数自体を抽出していることに注意してください)
あなたはここまでに説明された3つの概念を全て繋げることができます。イテレータから始まり、アダプタを幾つか繋ぎ、結果を消費といった感じです。これを見て下さい。
(1..1000)
.filter(|&x| x % 2 == 0)
.filter(|&x| x % 3 == 0)
.take(5)
.collect::<Vec<i32>>();
これは6, 12, 18, 24,
そして30
が入ったvectorがあなたに返されます。
イテレータ、イテレータアダプタ、そしてコンシューマがあなたの助けになることをほんの少しだけ体験できました。本当に便利なイテレータが幾つも用意されていますし、あなたがイテレータを自作することもできます。イテレータは全ての種類のリストに対し効率的な処理方法と安全性を提供します。これらは初めこそ珍しいかもしれませんが、もし使えばあなたは夢中になることでしょう。全てのイテレータとコンシューマのリストはイテレータモジュールドキュメントを参照して下さい。
-
和訳「消費」 原文 "consume"、コンシューマを適用してイテレータを消費するという意味合い ↩