Dのメタ正規表現

私はハブを駆け抜けましたが、ハブ「D」と「異常なプログラミング」に同時に書かれたものは見つかりませんでした。 Dを書くのは普通の人だけであるという、またはさらに悪いことに、Dを知ることでプログラマは普通の人になるという完全に間違った考えがあります。 急いで反論します。







厳密に言えば、私自身はDプログラマーではありません-単一の産業プロジェクトはありませんが、おいしいハイライトを選択する他の人のコードを掘り下げることを時々楽しんでいます。 また、Dは文字列を操作するための非常に強力なツールセットを提供するため、通常はスクリプト言語で実行されるテキストデータを処理するための小さなユーティリティを自分用に作成します。

さて、ワープロがある場所には、正規表現があります。 そして、ここでもDは、使いやすさと使いやすさの点で、高さであり、その正規表現ライブラリはPerlに近づいています。 しかし、Perlでは、正規表現は構文の一部であり、言語自体はその大部分を中心に構築されていると言えます。Dでは、Dmitry Olshanskyによって作成された標準ライブラリからのstd.regexである。 もう1つの優れた点は、式パーサーをコンパイル時に構築できることです(もちろん、式自体がリテラルで指定されている場合)。もちろん、それがどのように配置されているかはわかりません。

そしてここに、私の詳細を理解する 帽子が飛んだ 思考が生じましたが、ある正規表現を別の正規表現から呼び出すことは可能ですか? リテラルを挿入しないでください(たとえば、Perlで簡単に実行できます)が、1つの式のコンパイル済みコードを別の式の内部から直接呼び出します。 私の意見では、それで遊ぶのはかなり愚かなアイデアです。

それで、私たちは何が欲しいのでしょうか? これについて(今のところ、これは擬似コードです):







INT=regexp("\d+"); LIST=regexp("INT(,INT)*");
      
      





この投稿は、Dに精通している人だけでなく、異常なプログラマーの幅広い読者向けに設計されているため、まず、使用する言語構成体について説明します。







ネタバレ

私自身は、1年生がレッスンのためにクアトレインを学んだように、目の前にチートシートを置くことを好みます。







一般に、Dは基本的なプログラミングとメタプログラミングの技術の最も幅広い選択を提供しますが、ここでは必要最小限のセットに制限しようとしますが、それぞれを詳細に処理しようとします。 興味のある人には、言語の作者が書いた素晴らしいテンプレートの再訪記事を読むことを強くお勧めします。これは一般的なテンプレートに捧げられていますが、例として、私は基礎として取ったコンパイル時の正規表現パーサーを示しています。 はい、もちろん、実験用に小さなパーサーを作成しました。完全ではなく、完全に正確ではありませんが、理想的にはモルモットに適しています。

だから:







コンパイル時間の計算。



これにより、すべてが簡単になり、大まかに言って、コンパイル時に計算できる式はすべて計算されます。 2番目の基本構造であるstatic if(...)は、コンパイル時にチェックされる(またはまったくコンパイルされない)ため、メタプログラミングが大幅に簡素化されます。







パターン



Dでは、関数とクラスの両方のテンプレートは、追加のパラメーターセットを使用して宣言されます。







 int f(T)(T x int y); struct(T,U)  { T x; U y; }
      
      





と呼ばれます







  f!(int)(1, 2); A!(int,string) a;
      
      





テンプレートパラメータは、フローティングリテラルや文字列リテラル、 任意の文字など、ほぼすべてのものにすることができます。 ただし、 テンプレートキーワードも保存され、広く使用されるパラメーター化された名前空間が作成されます。







 template X(T) { const T x=0; }
      
      





ミックスイン



このコンストラクトは、コードにリテラルを直接挿入しますmixin("static int x=0;");



また便利になります。







文字列操作



数百万個ありますが、実際にスライスするだけで"ABCDEFGH"[2..4] == "CDE";



です。 "ABCDEFGH"[2..4] == "CDE";



上記の例もコンパイル時に計算されることに注意してください。







別名



これは単なる同義語を意味しますが、特にテンプレートパラメータ(言及されたすべての記号と同じ)の幅広いアプリケーションで使用されます







有名な名字



最初は初心者が彼からそれを吹き飛ばすので、私はそれを意識的に使用しません 。 結局のところ、それは構文糖に過ぎず、あなたはいつでもそれなしで行うことができます。







それが実際にはすべてのツールキットです。パーサーに行きます。



上面図:







 1. template regex(string re) 2. { 3. static if(re.length) { 4. // parse escaped sequences 5. static if(re[0] == '\\') { 6. alias atom=compile_escape!re; 7. 8. // parse character class 9. } else static if(re[0] == '[') { 10. alias atom=compile_class!re; 11. 12. // parse atom 13. } else static if(re[0] == '(') { 14. alias atom=compile_atom!re; 15. 16. // parse dot 17. } else static if(re[0] == '.') { 18. alias atom=compile_anychar!re; 19. 20. // parse regular literals 21 } else { 22. alias atom=compile_char!re; 23. } 24. 25. // parse predicate (if any) *+? 26. alias pred=compile_pred!(atom, re[atom.skip..$]); 27. alias result=both_of!(pred, regex!(re[pred.skip..$])); 28. 29. // public result 30. static const size_t skip=result.skip; 31. alias match=result.match; 32. 33. } else { 34. static const size_t skip=0; 35. // end of regular expression 36. alias match=test_empty!(re); 37. } 38. }
      
      





まず、それは何ですか? 関数でもクラスでもない、最も純粋な形式のテンプレートではなく、リテラルでパラメーター化されたコードの一部です。 ただし、 エイリアスは定期的に使用されます。

2番目に注意することは、この豊富なコードのうち、1つの小さなブランチのみがコンパイルされることです(カスケードの場合静的に注意してください)

戻り値? それらの2つがあります(pp。30-31および34-36):

static const size_t skip;



-使用した正規表現の文字数を示します。

alias match;



文字列パラメーターを使用して関数として呼び出すことができるもの (つまり、 alias )です。

さて、次のようなものを使用することが提案されています。







 alias myRe=regex!"A?[az]+\d*"; auto result=myRe.match(some_string);
      
      





今後、戻り値は2つの要素の構造であると言います。







 struct Match { bool _success; ulong length; bool opCast(T : bool)() const { return _success; } }
      
      





1つ目は、文字列が正規表現を満たすかどうかを示し、2つ目はトラバースされたサブストリングの長さを示します。 今、コンパイル中に、私たちはドラムに深く入っています。







枝に沿って歩きます。下から始める方が便利です:



正規表現が空(re.length == 0)(33ページ)の場合、常にtrueを返す関数へのリンク、つまりMatch(true、0)を返します 。 これで解析が正常に終了しました。

正規表現が空でない場合(ページ3)、 最初の文字に応じコンパイルXXXセットから最初にサブパーサーを選択します:service \。[(_またはその他の文字。サブパーサーは、トップレベルパーサーと同じインターフェイスを持ち、内部に定数を定義しますスキップして一致し、最後の(最も単純な、22ページ)ブランチでskip = 1と、次のように2行の最初の文字が等しいかどうかをチェックする関数へのリンクを返します。







 template compile_char(string re) { static const size_t skip=1; alias match=test_char!re; } // match single character Match test_char(string re)(string s) { static if(re.length) { if(s.length && s[0] == re[0]) return Match(1,1); } return Match(0,0); }
      
      





ここの2番目の構造は、実行時に呼び出される通常のテンプレート関数です。

残りのサブパーサーはそれほど複雑ではなく、同様に構成されていますが、パーサーが選択された後はどうなりますか? 述語はさらに進むことができます、 ?* +のいずれ 、または行かないので、選択されたサブパーサーは別のパーサーに渡されますcompile_pred(26ページ)







 // modify previous (term) regex with one of *+? predicates, or none template compile_pred(alias term, string re) { static if(re.length) { static if(re[0] == '*') { static const size_t skip=term.skip+1; alias match=zero_or_more!term; } else static if(re[0] == '+') { static const size_t skip=term.skip+1; alias match=one_or_more!term; } else static if(re[0] == '?') { static const size_t skip=term.skip+1; alias match=zero_or_one!term; } else { // no predicate, return term static const size_t skip=term.skip; alias match=term.match; } } else { // no predicate, end of regex, also return term static const size_t skip=term.skip; alias match=term.match; } } // conditionally match the expression, always match but matched length may be different Match zero_or_one(alias term)(string s) { Match r=term.match(s); return r? r : Match(1,0); }
      
      





ここで、既に実行されているシナリオによれば、述語に応じて、 スキップが 1ずつ増加し、 一致関数がテンプレート関数_zero_or_more(*)、zero_or_one(?)またはone_or more(+)のいずれかに設定されます。 または、述語がない場合、これらの値は両方ともアトミックパーサーから変更されません。







テンプレートと同様に、最後に再帰が必要です



メインパーサーに戻ると、述語を持つサブパーサーの完成した式を取得し、最終的に最後のサブパーサーに渡します。最後のサブパーサーは、両方のパーサーを順番に呼び出して、合計長を計算します(27ページ):







  alias result=both_of!(pred, regex!(re[pred.skip..$])); static const size_t skip=result.skip; alias match=result.match; ... // match both expressions template both_of(alias re1, alias re2) { static const size_t skip=re1.skip+re2.skip; alias match=test_join!(re1,re2); } // match both of expressions, return matched length Match test_join(alias re1, alias re2)(string s) { auto m1=re1.match(s); if(m1) { auto m2=re2.match(s[m1.length..$]); return Match(m1 && m2, m1.length+m2.length); } return m1; }
      
      





メインパーサーは、both_of サブパーサーの2番目のパラメーター自体を渡しますが、切り捨てられた正規表現-regex!(re [pred.skip .. $])を使用すると、サブパーサーで既に使用されている最初の文字が切り捨てられます。 したがって、正規表現は末尾に再帰的に渡され、 test_char、test_space、test_range、one_or_moreなどのテンプレート関数のチェーンが構築され、開始した場所に戻ります-test_empty関数が常に Matchを返す空の式(true、0)

解析は終了しました。







投稿の本質に移る前に、別の補助テンプレートを提供したいと思います(式「[]」および「()」の解析で既に使用されています)。 理解しやすいように、指定された文字までの文字列の長さを再帰的に計算します。 すぐにそれは私たちにとって有用になります:







 // misc., return length of literal up the to terminator template extract_until(string s, char terminator) { static assert(s.length, "missing terminator"); static if(s[0] == terminator) { static const length=0; } else { static const length=1+extract_until!(s[1..$], terminator).length; } }
      
      





最後にポイントに到達します



そこで、構造\ s \ d \ w \ xを理解するモデルパーサーを構築しました [az](AB)* +? 、別のブランチを簡単に追加できるようになりました。 他の行方不明の表現と一緒に誰かが気づいた場合、私はあまりにも面倒で、述語{n、m}の処理を構築できませんでした。 そのような美しいブラケットを使用しないのは罪なので、 {SUBEXPR}の表現のためにそれらを奪います。 まず、メインパーサーにブランチを追加します。







  // parse meta expression } else static if(re[0] == '{') { alias atom=compile_meta!re;
      
      





驚くほどシンプルなサブパーサーを追加します。







 template compile_meta(string re) { static const skip=2+extract_until!(re[1..$], '}').length; mixin("alias match="~re[1..skip-1]~".match;"); }
      
      





最初の行はalias match=SUBEXPR.match;



括弧の長さを閉じ括弧まで計算し、2行目はmixinを使用してフォームのリンクをコードに挿入しますalias match=SUBEXPR.match;



。 そしてそれだけです! 動き始めます。

これでようやく次のように書くことができます。







 alias NUM=regex!"\\d+"; alias INT=regex!"[+-]?{NUM}"; alias LIST=regex!"{INT}(,{INT})*"; assert(NUM.match("1234") == Match(true, "1234".length)); assert(INT.match("-12") == Match(true, "-12".length)); assert(LIST.match("1,+2,-3,4") == Match(true, "1,+2,-3,4".length));
      
      





ある種のバイソンが判明しました。







そして、私は思った...



私がハリウッド映画のキャラクターで、ある種のクレイジーな天才であれば、ジャンルの法則に沿って何かを見逃す必要があります。最終的には私の邪悪な計画を台無しにするという些細なことです。

そして、私はちょうどそれがどのようであるかを考えました- モジュールの循環依存 。 事実、これはすべて単一のファイル内で正常に機能し、このコードを別のライブラリまたはモジュールに分離しようとしても、何も機能しませんでした。 上記の例では、ユーザーコードで定義されているNUM文字とINT文字をmetareモジュールのレベルまでエクスポートする必要がありますが、これは明らかに不可能です。 しかし、いつか、どの言語でも、誰かが回避策を提案することはありません そしてついにこの世界を征服する

みなさん、ありがとうございました。

いつものように、誰かがそれを必要とする場合- コードへのリンク








All Articles