combine

make parser

combine

Crate combine

Module combine::parser::char

余り資料が見当らない。わざとテストがフェイルするように設定して例を実行。実行出来るまでに171箇もの木箱がコンパイルされた。効率悪いな。

[sakae@fb /tmp/combine]$ cargo test --example date
  :
---- test stdout ----
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `Ok((DateTime { date: Date { year: 2015, month: 9, day: 2 }, time: Time { hour: 18, minute: 54, second: 42, time_zone: 120 } }, ""))`,
 right: `Ok((DateTime { date: Date { year: 2015, month: 8, day: 2 }, time: Time { hour: 18, minute: 54, second: 42, time_zone: 120 } }, ""))`', examples/date.rs:181:5

nomなんかでも、そうなのだけど、exampleを実行すると、余計な木箱がわんさかコンパイルされる。クィックにやるなら、前回やったように一つだけ取出してきて実行した方が良い。それだと改変も思う存分出来るしね。

[sakae@fb /tmp/combine]$ cargo run --example date
  :
     Running `target/debug/examples/date`
2022-03-14T23:66:77                         // Press RETURN and C-d to eof
Parse error at line: 1, column: 20
Unexpected `
`
Expected `Z`, `-` or `+`
2021-15-14T25:81:99Z0900
OK

こういうのがパース出来てしまうのは、例としていかがなものかと。冒頭に詳しい亊は、 ISO 8601を参照って記述があった。date-timeは、不合理の権家だからなあ。

いっそ、date-timeを、浮動少数点にしちゃったらどうよ。基準日時をEPOCに習って1970年にする。日は1をインクリメント、1日内(23:59:59まで)を、少数点で表す。楽でいいぞ。EXCELLはこの方針でやってるね。

/// Parses a date
/// 2010-01-30
fn date<Input>() -> impl Parser<Input, Output = Date>
where
    Input: Stream<Token = char>,
    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
    (
        many::<String, _, _>(digit()),
        char('-'),
        two_digits(),
        char('-'),
        two_digits(),
    )

任意の桁数の表現が、プチ気になるぞ。こういう記述が求められるのか。すっきりしないな。

ci.sh

combineのTOPに、下記のような総合テスト用意されてた。bashで駆動ってのが食わないけど、そんなのを無視して、OpenBSDで実行してみる。

#!/bin/bash -x
set -ex

if [[ "$TRAVIS_RUST_VERSION" == "1.40.0" ]]; then
    cargo "$@" check
    cargo "$@" check --no-default-features
else
    cargo "$@" build
    cargo "$@" test --all-features
    cargo "$@" test --all-features --examples
     :
ob$ cargo "$@" test --all-features
   Compiling futures-sink v0.3.17
   Compiling tokio v1.12.0
   Compiling futures-channel v0.3.17
   Compiling futures-util v0.3.17
   Compiling tokio-util v0.6.8
   Compiling futures-executor v0.3.17
   Compiling futures v0.3.17
   Compiling combine v4.6.3 (/tmp/combine)
    Building [======================>  ] 174/182: combine, parser(test), as...
/tmp: write failed, file system is full
error: failed to write `/tmp/combine/target/debug/.fingerprint/combine-3a304743a712a81c/test-integration-test-buffered_stream`

オイラーが用意したRAMDISK(1G)を食い潰してしまった。これもそれ、実使用を想定してasyncな環境を作るため、わんさかと木箱を用意してるから。tokioだとかhttpね。

しょうがないのでdebianで実行

+ cargo test --all-features

running 47 tests
test error::tests_std::parse_clone_but_not_copy ... ok
test parser::byte::num::tests::no_rangestream ... ok
test parser::byte::tests::bytes_read_stream ... ok
 :
     Running tests/async.rs (target/debug/deps/async-1948641015d3d81d)

running 23 tests
test any_send_partial_state_do_not_forget_state ... ok
test choice_test ... ok
test decode_async_std ... ok
test decode_loop ... ok
 :
   Doc-tests combine

running 156 tests
test src/error.rs - error::Commit<T>::combine (line 291) ... ok
 :
test src/parser/char.rs - parser::char::alpha_num (line 181) ... ok
test src/parser/char.rs - parser::char::char (line 16) ... ok
test src/parser/char.rs - parser::char::crlf (line 110) ... ok
test src/parser/char.rs - parser::char::digit (line 32) ... ok
test src/parser/char.rs - parser::char::hex_digit (line 233) ... ok
 :
test src/parser/mod.rs - parser::EasyParser::easy_parse (line 974) ... ok
test src/parser/mod.rs - parser::Parser::and (line 414) ... ok
test src/parser/mod.rs - parser::Parser::and_then (line 740) ... ok
test src/parser/mod.rs - parser::Parser::boxed (line 836) ... ok

しっかりテストで安心出来ます。ちなみに、全テストを実施した時の残骸は1.5Gになってましたよ。

使いかたは、WEBからどうぞって亊だな。ci.shの最後にdocを作っているよ。

sakae@deb:/tmp/combine$ cargo doc --open
    Finished dev [unoptimized + debuginfo] target(s) in 0.12s
     Opening /tmp/combine/target/doc/combine/index.html

csv

昔結構技術書を買ってた。自分への投資。 Real World Haskee なんてのもそうだ。通読して終りにしちゃったな。今が再読する機会です。確かParsecも扱っていたな。

in haskell

ソースをお取り寄せ。 book-real-world-haskell (ch16)

一番簡単なCSVのパーサー。

import Text.ParserCombinators.Parsec

csvFile = endBy line eol
line = sepBy cell (char ',')
cell = many (noneOf ",\n")
eol = char '\n'

parseCSV :: String -> Either ParseError [[String]]
parseCSV input = parse csvFile "(unknown)" input

なんと、これだけで素朴なCSVファイルをパース出来るとな。声に出して読んでみると、ファイルは、ラインが寄り集まって、最後はEOLで終わるやつ。EOLってのは、\\n ね。世の中には、他のEOLもあるけど、取り敢えず無視。

ラインの構成は、セル(EXCELLで考えるんだ)をカンマでくわけしたもの。で、肝心のセルってのは、カンマとEOLじゃ無い文字の羅列。たったこれだけ。定義を書いただけって趣。

in rust

これをcombineの木箱を使って賭。

use combine::{
    many1,
    parser::char::{char, digit},
    sep_by, Parser,
};

fn type_of<T>(_: T) -> &'static str {
    std::any::type_name::<T>()
}

fn main() {
    let mut cell = many1(digit());
    println!("{}", type_of(&cell));
    let mut csv = sep_by(cell, char(',')).map(|words: Vec<String>| words);
    let result = csv.parse("221,130,64,59\n123,65,67,55\n");
    println!("{:?}", result.unwrap());
}

まだ、中途半端なやつだ。

&combine::parser::repeat::Many1<alloc::string::String, combine::parser::char::Digit<&str>>
(["221", "130", "64", "59"], "\n123,65,67,55\n")

地獄

haskellのお手本に習って、追加すればいいんだな。

fn main() {
    let cell = many1(digit());
    let mut line = sep_by(cell, char(','));
    let mut csv = sep_by(line, char('\n')).map(|words: Vec<String>| words);
    let result = csv.parse("221,130,64,59\n123,65,67,55\n");
    println!("{:?}", result.unwrap());
}

突然、地獄へ引っ張りこまれました。エラーのオンパレード。

error[E0283]: type annotations needed for `Many1<F, Digit<&str>>`
   --> src/main.rs:12:16
    |
12  |     let cell = many1(digit());
    |         ----   ^^^^^ cannot infer type for type parameter `F` declared on
the function `many1`
    |         |
    |         consider giving `cell` the explicit type `Many1<F, _>`, where the
type parameter `F` is specified
    |
    = note: cannot satisfy `_: Extend<char>`

推測できひんか。

note: required by a bound in `many1`
   --> /home/sakae/.cargo/registry/src/github.com-1285ae84e5963aae/combine-4.6.3
/src/parser/repeat.rs:532:8
    |
532 |     F: Extend<P::Output> + Default,
    |        ^^^^^^^^^^^^^^^^^ required by this bound in `many1`
help: consider specifying the type arguments in the function call
    |
12  |     let cell = many1::<F, Input, P>(digit());
    |                     +++++++++++++++

もう、お手上げです。

見様見まね

自分で考えていても解らん。benches/ なんて所にjson.rsがいた。ベンチマーク用? 藁にもすがる気持で、コードを眺めましたよ。

で、分った亊は、自分で関数を定義する必要があるって亊。その関数ってのは、新しい型を返す関数。

下記の例だと、cellは1つ以上の数値の集まり。それが見付かったら、整数に変換して出力するよ。そういうParserだからね。って言う関数だ。

use combine::{
    many1,
    parser::char::{char, digit},
    sep_by, ParseError, Parser, Stream,
};

fn cell<Input>() -> impl Parser<Input, Output = i32>
where
    Input: Stream<Token = char>,
    Input::Error: ParseError<Input::Token, Input::Range, Input::Position>,
{
    many1(digit())
        .map(|s: String| {
            let mut n = 0;
            for c in s.chars() {
                n = n * 10 + (c as i32 - '0' as i32);
            }
            n
        })
        .expected("integer")
}

fn main() {
    let line = sep_by(cell(), char(',')).map(|words: Vec<i32>| words);
    let mut csv = sep_by(line, char('\n')).map(|words: Vec<Vec<i32>>| words);
    let result = csv.parse("221,130,64,59\n305,135,67,55\n");
    println!("{:?}", result.unwrap());
}

して、結果は

([[221, 130, 64, 59], [305, 135, 67, 55], []], "")

何とか動き出したな。でも不満がある。

不満解消

不満解消に取り掛かる。以下は変更部分。

fn main() {
    let mut csv = sep_by(
        (cell(), char(','), cell(), char(','), cell(), char(','), cell())
            .map(|(ymdh, _, hi, _, lo, _, _)| [ymdh, hi, lo]),
        char('\n')
    );

    let input = "22031005,135,72,56
22031021,116,60,50
22031105,129,71,61
22031121,122,61,57";
    let result: Result<(Vec<[i32; 3]>, &str), easy::ParseError<&str>>
        = csv.easy_parse(input);
    println!("{:?}", result.unwrap().0);
}

csvのパーサーに、データに即した構造を持たせた。データは毎度お馴染の血圧データである。

今回はそのデータから脈拍を除くものを抽出する。cellはカンマで区切られたデータが4つ並んでいるよ。4つのデータの区切はリターン記号だよって定義。

cellの並びは4つを組にして(タプルで表現)、得られた結果をmapを使って配列に組み立てる。 だからmapの引数もタプルで表現。脈拍データはタプルの最終項に現れるけど、それは無視。勿論、',' もデータとして得られるけど、アンダーバーにして、無視してる。 ymdh,hi,loだけで、配列を組み立てている。

csvをパースする。その時に却ってくる構造をResult型で注釈してる。その型は、データ部とエラーの組。更にデータ部は、評価が終了したVec<[i32: 3]>と、未評価なインプットデータの組って表現だ。

最終のデータ表示は、resultをunwrap()して、データ部のタプルを取出し、更にその.0で、タプルの最左部分を使ってる。

[[22031005, 135, 72], [22031021, 116, 60], [22031105, 129, 71], [22031121, 122, 61]]

どうしろと?

上記では、データが決めうち。これじゃ実用にならん。ファイルから読み込んだデータを使いたい。

let input =  std::fs::read_to_string("current.csv").unwrap();

ファイルから全部読みしたのを使いたい。が、エラーだ。

error[E0597]: `input` does not live long enough
  --> src/main.rs:30:26
   |
30 |         = csv.easy_parse(&input);
   |                          ^^^^^^ borrowed value does not live long enough
31 |     println!("{:?}", result.unwrap().0);
32 | }
   | -
   | |
   | `input` dropped here while still borrowed

これに対する説明として、rustc –explain E0597 こんな事例で解説されてた。

struct Foo<'a> {
    x: Option<&'a u32>,
}

let mut x = Foo { x: None };
{
    let y = 0;
    x.x = Some(&y); // error: `y` does not live long enough
}
println!("{:?}", x.x);

下のやつが改訂版。

struct Foo<'a> {
    x: Option<&'a u32>,
}

let mut x = Foo { x: None };

let y = 0;
x.x = Some(&y);

println!("{:?}", x.x);

目をこらして違いを見付ないといかん。悪いやつは、ブロックの中でyを宣言して、それを使って代入してる。ブロックの外で使おうとすると、rustの規則によりyは藻屑となってるからエラー。

だから、ブロックを削除して、同一階層で使いましょうとな。こんなに分かりやすい例ならいいんだけど。。

何かcombineの深い所で、これに反する亊が起きているんだろうね。深入りは止めておこう。

どうしろと(2) ?

今迄cellって関数を使ってたけど、docs.rsに手軽な数値化を行うパーサーが出てた。すっきりっぽい。

let cell = spaces()
    .with(many1(digit())
          .map(|string: String| string.parse::<i32>().unwrap()));

一般的な文字列から数値に変換する方法だ。これを使ってやれ。

error[E0382]: use of moved value: `cell`
  --> src/main.rs:12:27
   |
8  |     let cell = spaces()
   |         ---- move occurs because `cell` has type `With<impl Parser<combine\
::easy::Stream<&str>>, combine::parser::combinator::Map<Many1<String, Digit<com\
bine::easy::Stream<&str>>>, [closure@src/main.rs:10:20: 10:67]>>`, which does n\
ot implement the `Copy` trait
...
12 |         (cell, char(','), cell, char(','), cell, char(','), cell)
   |          ----             ^^^^ value used here after move
   |          |
   |          value moved here

至る所が地雷源である。オイラーに取っては、combineって、取扱注意木箱ですな。 nom = alt(combine) になるかな。

etc