Edited at

Rust combineでand_then()を使うとき

※本記事の後半(エラー型~注意点)は間違っている部分があるので「Rust combineでand_then()を使うとき(訂正版)」も併せてご覧ください。


はじめに

RustのパーサコンビネータライブラリcombineにてParser::and_then()を使おうとしたところ、型合わせが結構大変だったのでその結果を書いておきます。

combineのバージョンは3.8.1です。


やりたいこと

単にmany1(digit())で数値を取り出してしてparse::<i32>()i32に変換して出したい、というだけです。

ここで問題は「parse::<i32>std::num::ParseIntErrorを返す可能性がある」ということで、Parser::map()は使えず、結果をResultで返せるParser::and_then()を使う必要があります。

すなわち

fn main() {

let mut parser = many1(digit()).and_then(|x: String| x.parse::<i32>());
dbg!(parser.easy_parse("1"));
}

という感じです。これは実はドキュメントに載っているコードそのものなので当然通ります。


and_thenを使った再利用可能なパーサ

次に、このmany1...の部分を切り出して再利用可能にしたいと思います。

fn main() {

let mut parser = parser(num);
dbg!(parser.easy_parse("1"));
}

fn num ...
{
many1(digit()).and_then(|x: String| x.parse::<i32>())
}

こんな感じです。問題はこのfn numの型は何か、ということです。

Web上でよく出てくるcombineの使用例だと再利用可能なパーサはこんな感じで定義されています。

fn num<I>(input: &mut I) -> ParseResult<i32, I>

where
I: Stream<Item = char>,
I::Error: ParseError<I::Item, I::Range, I::Position>,
{
many1(digit()).and_then(|x: String| x.parse::<i32>()).parse_stream(input)
}

これはコンパイルが通らず、以下のようなエラーになります。

error[E0277]: the trait bound `<<I as combine::stream::StreamOnce>::Error as combine::error::ParseError<char, <I as combine::stream::StreamOnce>::Range, <I as combine::stream::StreamOnce>::Position>>::StreamError: std::convert::From<std::num::ParseIntError>` is not satisfied

--> src/main.rs:36:10
|
36 | .and_then(|x: String| x.parse::<i32>())
| ^^^^^^^^ the trait `std::convert::From<std::num::ParseIntError>` is not implemented for `<<I as combine::stream::StreamOnce>::Error as combine::error::ParseError<char, <I as combine::stream::StreamOnce>::Range, <I as combine::stream::StreamOnce>::Position>>::StreamError`
|
= help: consider adding a `where <<I as combine::stream::StreamOnce>::Error as combine::error::ParseError<char, <I as combine::stream::StreamOnce>::Range, <I as combine::stream::StreamOnce>::Position>>::StreamError: std::convert::From<std::num::ParseIntError>` bound
= note: required because of the requirements on the impl of `std::convert::Into<<<I as combine::stream::StreamOnce>::Error as combine::error::ParseError<char, <I as combine::stream::StreamOnce>::Range, <I as combine::stream::StreamOnce>::Position>>::StreamError>` for `std::num::ParseIntError`

error[E0277]: the trait bound `<<I as combine::stream::StreamOnce>::Error as combine::error::ParseError<char, <I as combine::stream::StreamOnce>::Range, <I as combine::stream::StreamOnce>::Position>>::StreamError: std::convert::From<std::num::ParseIntError>` is not satisfied
--> src/main.rs:37:10
|
37 | .parse_stream(input)
| ^^^^^^^^^^^^ the trait `std::convert::From<std::num::ParseIntError>` is not implemented for `<<I as combine::stream::StreamOnce>::Error as combine::error::ParseError<char, <I as combine::stream::StreamOnce>::Range, <I as combine::stream::StreamOnce>::Position>>::StreamError`
|
= help: consider adding a `where <<I as combine::stream::StreamOnce>::Error as combine::error::ParseError<char, <I as combine::stream::StreamOnce>::Range, <I as combine::stream::StreamOnce>::Position>>::StreamError: std::convert::From<std::num::ParseIntError>` bound
= note: required because of the requirements on the impl of `std::convert::Into<<<I as combine::stream::StreamOnce>::Error as combine::error::ParseError<char, <I as combine::stream::StreamOnce>::Range, <I as combine::stream::StreamOnce>::Position>>::StreamError>` for `std::num::ParseIntError`
= note: required because of the requirements on the impl of `combine::parser::Parser` for `combine::parser::combinator::AndThen<combine::parser::repeat::Many1<std::string::String, combine::parser::char::Digit<I>>, [closure@src/main.rs:36:19: 36:47]>`

combineをやっているとコンパイラのエラーメッセージはあてにならないことも多いのですが、今回は妥当なhelpを出してくれています。

このhelp通りにつけると

fn num<I>(input: &mut I) -> ParseResult<i32, I>

where
I: Stream<Item = char>,
I::Error: ParseError<I::Item, I::Range, I::Position>,
<<I as combine::stream::StreamOnce>::Error as combine::error::ParseError<
char,
<I as combine::stream::StreamOnce>::Range,
<I as combine::stream::StreamOnce>::Position,
>>::StreamError: std::convert::From<std::num::ParseIntError>,
<I as combine::stream::StreamOnce>::Error: combine::error::ParseError<
char,
<I as combine::stream::StreamOnce>::Range,
<I as combine::stream::StreamOnce>::Position,
>,
{
many1(digit())
.and_then(|x: String| x.parse::<i32>())
.parse_stream(input)
}

となって、これは通ります。さすがに長いので通るギリギリまで削ります。

fn num<I>(input: &mut I) -> ParseResult<i32, I>

where
I: Stream<Item = char>,
I::Error: ParseError<char, I::Range, I::Position>,
<I::Error as ParseError<char, I::Range, I::Position>>::StreamError:
From<ParseIntError>,
{
many1(digit())
.and_then(|x: String| x.parse::<i32>())
.parse_stream(input)
}

これくらいなら許容範囲かと思います。

また、num()が直接Parserを返す実装も可能です。その場合は

fn main() {

let mut parser = num();
dbg!(parser.parse("1"));
}

fn num<I>() -> impl Parser<Input = I, Output = i32>
where
I: Stream<Item = char>,
I::Error: ParseError<char, I::Range, I::Position>,
<I::Error as ParseError<char, I::Range, I::Position>>::StreamError:
From<ParseIntError>,
{
many1(digit()).and_then(|x: String| x.parse::<i32>())
}

となります。こちらの方が若干きれいかもしれません。


(追記)エラー型

エラー型について書き忘れたので追記します。

ここまでの例ではエラー型はParseIntError前提でしたが、実際にはいろいろなエラー型を受けたいですし、単にメッセージだけ出したいことも多いです。なのでもう少し汎用的なエラー型にしておく必要があります。

最終的なエラー型はcombine::easy::Errorになるので以下のような感じで良さそうです。

fn num<I>(input: &mut I) -> ParseResult<i32, I>

where
I: Stream<Item = char>,
I::Error: ParseError<char, I::Range, I::Position>,
<I::Error as ParseError<char, I::Range, I::Position>>::StreamError:
From<::combine::easy::Error<char, I::Range>>,
{
many1(digit())
.and_then(|x: String| x.parse::<i32>())
.parse_stream(input)
}

ただ、このままだとand_thenのところで型推論がうまくいかずアノテーションしてあげる必要があるようです。

また、ParseIntErrorはそのままは渡せなくなったのでinto()で変換してやります。

std::error::ErrorからのFromがあるので変換できます)

fn num<I>(input: &mut I) -> ParseResult<i32, I>

where
I: Stream<Item = char>,
I::Error: ParseError<char, I::Range, I::Position>,
<I::Error as ParseError<char, I::Range, I::Position>>::StreamError:
From<::combine::easy::Error<char, I::Range>>,
{
many1(digit())
.and_then::<_, _, ::combine::easy::Error<char, I::Range>, _>(|x: String|
x.parse::<i32>().map_err(|x| x.into())
.parse_stream(input)
}

もし何らかのメッセージを渡したい場合はこんな感じです。

fn num<I>(input: &mut I) -> ParseResult<i32, I>

where
I: Stream<Item = char>,
I::Error: ParseError<char, I::Range, I::Position>,
<I::Error as ParseError<char, I::Range, I::Position>>::StreamError:
From<::combine::easy::Error<char, I::Range>>,
{
many1(digit())
.and_then::<_, _, ::combine::easy::Error<char, I::Range>, _>(|x: String|
x.parse::<i32>().map_err(|_| easy::Error::Expected(easy::Info::Borrowed("error!!!")))
.parse_stream(input)
}


注意点

and_thenを使う場合は、(おそらく)easy_parseが必須です。

例えば最初の例のeasy_parseparseに変えただけの

fn main() {

let mut parser = many1(digit()).and_then(|x: String| x.parse::<i32>());
dbg!(parser.parse("1"));
}

は通りません。

これはeasy_parseがエラーとしてcombine::stream::easy::ParseErrorを返すのに対し、parsecombine::error::ParseErrorを返す、という違いによります。

前者は他のエラー型を格納できるように拡張されていますが、後者はパーサ自体のエラーしか格納できないためだと思われます。

はじめstd::num::ParseIntErrorを何とかcombine::error::ParseErrorに変換しようと思って結構時間を無駄にしました。

ドキュメントの説明上はparseeasy_parseの違いはエラーメッセージを出しやすいかどうか、という感じに書かれていたのですが、実際にはこのような違いがあるようです。