Parboiledをさらに高速に動作させる方法は? どのような間違いを避けるのが最善ですか? Parboiled1の形式の継承をどうしますか? シリーズの最終記事では、これらの質問やその他の質問に答えるよう求められています。
サイクル構造:
- パート1。なぜパーボイルド?
- パート2.一致するテキスト
- パート3.データ抽出
- パート4.厳しい現実
性能
Parboiled2は高速ですが、場合によってはさらに高速に実行されることもあります。 このセクションでは、利用可能なマイクロ最適化について説明します。 最適化を実行する際の主なものは、適時性です。 しかし、表現力を失うことなく、もう少し最適なコードをすぐに作成できる場合は、この機会を必ず使用する必要があります。
小さいn <= 4のn.times
をn.times
します
小さいnに対して、演算子n回を繰り返す代わりに、いくつかの繰り返しルールを単純に連鎖さ
n.times
パフォーマンスを向上させることができます。 展開するのが理にかなっている繰り返しの数-状況によって異なりますが、この数は4を超えることはほとんどありません。
// rule { 4 times Digit } // rule { Digit ~ Digit ~ Digit ~ Digit }
この最適化の関連性は、Matthias自身によって発表されましたが、仮に、
n.times
演算子はそれを自分で実行できた可能性があります。
n.times
スタック操作のn.times
同様の手法を使用すると、値スタックからデータを抽出するときにパフォーマンスを少し絞ることができます。 たとえば、前のルールに適用できます:
def Digit4 = rule { Digit ~ Digit ~ Digit ~ Digit ~ push( #(charAt(-4))*1000 + #(charAt(-3))*100 + #(charAt(-2))*10 + #(lastChar) ) }
CharPredicate
再作成しないでください
CharPredicate
クラスの新しい機能を楽しむのはまったく普通のことですが、
rule
ブロック内に
CharPredicate
型の独自のインスタンスを作成しないでください。ルールが実行されるたびに述語が再作成され、パーサーのパフォーマンスが劇的に低下します。 したがって、毎回シンボリック述語を作成する代わりに、パーサー内でそれらを定数として定義します。
class MyParser(val input: ParserInput) extends Parser { val Uppercase = CharPredicate.from(_.isUpper) ... }
または、さらに良いことに、この宣言をパーサーのコンパニオンオブジェクトに送信します。
class MyParser(val input: ParserInput) extends Parser { ... } object MyParser { val Uppercase = CharPredicate.from(_.isUpper) }
セマンティック述語を使用する
これらのルールの特徴は、値スタックと相互作用しないことです。 詳細については、ドキュメントに記載されていますが、これらについて知っておくべき最も重要なことは次のとおりです。
セマンティック述語を使用する場合、パーサーは進行しません。つまり、カーソルを次の文字に移動しません。 したがって、それらが無意識に使用されると、パーサーはループする可能性があります。
大文字の文字述語宣言を覚えていますか? セマンティック述語
test
を使用して同じことを行うことができます。
def JavaUpperCase = rule { oneOrMore(test(currentChar.isUpper) ~ ANY) }
CharPredicate.All
を表示するCharPredicate.All
場所を使用します
悲しいかな、
CharPredicate.All
は大きな文字範囲に対しては遅く、
CharPredicate.All
も高速です。 この知識を活用してください。
逆述語を使用する
パーサーが改行の前にすべての文字をキャプチャすることを想像してください(明確にするために、Unixスタイルで)。 もちろん、これは
noneOf
で実行できますが、逆述語はより高速になります。
def foo = rule { capture(zeroOrMore(noneOf("\n"))) } // ? def foo = rule { capture(zeroOrMore(!'\n')) }
残念ながら、この見栄えの良い例はループします。パーサーは進行しないからです。 これを修正するには、パーサーカーソルを移動するが、スタックは変更しないルールが必要です。 たとえば、これは次のとおりです。
def foo = rule { capture(zeroOrMore( !'\n' ~ ANY )) }
foo
ルールは、
EOI
と改行を除くすべてを完全に吸収します。
バグレポート
無効な入力に対して意味のないメッセージを生成するパーサーを使用したいとは思わないでしょう。 Parboiled2を使用すると、エラーについて非常に明確に伝えることができます。
書式設定
したがって、何かが
ParseError
になった場合、パーサーは、タイプ
ParseError
オブジェクトを自由に渡します。このオブジェクトは、
formatError
メソッドを使用して読み取り可能な形式にできます。
val errorMessage = parser formatError error
何らかの理由でデフォルトの書式設定があなたに合わない場合は、明示的にパーサーに希望を渡す必要があります。
val errorMessage parser.formatError(error, new ErrorFormatter(showTraces = true))
ErrorFormatter
を記述したい場合、
ParseError
クラスの構造を
ParseError
で処理する
ParseError
があり
ParseError
。これは、この方法でParboiledの下部で宣言されます。
case class ParseError(position: Position, charCount: Int, traces: Seq[RuleTrace]) extends RuntimeException
また、エラーメッセージをユーザーに配信するためのいくつかのスキームの存在に注目する価値があります。リクエストでは、
ParseError
は、
Try
オブジェクトとしてだけでなく、たとえば、ポリモーフィック型または
ParseError
として表すことができます。 詳細はこちらをご覧ください 。
def Foo = rule { "foo" | fail(" !") }
微調整
組み込みのエラー報告メカニズムを回避するオプションがあります。 これを行うには、エラーの場合に表示するメッセージで
fail
ルールを使用します。
def Goldfinger = rule { "talk" | fail("to die") }
その後、機会が生じると、おおよそ次の形式でエラーメッセージが返されます。
Invalid input 'Bond', expected to die. (line 1, column 1):
名前付きルール
このタイプのルールの使用は、エラーをキャッチするためだけでなく、非常に便利です。 このメカニズムについては、「ベストプラクティス」セクションで詳しく説明しています。
アトミック
Parboiled2は、PEGベースのパーサーを生成します。 これは、パーサーが文字列ではなく文字列で動作することを意味します(多くの人が考えるように)。したがって、文字レベルでエラーも表示されます。 同意します-「ここにXがあり、YまたはZが予想されました」などのメッセージは、「ここにXXがあり、XYまたはXZが表示されると予想されました」よりも精神的な努力が必要です。 エラーレポートの行全体を表示するには、
atomi
マーカーがあり、ルールをその中にラップします。
def AtomicRuleTest = rule { atomic("foo") | atomic("fob") | atomic("bar") }
入り口で
foxes
ために
Invalid input "fox", expected "foo", "fob" or "bar" (line 1, column 1): foxes ^
静かな
選択肢が多すぎる場合、考えられるすべての選択肢をユーザーに常に通知する必要はありません。 たとえば、特定の場所では、パーサーは特定のルールに関連して多くの空白を予期します。 レポートの冗長性を排除するために、スペースについて黙っておくことができます。
quiet
トークンの使用は非常に簡単です。
def OptionalWhitespaces = rule { quiet(zeroOrMore(anyOf(" \t\n"))) }
正直なところ、私はこのルールの使用を奨励する状況に遭遇したことはありません。
atomic
ように、ドキュメントで詳細に説明されています 。
エラー回復
Parboiled1が勝つほとんど唯一のエピソード、Parboiled2はあまりうまくいっていません:パーサーは、最初に遭遇したエラーの目からのみドロップします。 ほとんどのシナリオでは、これは素晴らしいです。たとえば、ログの解析、テキストプロトコル、構成ファイル(多くの場合)に干渉しませんが、DSLやIDEのようなツールの開発者はこの状況を好まないでしょう。 Matiasはこれを修正することを約束しているため、今日この機能が本当に必要な場合は、バグトラッカーに書き込んでください。開発プロセスがスピードアップする可能性があります。
Parboiled1には、あらゆる場面に対応する膨大な数のParserRunnerがあります。 エラーが発生した場合に解析を続行する必要がある場合は、
RecoveringParserRunner
てください。
テスト中
複雑な開発者は、テストにspecs2フレームワークを使用し、ヘルパークラスTestParserSpecで補足します。 scalatestを使用する人にとっては不便に思えますが、その基本的な考え方は採用できます。 マティアスからの秘密で、彼の決定は、可変状態に依存しているため、特に正確ではありません。 おそらく将来的には、テスト用の本格的なフレームワークに似たものを期待するでしょう。
ルールは、個別にテストすることも一緒にテストすることもできます。 個人的に、私は各ルールではなくテストを書くことを好みますが、「特別な」ケースでのみメインルールをチェックします:
多くの形式、標準化されたものでさえ、非常に興味深い点を見つけることができます。 たとえば、BSDのようなRFC 3164メッセージ形式では、数字自体に1ビットがある場合でも、 常に 2つの位置が月の日付に割り当てられます。 RFC自体からの例を次に示します。
月の日が10未満の場合は、スペースと数字で表す必要があります。 たとえば、8月7日は"Aug 7"
として表され、"g"
と"7"
間に2つのスペースがあります。
この種の「興味深い点」に加えて、パーサー文字列に開き括弧、無効な文字を入力し、値スタックで操作の順序を確認できます。
テストでは、すぐに遭遇する別の微妙な点があります。 次のルールをテストするとします。
def Decimal: Rule0 = rule { ("+" | "-").? ~ Digit.+ ~ "." ~ Digit.+ }
これを行うには、明らかに間違った入力をパーサーに送信し、出力でエラーを待ちます。
// . val p = new MyParser("12.3.456").Decimal.run() // Success(()) p.isFailure shouldBe true //
しかし、テストを実行すると、パーサーが成功した結果を返したことがわかります。 なぜそう ルールには
EOI
はありませんが、
EOI
を追加すると、
Decimal
を使用するすべてのルールが台無しになります。 したがって、たとえば、unningなメタルールメカニズムを使用して、特別なテストルールを作成する必要があります。 前の例の最後にEOIを追加し、パーサーがエラーでクラッシュすることを確認しましょう。
Failure(ParseError(Position(5,1,6), Position(5,1,6), <2 traces>))
パーボイルドの欠点
パーボイルド2
人々に欠陥があるなら、なぜライブラリーに欠陥がないのですか? ここでは、Parboiled2も例外ではありません。
- C ++の最高の伝統における、エラーに関する長い、あまりにも一般的で完全に理解できないコンパイラメッセージ。 下の図に鮮明な例を示します(
~
演算子は誤ってルールで省略されました)。 その理由は、将来のバージョンで削除される見込みのあるタイプで高度なチェックを実行することに関連しています。

コンパイラは汚いことを誓います
- この問題はParboiled2ではなく、scalacに適用されます。 スタックから値を取得するラムダに、明示的に(未)定義された引数の型がある場合、コンパイラを破壊できます。
// def MyRule = rule { oneOrMore(Visible) ~> {s => "[" + s + "]"} } // def MyRule = rule { oneOrMore(Visible) ~> {s: String => "[" + s + "]"} }
動作するものと動作しないものは、コンパイラのバージョンに依存します。
- 多くのIDEはまだマクロ表現をサポートする方法を学んでおらず、Parboiled2は支援なしで構築されました。 したがって、開発環境の下線を信じてはなりません。 一度、それを忘れてしまったため、私は一日中、文字通り突然存在しないエラーを探して過ごしました。
- 失敗した解析のための回復メカニズムの欠如。 ドメイン固有の言語、またはParboiled2をコンパイラのフロントエンドとして使用したい言語を設計する場合、これは非常に失望します。 しかし、彼らはそれに取り組んでいます。 この機会を見たい場合-書いて、これは開発をスピードアップします。
- 小さなIDEやテキストエディターの開発者の多くは、現在提供されているエラーメッセージよりも柔軟なエラーメッセージを見たいと思っています。 現時点では、それらに影響を与える方法は2つしかありません。
- 名前付き規則
- 名前付きのネストされたルール。
ゆでた1
ほとんどのプロジェクトはまだParboiled1で記述されており、劇的に(企業内で)劇的に変化する可能性は低いため、Parboiled1には多くの欠点があります。 非常に限られたDSLに加えて、Parboiledには「Rule8」の問題があり、ログのパーサーの作成が複雑になります。 Parboiled1は、N個の要素を持つ各ルールに対して、Skalovタプルとの類推によるクラスが存在するように構築されます:
Rule0
、
Rule1
、
Rule7
。 これは、Javaなどの複雑なプログラミング言語を解析するのに十分であり、実際にツリー構造を解析するときに大きな問題を引き起こすことはありません。 ただし、ログファイルメッセージなどの線形構造からデータを抽出する必要がある場合、この制限は非常に簡単に実行できます。 これは、単一の結果ルールの代わりにタプルを使用することで解決されます。 以下に例を示します。
def Event: Rule1[LogEvent] = rule { Header ~ " " ~ UserData ~ " " ~ Message ~~> { (header, data, message) => SyslogEvent ( header._1, header._2, header._3, header._4, header._5, data._1, data._2, message ) } }
惨めに見えるが、問題は解決しました。
ベストプラクティス
このセクションでは、Parboiled2に固有のニュアンスだけでなく、パーサーコンビネーターで機能する一般的な真実について説明します。
シャルルティルス
ドキュメントに記載されていない便利なオブジェクトが1つあります: CharUtilsです。 これには、たとえば、文字の大文字小文字の変更、エスケープ、対応する文字(文字列)への整数値の変換など、生活を楽にする静的メソッドが多数含まれています。 など。これを使用すると、時間を節約できます。
単体テストを書く
1つの小さな、失敗した変更は、文法を破り、急性直腸痛を引き起こす可能性があります。 これは多くの人が無視しているありふれたアドバイスです。 パーサーは、たとえばIOほどテストするのは難しくありません。このルーチンにはMockオブジェクトやその他のトリックは必要ありませんが、非常に価値のある作業です。 パーサーのインフラストラクチャ全体がありました。 そして私を信じてください。エラーを探すときに最初にしたことは、エラーがなければ、座ってテストを書くことでした。
パーサーとルールを小さくする
パーサーをサブパーサーに分離します。 各コンポーネントは、非常に具体的なことを行う必要があります。 たとえば、Timestampフィールドが定義されているLogEventを解析する場合(特にこのTimestampが一部のRfcに対応する場合)、レイジーにならずに個別に取り出します。
- 第一に、それはあなたのメインのプレイヤーのコードを減らし、それをより見やすくします。
- 第二に、テストが非常に容易になります。 サブパーサーを単体テストでカバーします。 その後、メインパーサーの開発を開始します
さまざまなアプローチがあります。
- パーサーを特性に分割し、自己入力型の参照を使用します(この方法が好ましいです)。
- パーサーを個別のエンティティとして宣言し、構成を使用します。
- 組み込みのメカニズムを使用して、サブパーサーを作成します。
ルールは可能な限りコンパクトにする必要がありますが、コンパクトにすることはできません。 ルールが小さいほど、文法の間違いを見つけやすくなります。 開発者がルールを長くし、同時に
capture
再利用
capture
場合、開発者のロジックを理解することは非常に困難です。 暗黙のキャプチャは状況を悪化させる可能性があります。 ルールのタイプを指定することもサポートに役立ちます。
値スタックの文字列の代わりにケースオブジェクトを送信する
このアドバイスは、パーサーの動作を高速化するため、最適化に起因する可能性があります。 文字列ではなく、意味のあるオブジェクトをValueスタックに送信します。 これにより、パーサーが高速になり、コードがより明確になります。
悪い:
def logLevel = rule { capture("info" | "warning" | "error") ~ ':' }
良い:
def logLevel = rule { “info:” ~ push(LogLevel.Info) | “warning" ~ push(LogLevel.Warning) | “error" ~ push(LogLevel.Error) }
簡素化された構文を使用してオブジェクトを組み立てます
この美しい方法は、Parboiled1に登場しました。 魔法ではなく、ケースクラスコンストラクターが暗黙的に呼び出されます。 主なことは、値スタックに配置された引数の数とタイプが、ケースクラスコンストラクターのシグネチャと一致することです。
悪い:
def charsAST: Rule1[AST] = rule { capture(Characters) ~> ((s: String) => AText(s)) }
良い:
def charsAST = rule { capture(Characters) ~> AText }
名前付きルール
名前付きルールは、不明瞭なルール名の代わりにエイリアスを使用できるようにするため、エラーレポートを受信する際の生活を大幅に簡素化します。 または、特定のタグ(「この式」または「スタックを変更する」)でルールをマークします。 いずれにせよ、この機能について知っておくと役立ちます。
多くのParboiled1ユーザーはすでにこの機能を気に入っています。 たとえば、Pyboiledを使用してCypherを解析するNeo4J開発者。
Parboiled1での表示:
def Header: Rule1[Header] = rule("I am header") { ... }
Parboiled2の場合:
def Header: Rule1[Header] = namedRule("header is here") { ... }
ネストされたルールに名前を付けることもできます。
def UserName = rule { Prefix ~ oneOrMore(NameChar).named("username") ~ PostFix }
移行
ほとんどの場合、移行は単純なプロセスですが、多くの時間がかかります。 したがって、少なくともあなたの人生の貴重な時間を節約し、主な落とし穴を説明しようと思います。
クラスパス
最初のバージョンとの競合を避けるために、Parboiled2はクラスパス
org.parboiled2
使用し
org.parboiled2
(一方、
org.parboiled2
の最初のバージョンのクラスパス)。 ただし、
org.parboiled
は古いままです:
org.parboiled
。 このため、1つのプロジェクトに両方の依存関係を持たせ、新しいバージョンへの段階的な移行を実行できます。 ちなみに、これはいくつかの自律型パーサーで非常にうまく機能します。 パーサーがさまざまな場所で再利用される多数のモジュールで構成されている場合(私の場合のように)-すぐにすべてのモジュールに対して移行を行う必要があります。
テスト検証
単体テストの可用性とパフォーマンスを確認します。 それらはありますか? いや? それらを書きます。 移行プロセス中に、新しいDSLがより強力になり、不注意な変更が文法を壊したため、いくつかの文法を改良する必要がありました。 落下テストは多くの時間を節約しました。 移行時に、文法全体を破るなどの深刻な問題は発生しませんでした。 これが彼に起こった場合、誰かが彼らの経験を共有するかもしれません。
パーサー周辺のコード
これで、パーサーは毎回再作成されますが、これは必ずしも便利ではありません。 PB1では、パーサーを一度作成してから繰り返し使用することが本当に好きでした。 これで、この番号は機能しなくなります。 したがって、パーサーのコンストラクターを変更し、それを使用してコードを少し書き換える必要があります。これによりパフォーマンスが低下することを恐れないでください。
警告 Parboiled1を使用すると、実行時にルールを生成できます。 したがって、同様のパーサーを使用している場合は、ほとんどの場合、それを書き直す必要があります。Parboiled2は、ダイナミクスを非常に難しくするマクロ式を使用するため、パフォーマンスが向上します。
構成
パーサー要素の構成へのアプローチは変更されていません。これは移民にとって朗報です。 ただし、
Parser
はもはや特性ではなく、抽象クラスです。 トレイトは、ソフトウェアコンポーネントを構成する最も便利な手段であり、PB1では、これにより
Parser
を任意のモジュールに混合し、モジュールを一緒に混合することができました。 抽象クラスを支持する変更はこの機能には影響しませんでしたが、 自己入力型の参照を使用する必要があります 。
trait Numbers { this: Parser => // }
言語のこの機能を使用せず、毎回
Parser
特性を混合した人は、好みを変更する必要があります。
別の方法として、特性から完全なパーサーを作成し、それらから必要なルールを(メソッドとして)メインパーサーにインポートできます。 確かに、私はそれらがより視覚的であると思うので、私はまだ特性コンポジションを使用することを好みます。
プリミティブを取り除く
移行プロセス中に、プリミティブルールの個人ライブラリの監査を必ず手配してください
CharPredicate
ものをすべて削除して
CharPredicate
。 ライブラリの重量は減りますが、まったく消えることはありません。 多くの人が、さまざまな日付形式、電子メールを説明する文法、パーボイルドのHTTPヘッダーのサポートを追加したいと考えています。 Parboiledは、単にパーサーコンビネーターです。 ただし、古いコードを捨てることは非常に素晴らしいことに同意します。
おわりに
この一連の記事では、scala言語に存在する最も進歩的で有望な解析ツールについて説明しようとしました。短いチュートリアルを作成し、実際に直面しなければならない問題について話しました。この記事が最悪の場合に役立つことを望み、せいぜい行動のガイドになることを望みます。
使用されたソース
謝辞
この記事の理由と便利なツールを提供してくれたAlexanderとMatthiasに感謝します。ヤナ、私の多くの間違いの校正と編集に感謝します。私はもっと有能に書くことを約束します。firegurafikuとToo Tabooに、Vertskの最初の記事、校正、多数の修正、および後続の例のアイデアの支援に感謝します。シリーズの最後の記事の校正と訂正をしてくれたVlad Ledovskyに感謝します。ありがとうございnehaevを作品に巨大な彫像を(私はそれを行うにはしたくなかった)分割するアイデアのコードエラーで見つかった記事のために、とイゴールKustov。発見された不正確さとアーセニー・アレサンドロヴィッチのプライムトークに感謝します。
役に立つ提案。記事のサイクルに従って最後に到達したすべての人々に感謝します。仕事が無駄に終わっていないことを願っています。