無駄と文化

実用的ブログ

combineではParserのタプルもまたParserになる

自然数の和と積を表現する構造体を考えよう。

#[derive(Debug, PartialEq)]
enum Term {
    Sum(usize, usize),
    Prod(usize, usize),
}

このような素朴な enum を用意する。
数式をパースして Term を得るようなパーザを作りたい。

combine というパーザコンビネータのライブラリがあるのでこれを使う。

docs.rs

自然数のパーザや演算子のパーザは1行で書ける。
自然数は '0' ~ '9' の文字の1回以上の連なりと定義できる。演算子は '+', '*' のいずれかだ。

use combine::parser::char::{Parser, one_of, parser::char::digit};

fn number<'a>() -> impl Parser<&'a str, Output = usize> {
    // 自然数は digit の1回以上の連なり
    // つづいて String としての "123" を usize としての 123 に変換する
    many1(digit()).map(|s: String| s.parse::<usize>().unwrap())
}

fn operator<'a>() -> impl Parser<&'a str, Output = char> {
    // 演算子は '+' か '*' のいずれか
    one_of("+*".chars())
}

これらを組み合わせて Term のパーザーを作ろう。

 

例えば文字列 "2*3" をパースするとしたら「number(), operator(), number() を立て続けに適用するパーザ」を作ればよさそうだ。
この「複数のパーザを立て続けに適用するパーザ」を表現したいときどう書けるか。

実はパーザをタプルで包むだけでいい。

fn term<'a>() -> impl Parser<&'a str, Output = (usize, char, usize)> {
    (number(), operator(), number())
}

たったこれだけ。 Parser<Outout=T1>, Parser<Outout=T2>, ..., Parser<Outout=Tn> のタプルは Parser<Output=(T1, T2, ..., Tn)> になる。

 

(number(), operator(), number()) 自体がパーザなので、いつものメソッドが使える。例えば .map() で結果を変換できる。

fn term<'a>() -> impl Parser<&'a str, Output = Term> {
    (number(), operator(), number())
        .map(|(lhs, op, rhs): (usize, char, usize)| match op {
            '+' => Term::Sum(lhs, rhs),
            '*' => Term::Prod(lhs, rhs),
            _ => unreachable!(),
        })
}

 

パーザが書けたので、実際にパースする処理を書くのは簡単。

fn parse_term(s: &str) -> Option<Term> {
    match term().parse(s) {
        Ok((term, _)) => Some(term),
        Err(_) => None,
    }
}

#[test]
fn test_parse_term() {
    assert_eq!(parse_term("12+34"), Some(Term::Sum(12, 34)));
    assert_eq!(parse_term("7*8"), Some(Term::Prod(7, 8)));
    assert_eq!(parse_term("7/8"), None);
}

いい感じ。

 

combine には .and() メソッドもある

「複数のパーザを立て続けに適用するパーザ」を表現するには他にも方法がある。

.and() メソッドを使えば、 p1.and(p2) のようにして p1, p2 を立て続けに適用するパーザを表現できる。

use combine::parser::char::char;

fn negative_number<'a>() -> impl Parser<&'a str, Output = isize> {
    char('-').and(number()).map(|(_, n)| -(n as isize))
}

p1: Parser<Output=T1>, p2: Parser<Output=T2> に対して p1.and(p2)Parser<Output=(T1, T2)> になる。
(p1, p2)p1.and(p2) は同じ振る舞いになる。

 

ところがパーザが3つ以上連なるときに .and() を使って合成しようとすると、結果は少しいびつなものになる。

fn term<'a>() -> impl Parser<&'a str, Output = ((usize, char), usize)> {
    number().and(operator()).and(number())
    // メソッドチェーンで綺麗に書ける
    // しかし結果は (usize, char, usize) ではなく ((usize, char), usize) になる
}

.and() は呼び出しごとに一組ずつタプルで包むので、結果は ((T1, T2), T3) になる。もし4つのパーザを合成しようとすれば、結果は (((T1, T2), T3), T4) になるはずだ。
タプルがネストしてしまうのは少々ツラい。

一方、パーザをタプルで書き並べる形なら、結果がフラットで読みやすい。

fn term<'a>() -> impl Parser<&'a str, Output = (usize, char, usize)> {
    (number(), operator(), number())
    // 結果は (usize, char, usize) になる
}

 

タプルに対してトレイトを impl する

タプルに対して何らかのトレイトを実装するのは Rust では一般的なことだ。
combine の実装をサラリと見ておこう。

combine/src/parser/sequence.rs

tuple_parser!(PartialState1; A);
tuple_parser!(PartialState2; A, B);
tuple_parser!(PartialState3; A, B, C);
// ...
tuple_parser!(PartialState20; A, B, C, D, E, F, G, H, I, J, K, L, M, N, P, Q, R, S, T, U);

1要素のタプルから20要素のタプルまで、定義がずらりと並んでいる。

 

タプルに対してトレイトを impl する (カスの応用)

私がお気に入りのトレイトである From トレイト を例にする。

#[derive(Debug)]
struct Number {
    value: i32,
}

impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number { value: item }
    }
}

fn main() {
    let num = Number::from(30);
    println!("My number is {:?}", num);
    // => My number is Number { value: 30 }
}

自作の構造体 Number に対して From<i32> トレイトを impl することで Number::from(42) が呼び出し可能になる。

From<i32> を impl するだけで、Into トレイトも使用可能になって、let num: Number = 42.into(); のように書ける。これも便利。

 

ところで From トレイトは1引数のメソッドしかサポートしていないのが不便だったりする。
そこでタプルに対して From トレイトを impl することで擬似的に多引数に対応できる。

impl From<(i32, i32)> for Number {
    fn from((m, n): (i32, i32)) -> Self {
        Number { value: m * n }
    }
}

fn main() {
    let num = Number::from((30, 40));
    println!("My number is {:?}", num);
    // => My number is Number { value: 1200 }
}

Number::from() がまるで2引数関数のように呼べるようになった。

 

まとめ

combine にて、タプルに対してトレイトを impl するテクニックの綺麗な応用が見られて感激。

 

 

私からは以上です。