※本記事の後半(エラー型~注意点)は間違っている部分があるので「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_parse
をparse
に変えただけの
fn main() {
let mut parser = many1(digit()).and_then(|x: String| x.parse::<i32>());
dbg!(parser.parse("1"));
}
は通りません。
これはeasy_parse
がエラーとしてcombine::stream::easy::ParseError
を返すのに対し、parse
はcombine::error::ParseError
を返す、という違いによります。
前者は他のエラー型を格納できるように拡張されていますが、後者はパーサ自体のエラーしか格納できないためだと思われます。
はじめstd::num::ParseIntError
を何とかcombine::error::ParseError
に変換しようと思って結構時間を無駄にしました。
ドキュメントの説明上はparse
とeasy_parse
の違いはエラーメッセージを出しやすいかどうか、という感じに書かれていたのですが、実際にはこのような違いがあるようです。