環境
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
何か良い例や書き方、「こんなクソな書き方しねーよ!」等、ご意見があれば是非教えてください。