LoginSignup
73
57

More than 5 years have passed since last update.

Rustでイテレータを積極的に使っていくメモ (逐次更新)

Last updated at Posted at 2016-02-07

環境

Rust 1.6.0を前提に。
とはいえ、多少古くても問題ないはず。

前提

for i in x {
    foo(i);
}

というのは、

{
    let mut anonymous_iter = x.into_iter();
    while let Some(i) = anonymous_iter.next() {
        foo(i);
    }
}

と同じだ。


事例1: Option<Vec<_>> の中身のベクタについてループを回す

例として、「 Option<Vec<String>>Some であれば、中のベクタの文字列を大文字にして表示する」ことを考える。

普通に書いたコード
let optvec = Some(vec!["foo".to_string(), 2.to_string(), "bar2baz".to_string()]);
if let Some(ref vec) = optvec {
    for i in vec {
        println!("{:?} => {:?}", i, i.to_uppercase());
    }
}

Optionの中身を if let で取り出す、まあ普通のコード。

iteratorを使ったコードはこうなる:

iteratorを使う
let optvec = Some(vec!["foo".to_string(), 2.to_string(), "bar2baz".to_string()]);
for i in optvec.iter().flat_map(|v| v.iter()) {
    println!("{:?} => {:?}", i, i.to_uppercase());
}

べつに if letmatch を使ってもいいのだが、イテレータをうまく使うとネスト(というかインデント)を減らせるのでうれしい。

ちなみに、 optvec をmoveして良いのであれば、 into_iter を使う方が良い。オブジェクトをconsumeするような関数も呼べるためだ。

// 文字列をバイト列にして表示
for i in optvec.into_iter().flat_map(Vec::into_iter) {
    // iはmoveされてくるので、 `String::into_bytes(self)` が呼べる
    println!("{:?}", i.into_bytes());
} // まあこの場合mapを使った方がいいけどね

解説

Option::iter(), Option::into_iter()

Option<T> 型が「0個か1個の値を持つコンテナ」と考えれば、 VecHashMap 等と同じようにイテレータを使ったりループを回せるのは自然だ。
そんなわけで(かどうかは知らないが)、 Option<T> はイテレータを作ることができる。

for i in Some(1) { // for i in Some(1).into_iter() と同じ
    println!("{}", i); // => 1
}

if let はあらゆる(?)型についてdestructureできるので強いが、 Option については for in を使うという選択肢もあるということだ。
わかりやすいかは別にして。

Vec::iter(), Vec::into_iter()

その名の通り、ベクタの要素を列挙するイテレータを作るメンバ関数。

それは良いとして、なぜ optvec.iter().flat_map(|v| v.iter())|v| v.iter() を使ったか。
ここでの v&Vec<_> だが、実は Vec<T>::iter は存在せず、 v.iter() したとき Deref<Target=[T]> というトレイトによる参照外し経由で slice::iter が呼ばれることで、イテレータを取得できるようになっているのである。
よって、メンバ関数一発で Vec<_> のイテレータを得ることはできないから、暗黙のderefを有効活用して |v| v.iter() と書くのが一番短くなるのである。

Iterator::flat_map()

std::iter::Iterator - Rust ←公式ドキュメントを読めばわかる。
簡単に言えば、
「イテレータに対して、『モノを受け取ってイテレータを返す関数』を受け取り、それぞれのイテレータを繋げて返す」、
つまり Iter<T> -> (T -> Iter<U>) -> Iter<U> という感じの関数だ。(伝われ!)

事例2: Option<T> の中身が条件を満たしていなかったら None にする

追記 2017-11-15: Add Option::filter() according to RFC 2124 by LukasKalbertodt · Pull Request #45863 · rust-lang/rust という機能が入ったので、 Rust-1.22 からは、イテレータを使わずとも以下のようなコードで実現できます。

rust-1.27.0から
let optint = Some(3);
let positive = optint.filter(|&v| v >= 0);
println!("positive: {:?}", positive); // => 3

追記2 2017-12-05: Tracking issue for Option::filter (feature option_filter) · Issue #45860 · rust-lang/rust
Unstable でした……

追記3 2018-06-22: Option::filter は rust 1.27 で安定化されました🎉


例として、「 Option の中身が負であれば None にし、そうでなければそのままにする」ことを考える。

愚直に書いたコード
let optint = Some(3);
let positive = if let Some(i) = optint {
    if i >= 0 {
        Some(i)
    } else {
        None
    }
} else {
    None
};
println!("positive: {:?}", positive); // => 3

Some なら{非負なら Some 、それ以外なら None }、それ以外なら None
という感じ。ネストが重なって汚いうえ、Some を剥がしてまた包むという、なんとも美しくないコードだ。

ちょっとマシなコード
let optint = Some(3);
let positive = optint.and_then(|i| if i >= 0 { Some(i) } else { None });
println!("positive: {:?}", positive); // => 3

Option::and_then() を使って、一行にまとめた。
Cスタイルの三項演算子があればマシになるのだが、残念ながらRustでは if が式として使えるため、三項演算子は用意されていない。
(参考 (さんこうだけに): Remove ternary operator · Issue #1698 · rust-lang/rust)
ちなみに、ifの中括弧は省略できない。

イテレータを活用したコード:

let optint = Some(3);
let positive = optint.into_iter().find(|&v| v >= 0);
println!("positive: {:?}", positive); // => 3

短い。単純さは正義だ。
そして、「剥がして包む」という無駄に見える操作を書かずに済むようになった。

解説

Iterator::find()

std::iter::Iterator - Rust
名前から想像される通り、「与えられた条件を最初に満たした要素を返す(Some)、もしひとつもなければ None 」という関数だ。
これを Option のイテレータに使えば、 None の場合は要素が無いということになるので findNone を返し、 Some で条件を満たさない場合も None を返し、 Some で条件を満たしていればそれを Some で返す、ということになる。

Iterator::and_then()

std::option::Option - Rust
Haskell風に書くと Option<T> -> (T -> Option<U>) -> Option<U> である。
というか、まさしくHaskellで言うところの (>>=) だ。
ちなみに、こいつは Option だけでなく、 Result にも用意されている (std::result::Result - Rust)。

事例3: Option<T> のイテレータで、最初の None の直前までをunwrapしたもののイテレータを得る。 None 以降は捨てる

要するに、 take_while()filter_map() (或いは unwrap())を組み合わせたようなことをしたい場合。

愚直に書いたコード
let vec = vec!["1", "2", "3", "lol", "5"];
for num in vec.into_iter().map(|v| v.parse::<i32>().ok()).take_while(|v| v.is_some()).map(|v| v.unwrap()) {
    print!("{},", num);
}
// 出力: 1,2,3,
ちょっと工夫してみたつもりのコード
let vec = vec!["1", "2", "3", "lol", "5"];
for num in vec.into_iter().map(|v| v.parse::<i32>().ok()).take_while(|v| v.is_some()).filter_map(|v| v) {
    print!("{},", num);
}
// 出力: 1,2,3,

美しくないなぁ。

このコードの根本的な問題は、 一度 is_some() で型(variant)をチェックしておきながら、もう一度 unwrap()filter_map() で全く同じ確認がされる というところにある。
unwrap() だって、panicするか値を返すか選ぶために、ちゃんと型を確認しているのだ。
このオーバーヘッドをなくすためには、 Some であることの確認と、イテレータを切るのを、同時に行わなければならない。

そんな都合の良いメソッドが、実は用意されているのだ。
(思い付かない方は、手前味噌だがRustのイテレータの網羅的かつ大雑把な紹介 - Qiitaや、std::iter::Iterator - Rustを読んでみることをおすすめする。)

Iterator::scan() である。
こいつは、Iterator::fold()の途中経過を見えるようにしたようなものだが、イテレータの返す値として Option を返すことになっているので、これを利用する。

scan()を利用
let vec = vec!["1", "2", "3", "lol", "5"];
for num in vec.into_iter().scan((), |_, v| v.parse::<i32>().ok()) {
    print!("{},", num);
}
// 出力: 1,2,3,

解説

だいたい見ればわかるが。
Iterator::scan() の第1引数(この使い方では ())は、状態である。
今回は状態は不要なので () を渡そう。たぶん最適化がきく。(本当かな?)

第2引数は &mut State -> T -> Option<U> のような関数だ。
&mut State は状態。今回は使わないので _ で受ける。
T は元のイテレータの要素の型、ここでは文字列(&str)だ。これを v で受ける。
関数の戻り値 Option<U>U は、新しいイテレータの要素の型である。
今回はパース後の i32 が欲しいので、 Option<i32> を返す。
ただし parse()Result を返すので、 Result::ok()Option に変換する。

fold との組み合わせ

もし Ok(_) の値を fold() へ流そうとしているのであれば、 scan() を経由せず、 rust 1.27 で安定化された Iterator::try_fold() を直接使うべきである。

事例3: write!() 等でコンマ区切りのリストを表示する(ただしケツカンマは認めない)

多少コードは変化するが、基本的に io::Write でも fmt::Formatter でも使える。

list.iter().try_fold("", |sep, arg| {
    write!(f, "{}{}", sep, arg).map(|_| ", ")
})?;

実際それっぽい感じで使うと、こんな感じ (playground) になる。

或いは、以下のように汎用的な関数を作ることもできる。

use std::fmt;
use std::io;

fn write_with_sep<W, T, I>(mut w: W, iter: I, sep: &str) -> io::Result<()>
where
    W: io::Write,
    T: fmt::Display,
    I: IntoIterator<Item = T>,
{
    iter.into_iter().try_fold("", |s, item| {
        w.write_fmt(format_args!("{}{}", s, item))?;
        Ok(sep)
    }).map(|_| ())
}

fn main() {
    let src = vec![1, 2, 4, 8, 16];
    write_with_sep(io::stdout(), src, " => ").expect("Write failed");
}

(playground)

考え方

仕掛けとしては単純で、 try_fold は基本的に fold と同じで「前の要素を処理した結果を次の要素の処理へ渡す」という役割を持っている。
そこで、これを「処理の結果」である出力成功/失敗の伝達と、「区切りが必要か否か」の伝達の両方に同時に使ってやろうという発想である。

フラグを使った素朴な実装
let mut needs_leading_comma = false;
for item in iter {
    if needs_leading_comma {
        w.write_fmt(format_args!("{}", sep))?;
    }
    write!(w, "{}", item)?;
    needs_leading_comma = true;
}
Ok(())

if で「何も表示しない」コードと分岐する代わりに、「空文字列を表示する」コードにすることで分岐をまとめることができる。

ifを消した
let mut leading_sep = "";
for item in iter {
    write!(w, "{}{}", leading_sep, item)?;
    leading_sep = sep;
}
Ok(())

ここで、ループ中で伝達されるべき「状態」は leading_sep 、初期値は "" である。
try_fold で使うために、 leading_sepsep を代入する代わりに Ok(sep) を返す。

iter.into_iter().try_fold("", |leading_sep, item| {
    write!(w, "{}{}", leading_sep, item)?;
    Ok(sep)
})?;
Ok(())

はい。
お好みで .map(|_| ()) もどうぞ。

事例4: take_while で読み捨てられる最後の値を拾う

let a = [1, 2, 3, 4];
let mut iter = a.into_iter();

let result: Vec<i32> = iter
    .by_ref()
    .take_while(|n| **n != 3)
    .cloned()
    .collect();

assert_eq!(result, &[1, 2]);

let result: Vec<i32> = iter.cloned().collect();

assert_eq!(result, &[4]);

playground, 公式リファレンス の例より

この例からわかるように、3は読み捨てられてしまう。
これを拾いたい場合にどうするか。

inspect を使う。

let a = [1, 2, 3, 4];
let mut iter = a.into_iter();

let mut last_read = None; // <- Keep the last value
let result: Vec<i32> = iter
    .by_ref()
    .inspect(|n| last_read = Some(**n)) // <- Store the last value
    .take_while(|n| **n != 3)
    .cloned()
    .collect();

assert_eq!(result, &[1, 2]);
assert_eq!(last_loaded, Some(3)); // Here you are

let result: Vec<i32> = iter.cloned().collect();

assert_eq!(result, &[4]);

playground

解説

本来 inspect は、その名の通り、イテレータを流れる要素を検査する (特にデバッグ目的などでログを吐く) ために使われる。
リファレンスのサンプルコードでもそういった用途で使われている。

この関数は「要素の参照を受け取って () を返す」ものであり、副作用を前提に作られているため、実際には表示以外でも、値をイテレータに流すようなことでなければほぼ何でもできる。
そこで、これをログ出力ではなく「流れてきた値を複製して、イテレータ外に用意された変数に保存する」という目的に利用しているのが、上のコードである。

勿論無駄なコピーは発生してしまうが、それが気になるようであれば、最初からもっと効率の良いアダプタを自分で書くなり crate を探すなりループを使うなりするべきである。


まとめ

  • イテレータは for ループで使える
  • Option はイテレータを作れる
  • Iterator の関数を活用すると、列挙されるものの中身を弄れる
    • ので、ネストとかを減らせることがある
    • 「はがす」「はがさず変化させる」のような操作は、どうにかしてイテレータを使えると考えるべし
  • 以下のページは一度全体を読んでおくと様々な場面で活用できるので、全部の関数を眺めておくと、いざというとき「こんな関数あったよな……」と思い出せる

何か良い例や書き方、「こんなクソな書き方しねーよ!」等、ご意見があれば是非教えてください。

73
57
0

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
73
57