ResultやOptionが要素型の場合のiteratorの捌き方
Iterator<Result<T, E>>
とかIterator<Option<T>>
の扱い方がやっぱりむずいな、と思ったので、Result
やOption
が要素型の場合のiteratorの捌き方をまとめてみた。
枠組み
- 計算手法は2つ+αかな。
- Projection Operation(射影)
- Aggregation Operation(集計)
- Scan operation(走査1)
- 各要素が以下のデータ型であるような2iteratorを取り扱う。
- Option
- Result
- 計算が失敗したときの振る舞いも大きく分けて2つ。
- 終了する
- 継続する
で、(1)×(2)×(3)を組み合わせたときの処理が、rustで関数プログラミング的にどう実装されるのか例を通じてまとめたい。
進め方としては、1.の各計算手法に対して例を取り上げる。2.に関しては、Option
やResult
が登場する例として、前者では、std::collection::HashMap
のgetメソッドを、後者では、文字列のパースstr.parse::<T>
を取り扱う。その中で、3.でそれぞれ失敗したときの取扱いについて、それぞれ実装する。
各節独立に読めます3!
環境
rustc --version
rustc 1.19.0 (0ade33941 2017-07-17)
projection
Option<A>
の場合
(お題): あるHashMapが用意されていて、ある配列の各要素をkeyとしたときのvalueを求めたい.例えば、以下のようなHashMapがあったとき、
use std::collections::HashMap;
// create sample hashmap (assume that it's large) {'a':1, 'b':2...}
let hashmap = (b'a'..b'z')
.map(|s| s as char)
.zip(1..)
.collect::<HashMap<_,_>>();
['b', 'a', 'd', 'g', 'v']
に関しては、[2,1,4,7,22]
が答えとなる。今回は例として、
// もちろん0は英字小文字でないので失敗する。
let v = vec!['b', 'a', '0', 'g', 'v'];
について、失敗したときの実装を各種試みたい4。
- 失敗したら計算を終了させる場合
/* 失敗したら計算不可とする場合。 */
// Out: None
v.iter()
.map(|s| hashmap.get(s))
.collect::<Option<Vec<_>>>();
// Out: Some([2,1]) はじめの2つは成功するので
v.iter()
.map(|s| hashmap.get(s))
.take(2)
.collect::<Option<Vec<_>>>();
/* 失敗する直前までの結果を返す */
// Out: [2,1]
// scanの第一引数は状態。今回は前回までの状態を保存する必要が無いので、unit型とした
v.iter()
.scan((), |_, x| hashmap.get(x))
.collect::<Vec<_>>();
- 失敗しても計算を継続する場合
/* 失敗した場合でも、Noneを返したうえで、計算を継続させる */
// Out: [2,1,None,7,22]
v.iter()
.map(|s| hashmap.get(s))
.collect::<Vec<_>>();
/* 失敗した場合、失敗したものを無視して、計算を継続させる */
// Out: [2,1,7,22]
v.iter()
.filter_map(|s| hashmap.get(s))
.collect::<Vec<_>>();
// もしくは、flat_mapを使っても良い
// Out: [2,1,7,22]
v.iter()
.flat_map(|s| hashmap.get(s))
.collect::<Vec<_>>();
/* 失敗した場合、-1をsetした上で、計算を継続させる */
// Out: [2,1,-1,7,22]
let DEFAULT_VALUE = -1;
let m: Vec<i32> = v.iter()
.map(|s| *(hashmap.get(s).unwrap_or(&DEFAULT_VALUE)))
.collect();
Result<T,E>
の場合
(お題): 配列の各要素(文字列)を整数として読み取って、1足した結果を出力とする5.例えば、
["1","2","3", "4"]
だったら、[2,3,4,5]: Vec<i32>
のように出力したい。今回は例として、
let v = vec!["1","2","3b", "4", "5"];
について、失敗したときの実装を各種試みたい。(Option<A>
の場合と似たような感じになるが、一応。)
- 失敗したら計算を終了させる場合
/* 失敗したら計算不可とする場合。 */
// Out: Err(std::num::ParseIntError { kind: InvalidDigit }));
v.iter()
.map(|s|
s.parse::<i32>()
.map(|s| s+1))
.collect::<Result<Vec<_>, _>>();
// Out: Ok([2,3]) 先頭の要素2つは成功するので。
v.iter()
.map(|s|
s.parse::<i32>()
.map(|s| s+1))
.take(2)
.collect::<Result<Vec<_>, _>>();
/* 失敗する直前までの結果を返す */
// Out: [2,3]
v.iter()
.scan((), |_,&x|
x.parse::<i32>()
.map(|t| t+1)
.ok())
.collect::<Vec<_>>();
- 失敗しても計算を継続する場合
/* 失敗した場合でも、Err値を返したうえで、計算を継続させる */
// Out: [Ok(2), Ok(3), Err(ParseIntError { kind: InvalidDigit }), Ok(5), Ok(6)]
v.iter()
.map(|s|
s.parse::<i32>()
.map(|s| s+1))
.collect::<Vec<_>>();
/* 失敗した場合、失敗したものを無視して、計算を継続させる */
// Out: [2,3,5,6]
v.iter().
flat_map(|s|
s.parse::<i32>())
.map(|s| s+1)
.collect::<Vec<_>>();
// もしくはfilter_mapを使う
v.iter()
.filter_map(|s|
s.parse::<i32>()
.ok())
.map(|s| s+1)
.collect::<Vec<_>>();
/* 失敗した場合、-1をsetした上で、計算を継続させる */
// Out: [2,3,-1,5,6]
let DEFAULT_VALUE = -1;
v.iter()
.map(|s|
s.parse::<i32>()
.unwrap_or(DEFAULT_VALUE))
.collect::<Vec<_>>();
こんな感じで、aggregation
やscan
の場合も進んでいきたいと思います。
aggregation
Option<A>
の場合
(お題): あるHashMapが用意されていて、ある配列の各要素をkeyとしたときのvalueの総和を求めたい。例えば、以下のようなHashMapがあったとき、
use std::collections::HashMap;
// create sample hashmap (assume that it's large) {'a':1, 'b':2...}
let hashmap = (b'a'..b'z')
.map(|s| s as char)
.zip(1..)
.collect::<HashMap<_,_>>();
['b', 'a', 'd', 'g', 'v']
に関しては、36
が答えとなる([2,1,4,7,22]
なので)。今回は例として、
// もちろん0は英字小文字でないので失敗する。
let v = vec!['b', 'a', '0', 'g', 'v'];
について、失敗したときの実装を各種試みたい。
- 失敗したら計算を終了させる場合
/* 失敗したら計算不可とする場合。 */
// option型に関しては残念ながら、sumは用意されていないので、foldする。
// Out: None
v.iter()
.map(|s| hashmap.get(s))
.fold(Some(0), |s, x|
// Note) 関数(この場合はstd::ops::Addの`add` method)のリフトとかできればスッキリかけると思いますが、
// ちょっとわからんので、and_then以下で泥臭く書きました。
s.and_then(|ss|
x.map(|xx| xx+ss)));
);
// もしくは、result型にはsumメソッドがあるので、こうするのも良いかも。
// ok_orには何でもいいが、unit( `()` )をつけてる
// Out: None
v.iter()
.map(|s|
hashmap.get(s)
// Result<i32, ()>に変換
.ok_or(()))
.sum::<Result<i32, _>>()
.ok();
- 失敗しても計算を継続する場合
/* 失敗した値に対しては無視して、計算を継続する */
// sumのi32は戻り値の型を推論するのに必要。
// Out: 32
v.iter()
.flat_map(|s|
hashmap.get(s))
.sum::<i32>();
// 途中結果が気になったら、間にinspectをはさもう。
v.iter()
.flat_map(|s|
hashmap.get(s))
// `2, 1, 7, 22,`が中間結果として出力される:
.inspect(|s|
print!("{:?}, ", s))
.sum::<i32>();
Result<T,E>
の場合
(お題): 配列の各要素(文字列)を整数として読み取った後、総和を計算したい。例えば、["1","2","3", "4"]
だったら、10
が答えとなるようにしたい。今回は例として、
let v = vec!["1","2","3b", "4", "5"];
について、失敗したときの実装を各種試みたい。(これも、Option<A>
の場合と似たような感じになるが、一応。)
- 失敗したら計算を終了させる場合
/* 失敗したら計算不可とする場合。 */
// Out: Err(ParseIntError { kind: InvalidDigit })
// Result のi32はsumの型を決定するのに必要。
v.iter()
.map(|s|
s.parse::<i32>())
.sum::<Result<i32, _>>();
// Out: Ok(3) (2個目まではparse失敗しないので1+2)
v.iter()
.map(|s| s.parse::<i32>())
.take(2)
.sum::<Result<i32, _>>();
- 失敗しても計算を継続する場合
/* 失敗した値に対しては無視して、計算を継続する */
// Out: 12 (=1+2+4+5 なので)
v.iter()
.flat_map(|s|
s.parse::<i32>())
.sum::<i32>();
scan
Option<A>
の場合
(お題): あるHashMapが用意されていて、ある配列の各要素をkeyとしたときのvalueの総和を求めたい。ただし、aggregateの場合と違って、総和計算の途中結果も格納する。例えば、以下のようなHashMapがあったとき、
use std::collections::HashMap;
// create sample hashmap (assume that it's large) {'a':1, 'b':2...}
let hashmap = (b'a'..b'z')
.map(|s| s as char)
.zip(1..)
.collect::<HashMap<_,_>>();
['b', 'a', 'd', 'g', 'v']
に関しては、[2,3,7,14,36]が答えとなる。([2, 2+1, 2+1+4, 2+1+4+7, 2+1+4+7+22]
なので)。今回は例として、
// もちろん0は英字小文字でないので失敗する。
let v = vec!['b', 'a', '0', 'g', 'v'];
について、失敗したときの実装を各種試みたい。
- 失敗したら計算を終了させる場合
/* 失敗した場合、Noneを返す。成功したときはリストを返す */
// Out: None
v.iter()
.map(|s| hashmap.get(&s))
.scan(Some(0), |s, x| {
*s = x.and_then(|&xx|
s.map(|ss|
ss+xx));
Some(*s)
})
// Result<Vec<_>,_>と指定するのがポイント
.collect::<Option<Vec<_>>>();
// Out: Some([2, 3]) // 二つ目までは正常にscanできる
v.iter()
.map(|s| hashmap.get(&s))
.scan(Some(0), |s, x| {
*s = x.and_then(|&xx|
s.map(|ss|
ss+xx));
Some(*s)
})
.take(2)
.collect::<Option<Vec<_>>>();
/* 失敗する直前までの結果を返す */
// Out: [2,3]
v.iter()
.map(|s| hashmap.get(&s))
.scan(Some(0), |s, x| {
*s = x.and_then(|&xx|
s.map(|ss|
ss+xx));
*s
})
.collect::<Vec<_>>();
/* 失敗した場合、失敗した要素から先の要素はNoneが返される */
// Out: [Some(2), Some(3), None, None, None]
v.iter()
.map(|s| hashmap.get(s))
// sはResult型でmoveされるので、cloneする必要がある。
.scan(Some(0), |s, x| {
*s = x.and_then(|&xx|
s.map(|ss|
ss+xx));
Some(*s)
})
.collect::<Vec<_>>();
- 失敗しても計算を継続する場合
/* 失敗した場合、結果を据え置きのち、継続する */
// Out: [2, 3, 3, 10, 32]
v.iter()
.map(|s| hashmap.get(&s))
.scan(0, |s, x| {
*s = x.map(|&xx| xx+*s)
// ここで、失敗したときに前の状態を維持する
.unwrap_or(*s);
Some(*s)
})
.collect::<Vec<_>>();
// もしくは、map_orを使えばもっと簡潔に書ける。
// Out: [2, 3, 3, 10, 32]
v.iter()
.map(|s| hashmap.get(&s))
.scan(0, |s, x| {
*s = x.map_or(*s, |&xx|
xx+*s);
Some(*s)
})
.collect::<Vec<_>>();
/* 失敗した結果を完全に無視して、継続する */
// Out: [2, 3, 10, 32]
v.iter()
.flat_map(|s| hashmap.get(&s))
.scan(0, |s,&x| {
*s = x+(*s);
Some(*s)
}).collect::<Vec<_>>();
Result<T,E>
の場合
(お題): 配列の各要素(文字列)を整数として読み取って、配列を順番に足し上げた結果を出力する。ただし、今回は、途中経過も見えるようにする。例えば、
["1","2","3", "4"]
だったら、[1,3,6,10]: Vec<i32>
のように出力したい。
今回は例として、
let v = vec!["1","2","3b", "4", "5"];
について、失敗したときの実装を各種試みたい。(Option<A>
の場合と似たような感じになるが、一応。)
- 失敗したら計算を終了させる場合
/* 失敗した場合、Errを返す。成功したときはリストを返す */
// Out: Err(ParseIntError { kind: InvalidDigit })
v.iter()
.map(|s| s.parse::<i32>())
.scan(Ok(0), |s, x| {
*s = x.and_then(|xx|
// sはResult型でmoveされるので、cloneする必要がある。
s.clone()
.map(|ss| ss+xx));
Some(s.clone())
})
// Result<Vec<_>,_>と指定するのがポイント
.collect::<Result<Vec<_>,_>>();
// Out: Ok([1, 3])
v.iter()
.map(|s| s.parse::<i32>())
.scan(Ok(0), |s, x| {
*s = x.and_then(|xx|
s.clone()
.map(|ss| ss+xx));
Some(s.clone())
})
.take(2)
.collect::<Result<Vec<_>,_>>();
/* 失敗する直前までの結果を返す */
// Out: [1, 3]
v.iter()
.map(|s| s.parse::<i32>())
.scan(Ok(0), |s, x| {
*s = x.and_then(|xx|
s.clone()
.map(|ss| ss+xx));
s.clone()
.ok()
})
.collect::<Vec<_>>();
/* 失敗した場合、失敗した要素から先の要素はErrを返す */
// Out: [Ok(1), Ok(3), Err(ParseIntError { kind: InvalidDigit }), Err(ParseIntError { kind: InvalidDigit }), Err(ParseIntError { kind: InvalidDigit })]
v.iter()
.map(|s| s.parse::<i32>())
// sはResult型でmoveされるので、cloneする必要がある。
.scan(Ok(0), |s, x| {
*s = x.and_then(|xx|
s.clone()
.map(|ss| ss+xx));
Some(s.clone())
})
.collect::<Vec<_>>();
- 失敗しても計算を継続する場合
/* 失敗した場合、結果を据え置きのち、継続する */
// Out: [1,3,3,7,12]
v.iter()
.map(|s| s.parse::<i32>()
.ok())
.scan(0, |s, x| {
*s = x.map_or(*s, |xx|
xx+(*s));
Some(*s)
})
.collect::<Vec<_>>();
/* 失敗した結果を完全に無視して、継続する */
// Out: [1,3,7,12]
v.iter()
.flat_map(|s|
s.parse::<i32>())
.scan(0, |s,x| {
*s = x+(*s);
Some(*s)
}).collect::<Vec<_>>();
まとめ
- ResultやOptionが要素型の場合のiteratorの捌き方をまとめてみた1。
- 射影と集計の処理は似たような感じだが、走査の書き方がむずい(使用頻度は少ないものの)
感想
-
もっと簡潔に書ける部分がありそうだな、と思いつつ自分のrustに関するライブラリ知識が足りないので、コメントとか頂けるとうれしいです。
-
参考文献の3を以前に読んだことがあって、なにこれ、むずっ
と思ったけど、今になって役立ってる感ある。
-
iterator自体の使い方としてはここまで網羅できていれば、ほぼ十分な感じはありますが、実践で使っていると、意外と高階関数の引数に突っ込むクロージャー周りで詰まることが多いです6。こちらはちょっとまだ使いこなせていない感があるので、次回書きたいです。
-
collect
関数一つでIterator<Result<T, E>>
からResult<Vec<T>, E>
にも、Vec<Result<T>, E>
にもconvertできるのは、慣れればめっちゃ便利(FromIteratorがきちんと定義されてるからなんだけど)。
参考
- http://qnighy.hatenablog.com/entry/2017/06/14/220000
- https://qiita.com/tatsuya6502/items/cd41599291e2e5f38a4a
- https://www.amazon.co.jp/Scala関数型デザイン-プログラミング-―Scalazコントリビューターによる関数型徹底ガイド-impress-gear/dp/4844337769/ref=sr_1_1?s=books&ie=UTF8&qid=1509109822&sr=1-1&keywords=Scala関数型デザイン%26プログラミング 第一部はまさに、本記事の内容とかぶる。