1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

エラーハンドリング付きの filter_map のアプローチ

Last updated at Posted at 2024-11-28

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 が存在する。
mapfilter もイテレータ要素か、その参照を引数にとるクロージャを与え、 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>;
}

つまり CFromIterator<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) ) を挟んで TResult<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つ目を踏まえるとそうとも言えなくなると感じている。

どちらの方法でもクロージャの返値は ResultOption が絡んだ型になっているのだが、その包含順が2つの方法で逆転している。

標準ライブラリの方法 Option<Result<T,E>>
fallible-iterator の方法 Result<Option<T>,E>

どちらでも、値を返す場合、 None を返す場合、エラーを返す場合の3種類のケースがあることには変わりないが、 Result が外側にある方が使い勝手が良い。
filter_map を使っている以上、 SomeNone が現れるのは自然であり、そこに新たに写像や取捨選択とは関係のないエラーのケースを付け加えるのだから、 SomeNone と独立して扱われるべきだと考えられるからだ。
また、 None やエラーは包含の外側だと ? 演算子で簡単に early return ができ、特に Result 型を返す関数をクロージャ中で呼び出している場合など、内側で発生したエラーを、その詳細に触れずに一気にクロージャの外側に持ってこれるため、シンプルな表記になりやすい。

...ということで、どちらの方法も一長一短であることが分かった。

標準ライブラリの方法で ResultOption を入れ替えられるか

以上から、標準の 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<_>,_>() も書くのが億劫なので、ちょっとは楽をしたい。ということで FallibleIteratorcollect と同様に型パラメータ 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() }
}

ということで実装はできたものの、これにより減る労力はどれほどのものだろうか...

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?