環境
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を使ったコードはこうなる:
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<T>
型が「0個か1個の値を持つコンテナ」と考えれば、 Vec
や HashMap
等と同じようにイテレータを使ったりループを回せるのは自然だ。
そんなわけで(かどうかは知らないが)、 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 からは、イテレータを使わずとも以下のようなコードで実現できます。
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> -> (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
を返すことになっているので、これを利用する。
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");
}
考え方
仕掛けとしては単純で、 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]);
解説
本来 inspect
は、その名の通り、イテレータを流れる要素を検査する (特にデバッグ目的などでログを吐く) ために使われる。
リファレンスのサンプルコードでもそういった用途で使われている。
この関数は「要素の参照を受け取って ()
を返す」ものであり、副作用を前提に作られているため、実際には表示以外でも、値をイテレータに流すようなことでなければほぼ何でもできる。
そこで、これをログ出力ではなく「流れてきた値を複製して、イテレータ外に用意された変数に保存する」という目的に利用しているのが、上のコードである。
勿論無駄なコピーは発生してしまうが、それが気になるようであれば、最初からもっと効率の良いアダプタを自分で書くなり crate を探すなりループを使うなりするべきである。
まとめ
- イテレータは
for
ループで使える -
Option
はイテレータを作れる -
Iterator
の関数を活用すると、列挙されるものの中身を弄れる- ので、ネストとかを減らせることがある
- 「はがす」「はがさず変化させる」のような操作は、どうにかしてイテレータを使えると考えるべし
- 以下のページは一度全体を読んでおくと様々な場面で活用できるので、全部の関数を眺めておくと、いざというとき「こんな関数あったよな……」と思い出せる
何か良い例や書き方、「こんなクソな書き方しねーよ!」等、ご意見があれば是非教えてください。