Rust の std::iter::Iterator
トレイトには filter_map
という関数が用意されている。この関数に渡すクロージャ内でエラーが発生するようなケースを取り扱いたい場合に、どのようにすれば対応できるのかを以下で説明する。
そもそも filter_map
とは?
Rust ではイテレータを使って配列やベクタなどを処理するのは当たり前のように行われる。
流れとしては
-
.into_iter()
,.iter()
のように配列やベクタからイテレータを生成するステップ -
.skip()
,.map()
,filter()
のように要素に処理を行う、別のイテレータを生成するステップ -
.for_each()
,.collect()
,.reduce()
のようにイテレータを消費するステップ
の3つに分類される。
以下に map
を使う場合の例となるコードを与える。
let num_list: Vec<u8> = vec![3,4,5,7];
let squared_num_list: Vec<u8> =
// ベクタからイテレータを生成
num_list.into_iter()
// イテレータを消費し、処理を行った別のイテレータを生成
.map( |num| num*num )
// イテレータを消費して新しいベクタにする
.collect::<Vec<u8>>();
Rust には要素を写像する map
と、要素を取捨選択する filter
を組み合わせた filter_map
が存在する。
map
も filter
もイテレータ要素か、その参照を引数にとるクロージャを与え、 map
なら変換後の新しい値を返し、 filter
なら取るか捨てるかの bool
型の値を返す。
Rust には Some(T)
と None
から構成される Option<T>
型があるので、これら2つの機能を組み合わせた filter_map
関数が用意できる。
trait Iterator {
// イテレートする内容の型
type Item;
// `map` 関数の定義 (`Map` は写像後の新しいイテレータ)
fn map<T,F>(self, f: F) -> Map<Self, F>
where F: FnMut(Self::Item) -> T;
// `filter` 関数の定義 (`Filter` は取捨選択後の新しいイテレータ)
fn filter<T,F>(self, f: F) -> Filter<Self, F>
where F: FnMut(&Self::Item) -> bool;
// `filter_map` 関数の定義 (`FilterMap` は写像+取捨選択後の新しいイテレータ)
fn filter_map<T,F>(self, f: F) -> FilterMap<Self, F>
where F: FnMut(Self::Item) -> Option<T>;
}
実例は以下の通り
let num_list: Vec<u8> = vec![3,4,5,7];
let odd_squared_num_list: Vec<u8> =
num_list.into_iter()
.filter_map(|num| {
// 奇数だけ使う
if num % 2 === 1 { Some( num*num ) }
else None
})
.collect::<Vec<u8>>();
上記の例だと filter_map
に与えたクロージャが特にエラーになることはないが、場合によってはクロージャ内でエラーを発生させうる処理をしたいこともある。要素ごとに順番に filter_map
のクロージャを呼び出して、どこかでエラーが発生したら、その時点で終了するようにしたい。
以下ではこれを実現する方法を説明する。
2つのアプローチ
エラーハンドリング付きの filter_map
を挟む方法は自分が知る限り2種類存在している。
標準ライブラリを使う方法
Rust 標準ライブラリだけでこのような操作はできる。
let num_list: Vec<u8> = vec![0,3,4,5,7];
let odd_squared_num_list: Vec<u8> =
num_list.into_iter()
.filter_map(|num| {
// 0 の場合はエラーにする
if num == 0 { return Some(Err("エラー内容")) }
// 奇数だけ使う
if num % 2 == 1 { Some(Ok( num*num )) }
else { None }
})
.collect::<Result<Vec<u8>,_>>()?;
先ほどの例との違いは
- クロージャの戻り値の型が
Option<T>
からOption<Result<T,E>>
に変わる -
collect()
で消費したイテレータを集める先が単なるVec<T>
からResult<Vec<T>,E>
に変わる -
?
でResult<Vec<T>,E>
のエラーを外に伝播させることで、依然としてVec<T>
が返される (オプション)
詳細
.collect()
は次のように定義されている。
impl Iterator {
type Item;
fn collect<C>(self) -> C
where C: FromIterator<Self::Item>;
}
つまり C
が FromIterator<T>
トレイトを実装している場合に限って実行できる。
通常の .collect::<Vec<T>>()
ができるのは次のように Vec<T>
に対して FromIterator<T>
が実装されているからである。
impl<T> FromIterator<T> for Vec<T>
同様にして Result
に対しても FromIterator<T>
が実装されている。
impl<T,E,C> FromIterator<Result<T,E>> for Result<C,E>
where C: FromIterator<T>
つまり、既に FromIterator<T>
が実装されているコレクション型 C
であれば Result<T,E>
のイテレータを消費して Result<C,E>
を作ることができる。 C
として Vec<T>
を選んだのが今回の例である。これはイテレート中に Err
の要素があれば全体も Err
で終了するようだ。
fallible-iterator
を使う方法
エラーが発生しうるイテレータを扱うための fallible-iterator
というクレートが存在する。ここでは FallibleIterator
というイテレータのトレイトが用意されており、通常の std::iter::Iterator
トレイトの関数と同様の関数で、クロージャの返値の型が Result<*,E>
に置き換わっているため、関数内でエラーが発生した場合にそこで終了できるように設計されている。
FallibleIterator
に含まれる filter_map()
を使うようにすれば、次のように実装することができる。
use fallible_iterator::{ IteratorExt, FallibleIterator };
let num_list: Vec<u8> = vec![0,3,4,5,7];
let odd_squared_num_list: Vec<u8> =
num_list.into_iter()
// `Iterator` を `FallibleIterator` に変換
.map(|num| Ok(num) ).transpose_into_fallible()
.filter_map(|num| {
// 0 の場合はエラーにする
if num == 0 { Err("エラー内容")?; }
Ok(
// 奇数だけ使う
if num % 2 == 1 { Some( num*num ) }
else { None }
)
})
.collect::<Vec<u8>>()?;
.collect()
の型注釈は、 Result<Vec<u8>,_>
などとせず、単純にエラーでない場合のコレクション型である Vec<u8>
を記すだけで Result
型の値を返す。
詳細
.filter_map()
の前に .map().transpose_into_fallible()
を挟んでいる。これは Iterator<Item=u8>
を FallibleIterator<Item=u8,Error=??>
に変換している。
.transpose_into_fallible()
は Iterator<Item=Result<T,E>>
を満たす型を FallibleIterator<Item=T,Error=E>
を満たす型に変換する。
trait IteratorExt {
// `Convert` は `FallibleIterator` を満たす型
fn transpose_into_fallible<T,E>(self) -> Convert<Self>;
where Self: Iterator<Item=Result<T,E>>;
}
つまり予め Result<T,E>
型の要素を持つ Iterator
を準備しなくてはならず、そのために .map(|num| Ok(num) )
を挟んで T
を Result<T,??>
に変換している。
しかし、 Ok(num)
とするだけでは Result<T,E>
のエラー型 E
を決めることはできず、 .transpose_into_fallible()
を経た後も FallibleIterator
でのエラー型は不定である。この後に続く .filter_map()
により初めて実際のエラー型が決まる。 (或いは .map()
のクロージャの返値の型を明示的に示してもよい)
次に .filter_map()
について。 Iterator
の .filter_map()
ではそれ自身に Result
型を処理する方法は備わっていない一方で、 FallibleIterator
の .filter_map()
では Result
型を返さなくてはならない。
trait FallibleIterator {
// イテレートする内容の型
type Item;
// エラーが発生した場合のエラー内容の型
type Error;
fn filter_map<T,F>(self, f: F) -> FilterMap<Self, F>
where F: FnMut(Self::Item) -> Result<Option<T>,Self::Error>;
}
2つの方法を比較
以上2つの方法を比較した。これら2つの違いはざっくりとこのような感じである。
- 依存クレートの有無
-
.map().transpose_into_fallible()
の有無 -
.filter_map()
に与えるクロージャの返値の型の違い
依存クレートがあることや、 FallibleIterator
に変換するために1行挟む必要があり、しかもエラー型が不定形になることを踏まえると、 fallible-iterator
の使い勝手は悪そうな気がするが、3つ目を踏まえるとそうとも言えなくなると感じている。
どちらの方法でもクロージャの返値は Result
と Option
が絡んだ型になっているのだが、その包含順が2つの方法で逆転している。
標準ライブラリの方法 | Option<Result<T,E>> |
fallible-iterator の方法 |
Result<Option<T>,E> |
どちらでも、値を返す場合、 None
を返す場合、エラーを返す場合の3種類のケースがあることには変わりないが、 Result
が外側にある方が使い勝手が良い。
filter_map
を使っている以上、 Some
と None
が現れるのは自然であり、そこに新たに写像や取捨選択とは関係のないエラーのケースを付け加えるのだから、 Some
や None
と独立して扱われるべきだと考えられるからだ。
また、 None
やエラーは包含の外側だと ?
演算子で簡単に early return ができ、特に Result
型を返す関数をクロージャ中で呼び出している場合など、内側で発生したエラーを、その詳細に触れずに一気にクロージャの外側に持ってこれるため、シンプルな表記になりやすい。
...ということで、どちらの方法も一長一短であることが分かった。
標準ライブラリの方法で Result
と Option
を入れ替えられるか
以上から、標準の std::iter::Iterator
に対してクロージャが Result<Option<T>,E>
が返せるような filter_map
が存在すれば一番幸せになれると考えられる。
ということで、 filter_map
の代わりに Result<Option<T>,E>
型を返せるようにした filter_map_swapped
関数を Iterator
に生やすことにした。
以下にそのためのトレイトの実装例を示す。
use std::iter::Iterator;
// `Iterator` を拡張するトレイトの定義
pub trait FilterMapSwappedExt: Iterator {
// `T`: `filter_map_swapped` した後の要素の型
// `E`: 発生しうるエラーの型
// `F`: `Result<Option<T>,E>` を返すクロージャ型
fn filter_map_swapped<T,E,F>(self, f: F)
-> impl Iterator<Item = Result<T,E>>
where
Self: Sized,
F: FnMut(Self::Item) -> Result<Option<T>,E>;
}
// トレイトの実装
impl<I: Iterator> FilterMapSwappedExt for I {
fn filter_map_swapped<T,E,F>(self, mut f: F)
-> impl Iterator<Item = Result<T,E>>
where
Self: Sized,
F: FnMut(Self::Item) -> Result<Option<T>,E>
{
// 通常通り `filter_map` を呼び出し、この場限りのクロージャ型を渡している
// このクロージャに `filter_map_swapped` の引数のクロージャ `f` をムーブしている
self.filter_map(move |x| {
// `f` を呼び出して `Result<Option<T>,E>` を受け取り、 `Option<Result<T,E>>` に読み替えている
match f(x) {
Ok(Some(y)) => Some(Ok(y)),
Ok(None) => None,
Err(y) => Some(Err(y))
}
})
}
}
結局のところ通常の filter_map
を呼び出していることには変わりなく、最後に Result<Option<T>,E>
を Option<Result<T,E>>
に読み替える作業を追加しただけである。
大元の filter_map
関数の型定義になるべく合わせるようにしている。
filter_map
関数の返す型は、元のイテレータ I
と、引数で与えられるクロージャの型 F
を使って FilterMap<I,F>
と表される。
filter_map_swapped
でも FilterMap<I,F2>
を返しているのだが、この F2
(≠ F
) に相当する型は filter_map
と違って関数実装内部で定義されるクロージャの型であるから、関数の型定義で示すことはできない。
なので filter_map_swapped
の返値の型は FilterMap
などと具体的に示さずに、 impl Iterator<Item=Result<T,E>>
とトレイトによって示している。
オマケ: collect
もちょっと楽をする
.collect::<Result<Vec<_>,_>()
も書くのが億劫なので、ちょっとは楽をしたい。ということで FallibleIterator
の collect
と同様に型パラメータ C
を受け取って Result<C,E>
を返す関数を生やしてみる。
use std::iter::{ Iterator, FromIterator };
pub trait CollectExt<T,E>: Iterator<Item=Result<T,E>> {
fn collect_res<C>(self) -> Result<C,E>
where C: FromIterator<T>;
}
impl<I,T,E> CollectExt<T,E> for I
where I: Iterator<Item=Result<T,E>>
{
fn collect_res<C>(self) -> Result<C,E>
where C: FromIterator<T>
{ self.collect() }
}
ということで実装はできたものの、これにより減る労力はどれほどのものだろうか...