it-swarm.com.de

Gibt es einen bestimmten Grund für die schlechte Lesbarkeit des Syntaxdesigns für reguläre Ausdrücke?

Alle Programmierer scheinen sich darin einig zu sein, dass die Lesbarkeit von Code weitaus wichtiger ist als Einzeiler mit kurzer Syntax, die funktionieren, aber von einem erfahrenen Entwickler eine genaue Interpretation verlangen - aber genau so scheinen reguläre Ausdrücke entworfen worden zu sein. Gab es einen Grund dafür?

Wir sind uns alle einig, dass selfDocumentingMethodName() weitaus besser ist als e(). Warum sollte das nicht auch für reguläre Ausdrücke gelten?

Es scheint mir, dass anstatt eine Syntax der einzeiligen Logik ohne strukturelle Organisation zu entwerfen:

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

Und dies ist nicht einmal das strikte Parsen einer URL!

Stattdessen könnten wir eine Pipeline-Struktur für ein grundlegendes Beispiel organisieren und lesbar machen:

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

Welchen Vorteil bietet die extrem knappe Syntax eines regulären Ausdrucks außer der kürzestmöglichen Operation und Logiksyntax? Gibt es letztendlich einen bestimmten technischen Grund für die schlechte Lesbarkeit des Syntaxdesigns für reguläre Ausdrücke?

161
Viziionary

Es gibt einen großen Grund, warum reguläre Ausdrücke so knapp wie sie sind: Sie wurden als Befehle für einen Code-Editor verwendet, nicht als Sprache zum Codieren. Genauer gesagt war ed eine der Die ersten Programme, die reguläre Ausdrücke verwendeten, und von dort aus begannen reguläre Ausdrücke ihre Eroberung der Weltherrschaft. Zum Beispiel der Befehl edg/<regular expression>/p inspirierte bald ein separates Programm namens grep, das heute noch verwendet wird. Aufgrund ihrer Leistungsfähigkeit wurden sie anschließend standardisiert und in einer Vielzahl von Tools wie sed und vim verwendet

Aber genug für die Trivia. Warum sollte dieser Ursprung eine knappe Grammatik bevorzugen? Weil Sie keinen Editorbefehl eingeben, um ihn noch einmal zu lesen. Es reicht aus, dass Sie sich daran erinnern können, wie man es zusammensetzt, und dass Sie die Dinge damit machen können, die Sie tun möchten. Jedes Zeichen, das Sie eingeben müssen, verlangsamt jedoch Ihren Fortschritt beim Bearbeiten Ihrer Datei. Die Syntax für reguläre Ausdrücke wurde entwickelt, um relativ komplexe Suchvorgänge wegwerfbar zu schreiben, und genau das bereitet den Leuten Kopfschmerzen, die sie als Code verwenden, um Eingaben in ein Programm zu analysieren.

Der reguläre Ausdruck, den Sie zitieren, ist ein schreckliches Durcheinander, und ich glaube, niemand stimmt zu, dass er lesbar ist. Gleichzeitig ist ein Großteil dieser Hässlichkeit mit dem zu lösenden Problem verbunden: Es gibt mehrere Verschachtelungsebenen und die URL-Grammatik ist relativ kompliziert (sicherlich zu kompliziert, um in einer beliebigen Sprache kurz und bündig zu kommunizieren). Es ist jedoch sicher richtig, dass es bessere Möglichkeiten gibt, zu beschreiben, was diese Regex beschreibt. Warum werden sie nicht verwendet?

Ein großer Grund ist Trägheit und Allgegenwart. Es erklärt nicht, wie sie überhaupt so populär wurden, aber jetzt, da sie es sind, kann jeder, der reguläre Ausdrücke kennt, diese Fähigkeiten (mit sehr wenigen Unterschieden zwischen Dialekten) in hundert verschiedenen Sprachen und zusätzlich tausend Softwaretools ( zB Texteditoren und Befehlszeilentools). Letztere würden und könnten übrigens keine Lösung verwenden, die sich auf Schreiben von Programmen beläuft, da sie von Nicht-Programmierern stark genutzt werden.

Trotzdem werden reguläre Ausdrücke häufig überbeansprucht, dh auch dann angewendet, wenn ein anderes Tool viel besser wäre. Ich denke nicht, dass die Regex-Syntax schrecklich ist. Bei kurzen und einfachen Mustern ist es jedoch deutlich besser: Das archetypische Beispiel für Bezeichner in C-ähnlichen Sprachen, [a-zA-Z_][a-zA-Z0-9_]* kann mit einem absoluten Minimum an Regex-Kenntnissen gelesen werden, und sobald dieser Balken erreicht ist, ist er sowohl offensichtlich als auch kurz und bündig. Weniger Zeichen zu benötigen ist nicht von Natur aus schlecht, ganz im Gegenteil. Prägnant zu sein ist eine Tugend, vorausgesetzt, Sie bleiben verständlich.

Es gibt mindestens zwei Gründe, warum sich diese Syntax bei einfachen Mustern wie diesen auszeichnet: Sie erfordert für die meisten Zeichen kein Escapezeichen, liest sich also relativ natürlich und verwendet alle verfügbaren Satzzeichen, um eine Vielzahl einfacher Parsing-Kombinatoren auszudrücken. Am wichtigsten ist vielleicht, dass für die Sequenzierung kein überhaupt nichts erforderlich ist. Sie schreiben das erste, dann das, was danach kommt. Vergleichen Sie dies mit Ihrem followedBy, insbesondere wenn das folgende Muster nicht ein Literal, aber ein komplizierterer Ausdruck ist.

Warum bleiben sie in komplizierteren Fällen zurück? Ich kann drei Hauptprobleme sehen:

  1. Es gibt keine Abstraktionsmöglichkeiten. Formale Grammatiken, die aus demselben Gebiet der theoretischen Informatik stammen wie Regexes, haben eine Reihe von Produktionen, sodass sie Zwischenteile des Musters benennen können:

    # 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. Wie wir oben sehen konnten, ist ein Leerzeichen ohne besondere Bedeutung nützlich, um eine Formatierung zu ermöglichen, die für die Augen einfacher ist. Gleiches gilt für Kommentare. Reguläre Ausdrücke können das nicht, weil ein Leerzeichen genau das ist, ein wörtliches ' '. Beachten Sie jedoch: Einige Implementierungen erlauben einen "ausführlichen" Modus, in dem Leerzeichen ignoriert werden und Kommentare möglich sind.

  3. Es gibt keine Metasprache, um gängige Muster und Kombinatoren zu beschreiben. Zum Beispiel kann man eine digit Regel einmal schreiben und sie weiterhin in einer kontextfreien Grammatik verwenden, aber man kann sozusagen keine "Funktion" definieren, die eine Produktion p erhält und eine erstellt Neue Produktion, die etwas Besonderes damit macht, zum Beispiel eine Produktion für eine durch Kommas getrennte Liste von Vorkommen von p erstellen.

Der von Ihnen vorgeschlagene Ansatz löst diese Probleme mit Sicherheit. Es löst sie einfach nicht sehr gut, weil es weitaus prägnanter dafür handelt, als es notwendig ist. Die ersten beiden Probleme können gelöst werden, während sie in einer relativ einfachen und knappen domänenspezifischen Sprache bleiben. Die dritte, na ja ... eine programmatische Lösung erfordert natürlich eine universelle Programmiersprache, aber meiner Erfahrung nach ist die dritte bei weitem das geringste dieser Probleme. Nur wenige Muster haben genug Vorkommen derselben komplexen Aufgabe, nach der sich der Programmierer nach der Fähigkeit sehnt, neue Kombinatoren zu definieren. Und wenn dies notwendig ist, ist die Sprache oft so kompliziert, dass sie ohnehin nicht mit regulären Ausdrücken analysiert werden kann und sollte.

Lösungen für diese Fälle existieren. Es gibt ungefähr zehntausend Parser-Kombinator-Bibliotheken, die ungefähr das tun, was Sie vorschlagen, nur mit einem anderen Satz von Operationen, oft einer anderen Syntax und fast immer mit mehr Analysekraft als reguläre Ausdrücke (dh sie befassen sich mit kontextfreien Sprachen oder einigen beträchtlichen Teilmenge davon). Dann gibt es Parser-Generatoren, die mit dem oben beschriebenen Ansatz "Verwenden Sie ein besseres DSL" arbeiten. Und es gibt immer die Möglichkeit, einen Teil der Analyse von Hand in den richtigen Code zu schreiben. Sie können sogar mischen und anpassen, indem Sie reguläre Ausdrücke für einfache Unteraufgaben verwenden und die komplizierten Dinge im Code ausführen, der die regulären Ausdrücke aufruft.

Ich weiß nicht genug über die frühen Jahre des Rechnens, um zu erklären, wie reguläre Ausdrücke so populär wurden. Aber sie sind hier, um zu bleiben. Sie müssen sie nur mit Bedacht einsetzen und nicht verwenden, wenn dies klüger ist.

62
user7043

Historische Perspektive

Der Wikipedia-Artikel ist ziemlich detailliert über die Ursprünge regulärer Ausdrücke (Kleene, 1956). Die ursprüngliche Syntax war relativ einfach mit nur *, +, ?, | Und Gruppierung (...). Es war knapp ( und lesbar, die beiden sind nicht unbedingt gegensätzlich), weil formale Sprachen dazu neigen, mit knappen mathematischen Notationen ausgedrückt zu werden.

Später entwickelten sich die Syntax und die Funktionen mit den Editoren und wuchsen mit Perl , das vom Design her knapp zu sein versuchte ( "gängige Konstruktionen sollten kurz sein" ). Dies hat die Syntax stark komplexiert, aber beachten Sie, dass die Benutzer jetzt an reguläre Ausdrücke gewöhnt sind und diese gut schreiben (wenn nicht lesen) können. Die Tatsache, dass sie manchmal nur zum Schreiben bestimmt sind, deutet darauf hin, dass sie im Allgemeinen nicht das richtige Werkzeug sind, wenn sie zu lang sind. Reguläre Ausdrücke sind bei Missbrauch in der Regel nicht lesbar.

Über stringbasierte reguläre Ausdrücke hinaus

Wenn wir über alternative Syntaxen sprechen, schauen wir uns eine an, die bereits existiert ( cl-ppcre , in Common LISP ). Ihr langer regulärer Ausdruck kann wie folgt mit ppcre:parse-string Analysiert werden:

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

... und ergibt folgende Form:

(: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)

Diese Syntax ist ausführlicher und wenn Sie sich die Kommentare unten ansehen, nicht unbedingt besser lesbar. Gehen Sie also nicht davon aus, dass die Dinge automatisch klarer werden, weil Sie eine weniger kompakte Syntax haben.

Wenn Sie jedoch Probleme mit Ihren regulären Ausdrücken haben, können Sie Ihren Code möglicherweise entschlüsseln und debuggen, indem Sie sie in dieses Format umwandeln. Dies ist ein Vorteil gegenüber stringbasierten Formaten, bei denen es schwierig sein kann, einen einzelnen Zeichenfehler zu erkennen. Der Hauptvorteil dieser Syntax besteht darin, reguläre Ausdrücke mithilfe eines strukturierten Formats anstelle einer stringbasierten Codierung zu bearbeiten. So können Sie komponieren und bauen solche Ausdrücke wie jede andere Datenstruktur in Ihrem Programm verwenden. Wenn ich die obige Syntax verwende, liegt dies im Allgemeinen daran, dass ich Ausdrücke aus kleineren Teilen erstellen möchte (siehe auch meine CodeGolf-Antwort ). Für Ihr Beispiel können wir schreiben 1 ::

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

String-basierte reguläre Ausdrücke können auch mithilfe von String-Verkettung und/oder Interpolation in Hilfsfunktionen erstellt werden. Es gibt jedoch Einschränkungen bei String-Manipulationen, die dazu neigen, nordnung den Code (denken Sie an Verschachtelungsprobleme, ähnlich wie Backticks vs. $(...) in bash; auch , Escape-Zeichen können Kopfschmerzen verursachen).

Beachten Sie auch, dass das obige Formular (:regex "string") Formulare zulässt, sodass Sie knappe Notationen mit Bäumen mischen können. All dies führt meiner Meinung nach zu einer guten Lesbarkeit und Zusammensetzbarkeit. es befasst sich indirekt mit den drei von delnan ausgedrückten Problemen (d. h. nicht in der Sprache der regulären Ausdrücke selbst).

Schlussfolgern

  • Für die meisten Zwecke ist die knappe Notation tatsächlich lesbar. Es gibt Schwierigkeiten beim Umgang mit erweiterten Notationen, die das Zurückverfolgen usw. beinhalten, aber ihre Verwendung ist selten gerechtfertigt. Die ungerechtfertigte Verwendung regulärer Ausdrücke kann zu unlesbaren Ausdrücken führen.

  • Reguläre Ausdrücke müssen nicht als Zeichenfolgen codiert werden. Wenn Sie über eine Bibliothek oder ein Tool verfügen, mit dem Sie reguläre Ausdrücke erstellen und erstellen können, werden Sie vermeiden viele potenzielle Fehler im Zusammenhang mit Zeichenfolgenmanipulationen.

  • Alternativ sind formale Grammatiken besser lesbar und können Unterausdrücke besser benennen und abstrahieren. Terminals werden im Allgemeinen als einfache reguläre Ausdrücke ausgedrückt.


1. Möglicherweise möchten Sie Ihre Ausdrücke lieber zum Zeitpunkt des Lesens erstellen, da reguläre Ausdrücke in der Regel Konstanten in einer Anwendung sind. Siehe create-scanner und load-time-value :

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

Das größte Problem bei Regex ist nicht die zu knappe Syntax. Wir versuchen, eine komplexe Definition in einem einzigen Ausdruck auszudrücken, anstatt sie aus kleineren Bausteinen zusammenzusetzen. Dies ähnelt der Programmierung, bei der Sie niemals Variablen und Funktionen verwenden und stattdessen Ihren Code in eine einzige Zeile einbetten.

Vergleiche Regex mit BNF . Die Syntax ist nicht viel sauberer als bei Regex, wird aber anders verwendet. Sie definieren zunächst einfache benannte Symbole und setzen sie zusammen, bis Sie zu einem Symbol gelangen, das das gesamte Muster beschreibt, mit dem Sie übereinstimmen möchten.

Schauen Sie sich zum Beispiel die URI-Syntax in rfc3986 an:

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

Sie könnten fast dasselbe mit einer Variante der Regex-Syntax schreiben, die das Einbetten benannter Unterausdrücke unterstützt.


Persönlich denke ich, dass eine knappe Regex-ähnliche Syntax für häufig verwendete Funktionen wie Zeichenklassen, Verkettung, Auswahl oder Wiederholung in Ordnung ist, aber für komplexere und seltenere Funktionen wie vorausschauende ausführliche Namen sind vorzuziehen. Ganz ähnlich wie wir Operatoren wie + Oder * In der normalen Programmierung verwenden und für seltenere Operationen zu benannten Funktionen wechseln.

25
CodesInChaos

selfDocumentingMethodName () ist weitaus besser als e ()

ist es? Es gibt einen Grund, warum die meisten Sprachen {und} als Blocktrennzeichen anstelle von BEGIN und END haben.

Leute mögen Knappheit, und wenn Sie die Syntax kennen, ist eine kurze Terminologie besser. Stellen Sie sich Ihr Regex-Beispiel vor, wenn d (für Ziffer) 'Ziffer' wäre, wäre der Regex noch schrecklicher zu lesen. Wenn Sie es leichter mit Steuerzeichen analysieren könnten, würde es eher wie XML aussehen. Weder sind so gut, wenn Sie die Syntax kennen.

Um Ihre Frage richtig zu beantworten, müssen Sie sich darüber im Klaren sein, dass Regex aus den Tagen stammt, als die Knappheit obligatorisch war. Es ist leicht zu glauben, dass ein 1-MB-XML-Dokument heute keine große Sache ist, aber wir sprechen von Tagen, an denen 1 MB ziemlich viel war Ihre gesamte Speicherkapazität. Damals wurden auch weniger Sprachen verwendet, und Regex ist keine Million Meilen von Perl oder C entfernt, sodass die Syntax den damaligen Programmierern bekannt ist, die mit dem Erlernen der Syntax zufrieden wären. Es gab also keinen Grund, es ausführlicher zu machen.

12
gbjbaanb

Regex ist wie Legostücke. Auf den ersten Blick sehen Sie einige unterschiedlich geformte Kunststoffteile, die zusammengefügt werden können. Du denkst vielleicht, es gäbe nicht zu viele mögliche verschiedene Dinge, die du formen kannst, aber dann siehst du die erstaunlichen Dinge, die andere Leute tun, und du fragst dich nur, wie ein erstaunliches Spielzeug es ist.

Regex ist wie Legostücke. Es gibt nur wenige Argumente, die verwendet werden können, aber wenn Sie sie in verschiedenen Formen verketten, werden Millionen verschiedener Regex-Muster gebildet, die für viele komplizierte Aufgaben verwendet werden können.

Menschen verwendeten selten Regex-Parameter allein. In vielen Sprachen können Sie die Länge einer Zeichenfolge überprüfen oder die numerischen Teile herausteilen. Sie können Zeichenfolgenfunktionen verwenden, um Texte zu schneiden und zu reformieren. Die Kraft von Regex wird bemerkt, wenn Sie komplexe Formulare verwenden, um sehr spezifische komplexe Aufgaben zu erledigen.

Sie finden Zehntausende von Regex-Fragen auf SO und sie werden selten als Duplikat markiert. Dies allein zeigt die möglichen eindeutigen Anwendungsfälle, die sich stark voneinander unterscheiden.

Und es ist nicht einfach, vordefinierte Methoden anzubieten, um diese sehr unterschiedlichen einzigartigen Aufgaben zu bewältigen. Sie haben Zeichenfolgenfunktionen für diese Art von Aufgaben, aber wenn diese Funktionen für Ihre spezifische Aufgabe nicht ausreichen, ist es Zeit, Regex zu verwenden.

6
FallenAngel

Ich erkenne, dass dies eher ein Problem der Praxis als der Potenz ist. Das Problem tritt normalerweise auf, wenn reguläre Ausdrücke direkt implementiert werden, anstatt eine zusammengesetzte Natur anzunehmen. Ebenso wird ein guter Programmierer die Funktionen seines Programms in prägnante Methoden zerlegen.

Beispielsweise könnte eine Regex-Zeichenfolge für eine URL von ungefähr reduziert werden:

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

zu:

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

Reguläre Ausdrücke sind raffinierte Dinge, aber sie neigen dazu, von denen missbraucht zu werden, die sich in ihre scheinbare Komplexität vertiefen. Die daraus resultierenden Ausdrücke sind rhetorisch und haben keinen langfristigen Wert.

2
toplel32

Wie @cmaster sagt, wurden Regexps ursprünglich nur für die Verwendung im laufenden Betrieb entwickelt, und es ist einfach bizarr (und leicht deprimierend), dass die Zeilenrauschsyntax immer noch die beliebteste ist. Die einzigen Erklärungen, die mir einfallen, sind entweder Trägheit, Masochismus oder Machismus (es kommt nicht oft vor, dass Trägheit der attraktivste Grund ist, etwas zu tun ...)

Perl unternimmt einen eher schwachen Versuch, sie lesbarer zu machen, indem Leerzeichen und Kommentare zugelassen werden, unternimmt jedoch keine einfallsreichen Schritte.

Es gibt andere Syntaxen. Eine gute ist die scsh-Syntax für Regexps , die meiner Erfahrung nach Regexps erzeugt, die relativ einfach zu tippen sind, aber nachträglich noch lesbar sind.

[ scsh ist aus anderen Gründen großartig, nur einer davon ist der berühmte Bestätigungstext ]

0
Norman Gray

Ich glaube, reguläre Ausdrücke sollten so allgemein und einfach wie möglich sein, damit sie überall (ungefähr) auf die gleiche Weise verwendet werden können.

Ihr Beispiel für regex.isRange(..).followedBy(..) ist sowohl an die Syntax einer bestimmten Programmiersprache als auch an den objektorientierten Stil (Methodenverkettung) gekoppelt.

Wie würde dieser exakte 'Regex' zum Beispiel in C aussehen? Der Code müsste geändert werden.

Der "allgemeinste" Ansatz wäre, eine einfache, prägnante Sprache zu definieren, die dann ohne Änderung leicht in jede andere Sprache eingebettet werden kann. Und genau das sind (fast) Regex.

0
Aviv Cohn

Perl-kompatible reguläre Ausdrücke Engines sind weit verbreitet und bieten eine knappe Syntax für reguläre Ausdrücke, die viele Editoren und Sprachen verstehen. Wie @ JDługosz in den Kommentaren hervorhob, hat Perl 6 (nicht nur eine neue Version von Perl 5, sondern eine völlig andere Sprache) versucht, reguläre Ausdrücke lesbarer zu machen, indem sie aus individuell definierten Elementen aufgebaut wurden . Hier ist zum Beispiel eine Beispielgrammatik zum Parsen von URLs aus Wikibooks :

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

Durch Aufteilen des regulären Ausdrucks auf diese Weise kann jedes Bit einzeln definiert werden (z. B. Einschränkung von domain als alphanumerisch) oder durch Unterklassen erweitert werden (z. B. FileURL is URL, Dass nur Einschränkungen protocol gelten "file").

Also: Nein, es gibt keinen technischen Grund für die Knappheit regulärer Ausdrücke, aber neuere, sauberere und besser lesbare Darstellungsweisen gibt es bereits! Hoffentlich sehen wir einige neue Ideen in diesem Bereich.

0
Gaurav