自然数の和と積を表現する構造体を考えよう。
#[derive(Debug, PartialEq)] enum Term { Sum(usize, usize), Prod(usize, usize), }
このような素朴な enum を用意する。
数式をパースして Term を得るようなパーザを作りたい。
combine というパーザコンビネータのライブラリがあるのでこれを使う。
自然数のパーザや演算子のパーザは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 するテクニックの綺麗な応用が見られて感激。
私からは以上です。