イテレータ
イテレータとは、一連のデータを順番に返すメソッドを持つデータです。言葉で説明するのは難しいので、コードを示します。
fn main() {
let name_list: Vec<i32> = vec![1, 3, 5, 7];
// into_iter()メソッドはイテレータを作るメソッド。
// IntoIteratorトレイトを実装することでイテレータを作成できるようになる。
let mut itre = name_list.into_iter();
// イテレータに対してnext()メソッドを呼ぶと、順番にデータを返す。
// イテレータの終わりを示すため、戻り値はOption型に包まれる。
// Noneが返ってきたら最後のデータを返したということ。
println!("{:?}", &mut itre.next()); // Some(1)
println!("{:?}", &mut itre.next()); // Some(3)
println!("{:?}", &mut itre.next()); // Some(5)
println!("{:?}", &mut itre.next()); // Some(7) これが最後のデータ
println!("{:?}", &mut itre.next()); // None Noneなのでイテレータが消費されたと分かる
println!("{:?}", &mut itre.next()); // None
println!("{:?}", &mut itre.next()); // None
}
イテレータの使い方
イテレータに対してnext()
メソッドを直接呼ぶ使い方は通常しません。
イテレータトレイトに実装された様々なメソッドを利用するためにイテレータを作成します。
大きく分けて2種類のメソッドがあります。
消費アダプタ
イテレータからイテレータ以外のデータを返すメソッドです。実際に値を取り出すため、イテレータは消費されます。
以下のようなメソッドがあります。
fn main() {
// スライスはiter()メソッドでイテレータにすることが出来る。
let sum = [1, 2, 3, 4].iter().sum::<i32>(); // 合計
let max = [1, 2, 3, 4].iter().max(); // 最大(Option型)
let min = [1, 2, 3, 4].iter().min(); // 最小(Option型)
let cnt = [1, 2, 3, 4].iter().count(); // 要素数
println!(
"sum = {:?}, max = {:?}, min = {:?}, cnt = {:?}",
sum, max, min, cnt
); // sum = 10, max = Some(4), min = Some(1), cnt = 4
// find()はboolを返すクロージャを引数に取り、クロージャの返り値がtrueになる最初の要素を返す。
// クロージャがtrueを返さなかった場合はNoneを返す。
let taro = ["太郎", "花子", "一郎", "清"]
.iter()
.find(|&&x| x == "太郎");
println!("{:?}", taro); // Some("太郎")
// nth()は数値を引数に取り、引数の順番に位置する要素を返す。
let ichiro = ["太郎", "花子", "一郎", "清"].iter().nth(2);
println!("{:?}", ichiro); // Some("一郎")
// fold()はアキュームレータに対してイテレータの各要素を使って処理を積み重ねていくメソッド。
// 以下の例ではaccがアキュームレータ。アキュームレータの初期値は "".to_string() (空のString)。
// 空の文字列にイテレータの各要素を追加していく。
let names = ["太郎", "花子", "一郎", "清"]
.iter()
.fold("".to_string(), |acc, x| acc + x);
println!("{:?}", names); // "太郎花子一郎清"
// 以下の例ではアキュームレータをタプルにして合計、最大、最小、要素数をそれぞれ求めている。
let summary = [1, 2, 3, 4]
.iter()
.fold((0, 1, 1, 0), |acc, &x| {
(
acc.0 + x,
if acc.1 < x { x } else { acc.1 },
if acc.2 > x { x } else { acc.2 },
acc.3 + 1,
)
});
println!("(sum, max, min, count) = {:?}", summary); // (sum, max, min, count) = (10, 4, 1, 4)
}
イテレータアダプタ
イテレータからイテレータを返すメソッドです。イテレータアダプタを繰り返し適用することも可能です。
以下のようなメソッドがあります。
fn main() {
// collect()はイテレータをベクタなどの複数のデータを格納するデータ型に変換する。
// enumerate()はイテレータの要素と順番を含んだタプルのイテレータを返す。
// イテレータにインデックスを付加するイメージ。
let enumerate = vec![1, 2, 4, 8, 16, 32, 64, 128, 256]
.into_iter()
.enumerate()
.collect::<Vec<(usize, u32)>>();
println!("{:?}", enumerate); // [(0, 1), (1, 2), (2, 4), (3, 8), (4, 16), (5, 32), (6, 64), (7, 128), (8, 256)]
// (1..5)は1から4まで1ずつカウントアップするイテレータ。
// map()はイテレータの各要素に対して、引数で受け取ったクロージャ実行し、クロージャの返り値を要素とするイテレータを返す。
let twice = (1..5).map(|x| x * 2).collect::<Vec<i32>>();
println!("{:?}", twice); // [2, 4, 6, 8]
// (37..)は37から1ずつ無限にカウントアップするイテレータ。
// take()は引数の数値の順番までの要素を持つイテレータを返す。
// filter()はboolを返すクロージャを引数に取り、要素に対してクロージャを実行し、クロージャの返り値がtrueの要素のみを持ったイテレータを返す。
// 以下の例では37の倍数を小さい順に10個得ることが出来る。
let multiple_of_37 = (37..)
.filter(|x| x % 37 == 0)
.take(10)
.collect::<Vec<u32>>();
println!("{:?}", multiple_of_37); // [37, 74, 111, 148, 185, 222, 259, 296, 333, 370]
}
イテレータは遅延評価
イテレータは作成しただけでは何もしません。next()
メソッドか消費アダプタを呼び出して初めて処理が実行されます。
この性質のおかげで、イテレータアダプタを呼び出すたびに大量のデータを生成することがなく、無駄がないです。さらに、無限に続くイテレータを扱うことも可能となります。
下のプログラムでは無限に続くイテレータを作成しています。
さらにそのイテレータに対してmap()
を呼び出しています。このメソッドを呼んだ段階ではイテレータは無限の要素数を持っています。しかし、メモリが枯渇することはありません。
無限に続くイテレータは実際に無限のデータを持っているのではなく、無限にデータを返すだけです。要素数が分からないまま任意の処理をした後、必要な分だけデータを取り出せば良いのです。
fn main() {
// 1..は無限に値をカウントアップするイテレータだが、メモリを無限に消費することはない。
let endless_itre = 1..;
// 1000以下の123の倍数をリストアップする。
let multiple_of_123 = endless_itre
.map(|x| x * 123) // この時点ではイテレータは無限の要素数を持っている。
.take_while(|&x| x <= 1000) // ここでイテレータの要素数が有限個になる。
.collect::<Vec<usize>>(); // ここで初めて計算が実行され、ベクタが作成される。
println!("{:?}", multiple_of_123);
//[123, 246, 369, 492, 615, 738, 861, 984]
}
余談
forループを使うかイテレータアダプタを使うか悩むことあります。特に、fold()
やscan()
はクロージャで処理を記述でき、状態を持てるため、何でも出来る気がします。
イテレータの詳細な動きがよく分かっていません。特に、無限に続くイテレータを初めて見たときはなぜメモリが枯渇しないのかと不思議に思いました。イテレータがデータを持っており、イテレータアダプタを呼び出すたびに新たなデータが作成されると考えていたからです。しかし実際は少なくともすべてのデータは持ってません。謎です。とりあえず、各イテレータアダプタの処理を合成し、1つづつデータを処理しているのだろうと考えています。まあ、そんなことは分からなくてもイテレータは便利です。