10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rust combineでand_then()を使うとき

Last updated at Posted at 2019-03-26

※本記事の後半(エラー型~注意点)は間違っている部分があるので「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の違いはエラーメッセージを出しやすいかどうか、という感じに書かれていたのですが、実際にはこのような違いがあるようです。

10
4
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
10
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?