環境

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> が 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を使ったコードはこうなる:

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 let や match を使ってもいいのだが、イテレータをうまく使うとネスト(というかインデント)を減らせるのでうれしい。

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

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

Iterator::flat_map()

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

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

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

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 の場合は要素が無いということになるので find も None を返し、 Some で条件を満たさない場合も None を返し、 Some で条件を満たしていればそれを Some で返す、ということになる。

Iterator::and_then()

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

事例3: Option のイテレータで、最初の 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 を返すことになっているので、これを利用する。

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 のような関数だ。
&mut State は状態。今回は使わないので _ で受ける。
T は元のイテレータの要素の型、ここでは文字列(&str)だ。これを v で受ける。
関数の戻り値 Option の U は、新しいイテレータの要素の型である。
今回はパース後の i32 が欲しいので、 Option を返す。
ただし 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 で「何も表示しない」コードと分岐する代わりに、「空文字列を表示する」コードにすることで分岐をまとめることができる。

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

ここで、ループ中で伝達されるべき「状態」は leading_sep 、初期値は “” である。
try_fold で使うために、 leading_sep に sep を代入する代わりに 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 の関数を活用すると、列挙されるものの中身を弄れる

ので、ネストとかを減らせることがある
「はがす」「はがさず変化させる」のような操作は、どうにかしてイテレータを使えると考えるべし

以下のページは一度全体を読んでおくと様々な場面で活用できるので、全部の関数を眺めておくと、いざというとき「こんな関数あったよな……」と思い出せる

std::iter::Iterator – Rust
std::option::Option – Rust
std::result::Result – Rust

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

广告
将在 10 秒后关闭
bannerAds