web-dev-qa-db-ja.com

正規表現の構文設計が読みにくくなる理由はありますか?

プログラマー全員が、コードの読みやすさは、機能する短い構文の1行よりもはるかに重要であることに同意しているようですが、上級の開発者はある程度の正確さで解釈する必要がありますが、これは正規表現の設計方法とまったく同じようです。これには理由がありましたか?

selfDocumentingMethodName()e()よりもはるかに優れていることに私たちは皆同意します。なぜそれが正規表現にも当てはまらないのですか?

構造的な編成のない1行ロジックの構文を設計するのではなく、

var parse_url = /^(?:([A-Za-z]+):)?(\/{0,3})(0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;

そして、これはURLの厳密な解析でさえありません!

代わりに、基本的な例として、いくつかのパイプライン構造を構成して読み取り可能にすることができます。

string.regex
   .isRange('A-Z' || 'a-z')
   .followedBy('/r');

正規表現の非常に簡潔な構文には、可能な最短の演算と論理構文以外にどのような利点がありますか?結局のところ、正規表現構文のデザインが読みにくくなるのには、特定の技術的な理由がありますか?

161
Viziionary

正規表現が単純であるように設計された大きな理由が1つあります。正規表現は、コードを記述する言語としてではなく、コードエディターへのコマンドとして使用するために設計されました。より正確には、edは、正規表現を使用する最初のプログラム、そしてそこから正規表現が世界支配の征服を始めました。たとえば、edコマンドg/<regular expression>/pはすぐにgrepと呼ばれる別のプログラムに影響を与えました。これは現在も使用されています。それらの力のために、それらはその後標準化され、sedvimなどのさまざまなツールで使用されました。

しかし、雑学には十分です。では、なぜこのOriginは簡潔な文法を支持するのでしょうか?エディタコマンドを入力してそれをもう一度読む必要がないからです。それを組み立てる方法を覚えておくことができ、やりたいことをそれで行うことができれば十分です。ただし、入力する必要があるすべての文字により、ファイルの編集の進行が遅くなります。正規表現の構文は、比較的複雑な検索をスローアウェイ方式で作成するように設計されており、プログラムへの入力を解析するためのコードとしてそれらを使用する人々の頭痛の種となっています。

あなたが引用する正規表現はひどい混乱であり、それが読みやすいということに誰もが同意しているとは思わない。同時に、その醜さの多くは解決される問題に固有です:ネストのいくつかのレイヤーがあり、URL文法は比較的複雑です(確かに複雑すぎて、どの言語でも簡潔に通信することはできません)。しかし、この正規表現が説明していることを説明するより良い方法があることは確かに本当です。では、なぜ使用されないのでしょうか。

大きな理由は、慣性と普遍性です。そもそもそれらがどのようにして人気を博したかは説明していませんが、今では、正規表現を知っている人なら誰でも、100の異なる言語と追加の1,000のソフトウェアツールでこれらのスキルを使用できます(方言の違いはほとんどありません)。たとえば、テキストエディタやコマンドラインツールなど)。ちなみに、後者はプログラマではないため、ライティングプログラムとなるソリューションを使用することはできず、使用することもできませんでした。

それにもかかわらず、正規表現は頻繁に使いすぎて、別のツールがはるかに優れている場合でも適用されます。正規表現の構文はひどいだとは思いません。しかし、Cのような言語での識別子の典型的な例である[a-zA-Z_][a-zA-Z0-9_]*は、最小限の正規表現の知識で読み取ることができ、そのバーが満たされると、明白かつ見事なものになります。簡潔。必要な文字数が少なくても、本質的に悪いわけではなく、まったく逆です。簡潔であることがあなたが理解しやすいままであるという条件で美徳です。

この構文がこれらのような単純なパターンに優れている理由は少なくとも2つあります。ほとんどの文字をエスケープする必要がないため、比較的自然に読み取られます。また、使用可能な句読点をすべて使用して、さまざまな単純な解析コンビネーターを表現します。おそらく最も重要なことは、シーケンス処理に何でもを必要としないことです。あなたは最初のものを書いて、それからそれに続くものを書きます。これをfollowedByと比較すると、特に次のパターンがnotリテラルであるが、より複雑な式である場合は特にそうです。

では、なぜもっと複雑なケースでは不十分なのでしょうか?私は3つの主な問題を見ることができます:

  1. 抽象化機能はありません。正規表現と同じ理論的なコンピューターサイエンスの分野に由来する正式な文法には一連の生成物があるため、パターンの中間部分に名前を付けることができます。

    # This is not equivalent to the regex in the question
    # It's just a mock-up of what a grammar could look like
    url      ::= protocol? '/'? '/'? '/'? (domain_part '.')+ tld
    protocol ::= letter+ ':'
    ...
    
  2. 上記のように、特別な意味を持たない空白は、目にやさしいフォーマットを可能にするのに役立ちます。コメントも同じです。スペースはそれだけのリテラル' 'であるため、正規表現ではそれができません。ただし、一部の実装では、空白が無視され、コメントが可能な「詳細」モードが許可されています。

  3. 一般的なパターンとコンビネーターを説明するメタ言語はありません。たとえば、digitルールを1回記述して、それをコンテキストフリーの文法で使い続けることができますが、プロダクションpが与えられ、いわば「関数」を定義して、たとえば、pの出現をコンマで区切ったリストの生成を作成するなど、追加の処理を行う新しい生成。

あなたが提案するアプローチは確かにこれらの問題を解決します。必要以上に簡潔にトレードするので、うまく解決しません。最初の2つの問題は、比較的単純で簡潔なドメイン固有の言語で解決しながら解決できます。 3番目、まあ...プログラムによるソリューションには、もちろん汎用プログラミング言語が必要ですが、私の経験では、3番目はこれらの問題の中で最も少ないものです。プログラマーが新しいコンビネーターを定義する能力を求めているのと同じ複雑なタスクが十分に発生するパターンはほとんどありません。そして、これが必要な場合、言語はしばしば複雑になり、とにかく正規表現で解析できないし、解析すべきではありません。

それらのケースに対する解決策が存在します。およそ1万個のパーサーコンビネーターライブラリがあり、提案する操作を大まかに実行します。これは、操作のセットが異なり、構文が異なることが多く、ほとんどの場合、正規表現よりも多くの解析能力を備えています(つまり、コンテキストフリー言語またはかなりのサイズを扱います)それらのサブセット)。次に、前述の「より良いDSLを使用する」アプローチに対応するパーサージェネレータがあります。そして、適切なコードで、解析の一部を手動で書くオプションが常にあります。単純なサブタスクに正規表現を使用し、正規表現を呼び出すコードで複雑なことを行って、組み合わせることもできます。

正規表現がどのように人気を博したようになったのかを説明するのに、コンピューティングの初期の頃については十分に知りません。しかし、彼らは留まるためにここにいます。あなたはそれらを賢く使う必要があります、そしてそれがより賢いときはnotを使ってください。

62
user7043

歴史的展望

Wikipediaの記事 は、正規表現の起源について非常に詳細です(Kleene、1956)。元の構文は比較的単純で、_*_、_+_、_?_、_|_およびグループ化_(...)_のみでした。形式的な言語は簡潔な数学的表記で表現される傾向があるため、簡潔でした(および読み取り可能、2つは必ずしも反対ではありません)。

その後、構文と機能はエディタで進化し、 Perl で成長しました。これは、設計によって簡潔にすることを試みていました( "一般的な構造は短くする必要があります" )。これにより構文が大幅に複雑になりましたが、今では正規表現に慣れているため、それらを書く(読むのではなくても)のが得意です。それらが書き込み専用である場合があるという事実は、長すぎる場合、通常は適切なツールではないことを示唆しています。 正規表現は、乱用されていると読みにくくなる傾向があります。

文字列ベースの正規表現を超えて

代替構文について言えば、すでに存在する構文を見てみましょう( cl-ppcreCommon LISP )。長い正規表現は、次のように_ppcre:parse-string_で解析できます。

_(let ((*print-case* :downcase)
      (*print-right-margin* 50))
  (pprint
   (ppcre:parse-string "^(?:([A-Za-z]+):)?(\\/{0,3})(0-9.\\-A-Za-z]+)(?::(\\d+))?(?:\\/([^?#]*))?(?:\\?([^#]*))?(?:#(.*))?$")))
_

...結果は次の形式になります。

_(:sequence :start-anchor
 (:greedy-repetition 0 1
  (:group
   (:sequence
    (:register
     (:greedy-repetition 1 nil
      (:char-class (:range #\A #\Z)
       (:range #\a #\z))))
    #\:)))
 (:register (:greedy-repetition 0 3 #\/))
 (:register
  (:sequence "0-9" :everything "-A-Za-z"
   (:greedy-repetition 1 nil #\])))
 (:greedy-repetition 0 1
  (:group
   (:sequence #\:
    (:register
     (:greedy-repetition 1 nil :digit-class)))))
 (:greedy-repetition 0 1
  (:group
   (:sequence #\/
    (:register
     (:greedy-repetition 0 nil
      (:inverted-char-class #\? #\#))))))
 (:greedy-repetition 0 1
  (:group
   (:sequence #\?
    (:register
     (:greedy-repetition 0 nil
      (:inverted-char-class #\#))))))
 (:greedy-repetition 0 1
  (:group
   (:sequence #\#
    (:register
     (:greedy-repetition 0 nil :everything)))))
 :end-anchor)
_

この構文はより冗長であり、以下のコメントを見ると、必ずしもより読みやすいとは限りません。 したがって、構文がコンパクトではないため、状況が自動的に明確になると仮定しないでください

ただし、正規表現で問題が発生し始めた場合は、正規表現をこの形式に変換すると、コードの解読とデバッグに役立つ場合があります。これは、1文字のエラーを見つけるのが難しい文字列ベースの形式に比べて1つの利点です。 この構文の主な利点は、文字列ベースのエンコーディングではなく構造化されたフォーマットを使用して正規表現を操作することです。これにより、プログラム内の他のデータ構造と同様に、このような式をcomposeおよびbuildできます。上記の構文を使用する場合、これは通常、小さい部分から式を作成したいためです( my CodeGolfの回答 も参照)。あなたの例として、私たちは書くかもしれません1

_`(:sequence
   :start-anchor
   ,(protocol)
   ,(slashes)
   ,(domain)
   ,(top-level-domain) ... )
_

文字列ベースの正規表現は、ヘルパー関数でラップされた文字列連結または補間を使用して構成することもできます。ただし、文字列操作には制限があります クラッターコード (bashでの$(...)とは異なり、ネストの問題について考えてください。また、 、エスケープ文字は頭痛の種になるかもしれません)。

また、上記のフォームでは_(:regex "string")_フォームを使用できるので、ツリーと簡潔な表記を混在させることができます。これらすべてにより、IMHOは読みやすく、構成しやすくなっています。 delnanによって表現された3つの問題 に間接的に対処します(つまり、正規表現自体の言語ではありません)。

おわりに

  • ほとんどの場合、簡潔な表記は実際には読みやすいです。バックトラッキングなどを含む拡張表記を処理する際には困難がありますが、それらの使用が正当化されることはめったにありません。正規表現を不正に使用すると、表現が読めなくなる可能性があります。

  • 正規表現を文字列としてエンコードする必要はありません。正規表現の作成と作成に役立つライブラリまたはツールがある場合、文字列操作に関連する多くの潜在的なバグを回避します。

  • または、正式な文法は読みやすく、部分式の命名と抽象化に優れています。端末は通常、単純な正規表現として表現されます。


1。正規表現はアプリケーションでは定数になる傾向があるため、式を読み取り時に作成することをお勧めします。 _create-scanner_ および _load-time-value_ を参照してください:

_'(:sequence :start-anchor #.(protocol) #.(slashes) ... )
_
39
coredump

正規表現の最大の問題は、過度に簡潔な構文ではなく、小さなビルディングブロックから構成するのではなく、単一の式で複雑な定義を表現しようとすることです。これは、変数や関数を使用せず、コードをすべて1行に埋め込むプログラミングに似ています。

正規表現を [〜#〜] bnf [〜#〜] と比較します。構文は正規表現ほどきれいではありませんが、使い方は異なります。まず、単純な名前付きシンボルを定義し、照合するパターン全体を表すシンボルに到達するまで、それらを作成します。

たとえば、 rfc3986 のURI構文を見てください。

URI           = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
scheme        = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
hier-part     = "//" authority path-abempty
              / path-absolute
              / path-rootless
              / path-empty
...

名前付きサブ式の埋め込みをサポートする正規表現構文のバリアントを使用して、ほぼ同じことを書くことができます。


個人的には、構文のような簡潔な正規表現は、文字クラス、連結、選択、または繰り返しなどの一般的に使用される機能には問題ないと思いますが、先読みの詳細名などのより複雑でまれな機能が望ましいです。通常のプログラミングで+*のような演算子を使用する方法とよく似ており、まれな操作のために名前付き関数に切り替えます。

25
CodesInChaos

selfDocumentingMethodName()はe()よりもはるかに優れています

それは...ですか?ほとんどの言語で、BEGINおよびENDではなく{および}がブロック区切り文字として使用されているのには理由があります。

人々は簡潔さを好み、構文を理解したら、短い用語の方が適しています。あなたの正規表現の例を想像してみてください。もしd(数字)が '数字'だったとしたら、正規表現はもっとひどいものになるでしょう。制御文字で簡単に解析できるようにすると、XMLのように見えます。構文がわかれば、どちらも上手くいきません。

ただし、質問に適切に答えるには、正規表現が簡潔さが必須であった時代から来ていることを理解する必要があります.1 MBのXMLドキュメントは今日大した問題ではないと考えるのは簡単ですが、1 MBがかなり多かった日について話しています全体のストレージ容量。当時使用されていた言語も少なく、正規表現はPerlやCから100万マイルも離れていないため、構文は、その日の構文を習得したいと思うプログラマーには馴染み深いものです。したがって、これをより冗長にする理由はありませんでした。

12
gbjbaanb

正規表現はレゴのピースのようなものです。一見すると、結合できるいくつかの異なる形状のプラスチック部品が表示されます。あなたが形作ることができるあまり多くの可能な異なるものはないと思うかもしれませんが、それからあなたは他の人がする驚くべきことを見て、そしてそれがどれほど驚くべきおもちゃであるのかと思っているだけです。

正規表現はレゴのピースのようなものです。使用できる引数はほとんどありませんが、さまざまな形式でそれらをチェーンすると、多くの複雑なタスクに使用できる数百万の異なる正規表現パターンが形成されます。

正規表現パラメーターだけを使用することはめったにありません。多くの言語では、文字列の長さをチェックしたり、数値部分を分割したりするための関数を提供しています。文字列関数を使用して、テキストをスライスして再構成できます。複雑なフォームを使用して非常に具体的な複雑なタスクを実行すると、正規表現の威力がわかります。

SOには数万の正規表現の質問があり、重複としてマークされることはめったにありません。これだけでは、非常に異なる可能性のある一意のユースケースが示されます。

そして、この非常に異なるユニークなタスクを処理するための事前定義されたメソッドを提供することは容易ではありません。 これらの種類のタスクには文字列関数がありますが、それらの関数が特定のタスクに十分でない場合は、正規表現を使用するときです

6
FallenAngel

私はこれが効力というより実践の問題であることを認識しています。この問題は通常、複合的な性質を仮定するのではなく、正規表現が直接で実装されている場合に発生します。同様に、優れたプログラマーは、自分のプログラムの機能を簡潔なメソッドに分解します。

たとえば、URLの正規表現文字列は、およそ次のように削減できます。

UriRe = [scheme][hier-part][query][fragment]

に:

UriRe = UriSchemeRe + UriHierRe + "(/?|/" + UriQueryRe + UriFragRe + ")"
UriSchemeRe = [scheme]
UriHierRe = [hier-part]
UriQueryRe = [query]
UriFragRe = [fragment]

正規表現は気の利いたものですが、apparentの複雑さに夢中になっている人によって乱用されがちです。結果の表現は修辞的で、長期的な価値はありません。

2
toplel32

@cmasterが言うように、正規表現は元々オンザフライでのみ使用されるように設計されており、ラインノイズ構文が依然として最も人気のある構文であることは単に奇妙です(そして少し気がかりです)。私が考えることができる唯一の説明は、慣性、マゾヒズム、またはマシモのいずれかを含みます(「慣性」が何かをする最も魅力的な理由であるとは限りません...)

Perlは、空白とコメントを許可することで、読みやすくするためにやや弱い試みをしますが、想像力に富んだことは何もしません。

他の構文があります。良いものは regexpsのscsh構文 です。これは、私の経験では、かなり簡単に入力できますが、事実の後で読むことができる正規表現を生成します。

[ scsh は他の理由ですばらしいですが、その1つが有名な 謝辞のテキスト ]です。

0
Norman Gray

正規表現はできるだけ「一般的」かつシンプルに設計されているため、どこでも(ほぼ)同じように使用できます。

あなたはregex.isRange(..).followedBy(..)が特定のプログラミング言語の構文とおそらくオブジェクト指向のスタイル(メソッドチェーン)の両方に結合されている例です。

たとえば、この正確な「正規表現」はCではどのように見えますか?コードを変更する必要があります。

最も「一般的な」アプローチは、変更なしで他の言語に簡単に組み込むことができる単純な簡潔な言語を定義することです。そして、それが(ほとんど)正規表現です。

0
Aviv Cohn

Perl互換の正規表現 エンジンは広く使用されており、多くのエディターや言語が理解できる簡潔な正規表現構文を提供します。 @JDługoszがコメントで指摘したように、 Perl 6 (Perl 5の新しいバージョンだけでなく、完全に異なる言語)は、個別に定義された要素から正規表現を作成することで、それらをより読みやすくしようとしました。たとえば、URLを解析するための文法の例を次に示します Wikibooksから

grammar URL {
  rule TOP {
    <protocol>'://'<address>
  }
  token protocol {
    'http'|'https'|'ftp'|'file'
  }
  rule address {
    <subdomain>'.'<domain>'.'<tld>
  }
  ...
}

このような正規表現を分割することで、各ビットを個別に定義(例:domainを英数字に制限)したり、サブクラス化(例:FileURL is URLその制約protocol"file")。

だから:いいえ、正規表現が簡潔であることには技術的な理由はありませんが、それらを表現するための新しく、よりクリーンで読みやすい方法がすでにここにあります!うまくいけば、この分野でいくつかの新しいアイデアが見つかるでしょう。

0
Gaurav