web-dev-qa-db-fra.com

Y a-t-il une raison spécifique à la mauvaise lisibilité de la conception de la syntaxe des expressions régulières?

Les programmeurs semblent tous convenir que la lisibilité du code est beaucoup plus importante que les lignes simples à syntaxe courte qui fonctionnent, mais nécessitent un développeur senior pour interpréter avec un certain degré de précision - mais cela semble être exactement la façon dont les expressions régulières ont été conçues. Y avait-il une raison à cela?

Nous convenons tous que selfDocumentingMethodName() est bien meilleur que e(). Pourquoi cela ne devrait-il pas s'appliquer également aux expressions régulières?

Il me semble qu'au lieu de concevoir une syntaxe de logique monoligne sans organisation structurelle:

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

Et ce n'est même pas une analyse stricte d'une URL!

Au lieu de cela, nous pourrions rendre une structure de pipeline organisée et lisible, pour un exemple de base:

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

Quel avantage offre la syntaxe extrêmement concise d'une expression régulière autre que la syntaxe logique et de fonctionnement la plus courte possible? En fin de compte, y a-t-il une raison technique spécifique à la mauvaise lisibilité de la conception de la syntaxe des expressions régulières?

161
Viziionary

Il y a une grande raison pour laquelle les expressions régulières ont été conçues de manière aussi laconique qu'elles sont: elles ont été conçues pour être utilisées comme des commandes pour un éditeur de code, pas comme un langage à coder. Plus précisément, ed était l'un des premiers programmes à utiliser des expressions régulières, et à partir de là, les expressions régulières ont commencé leur conquête de la domination du monde. Par exemple, la commande edg/<regular expression>/p a bientôt inspiré un programme distinct appelé grep, qui est toujours utilisé aujourd'hui. En raison de leur puissance, ils ont ensuite été standardisés et utilisés dans une variété d'outils comme sed et vim

Mais assez pour le trivia. Alors, pourquoi cette origine favoriserait-elle une grammaire laconique? Parce que vous ne tapez pas une commande d'éditeur pour la lire même une fois de plus. Il suffit que vous vous souveniez comment l'assembler et que vous puissiez faire ce que vous voulez faire. Cependant, chaque caractère que vous devez taper ralentit votre progression dans l'édition de votre fichier. La syntaxe des expressions régulières a été conçue pour écrire des recherches relativement complexes de manière jetable, et c'est précisément ce qui donne aux gens des maux de tête qui les utilisent comme code pour analyser certaines entrées d'un programme.

179

L'expression régulière que vous citez est un terrible gâchis et je pense que personne n'est d'accord pour dire qu'elle est lisible. Dans le même temps, une grande partie de cette laideur est inhérente au problème résolu: il existe plusieurs couches d'imbrication et la grammaire des URL est relativement compliquée (certainement trop compliquée pour communiquer succinctement dans n'importe quelle langue). Cependant, il est certainement vrai qu'il existe de meilleures façons de décrire ce que cette expression régulière décrit. Alors pourquoi ne sont-ils pas utilisés?

Une grande raison est l'inertie et l'ubiquité. Cela n'explique pas comment ils sont devenus si populaires en premier lieu, mais maintenant qu'ils le sont, quiconque connaît les expressions régulières peut utiliser ces compétences (avec très peu de différences entre les dialectes) dans une centaine de langues différentes et un millier d'outils logiciels supplémentaires ( (éditeurs de texte et outils de ligne de commande, par exemple). Soit dit en passant, ce dernier n'utiliserait pas et ne pourrait pas utiliser de solution équivalant à écrire des programmes, car ils sont largement utilisés par des non-programmeurs.

Malgré cela, les expressions régulières sont souvent surutilisées, c'est-à-dire appliquées même lorsqu'un autre outil serait bien meilleur. Je ne pense pas que la syntaxe des regex soit terrible. Mais il est clairement bien meilleur pour les modèles courts et simples: l'exemple archétypal des identificateurs dans les langages de type C, [a-zA-Z_][a-zA-Z0-9_]* peut être lu avec un minimum absolu de connaissances regex et une fois que cette barre est remplie, elle est à la fois évidente et bien succincte. Exiger moins de caractères n'est pas intrinsèquement mauvais, bien au contraire. Être concis est une vertu à condition de rester compréhensible.

Il y a au moins deux raisons pour lesquelles cette syntaxe excelle dans des modèles simples comme ceux-ci: elle ne nécessite pas d'échappement pour la plupart des caractères, donc elle se lit relativement naturellement, et elle utilise toute la ponctuation disponible pour exprimer une variété de combinateurs d'analyse simples. Plus important encore, il ne nécessite pas rien du tout pour le séquençage. Vous écrivez la première chose, puis la chose qui vient après. Comparez cela avec votre followedBy, en particulier lorsque le modèle suivant est pas une expression littérale mais plus compliquée.

Alors pourquoi échouent-ils dans des cas plus compliqués? Je peux voir trois problèmes principaux:

  1. Il n'y a aucune capacité d'abstraction. Les grammaires formelles, qui proviennent du même domaine de l'informatique théorique que les expressions rationnelles, ont un ensemble de productions, afin qu'elles puissent donner des noms aux parties intermédiaires du modèle:

    # 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. Comme nous avons pu le voir ci-dessus, les espaces sans signification particulière sont utiles pour permettre un formatage plus agréable pour les yeux. Même chose avec les commentaires. Les expressions régulières ne peuvent pas faire cela, car un espace n'est que cela, un littéral ' '. Remarque cependant: certaines implémentations permettent un mode "verbeux" où les espaces sont ignorés et les commentaires sont possibles.

  3. Il n'y a pas de méta-langage pour décrire les modèles et les combinateurs courants. Par exemple, on peut écrire une règle digit une fois et continuer à l’utiliser dans une grammaire sans contexte, mais on ne peut pas définir une "fonction" pour ainsi dire à laquelle on donne une production p et crée un nouvelle production qui en fait quelque chose de plus, par exemple créer une production pour une liste d'occurrences de p séparées par des virgules.

L'approche que vous proposez résout certainement ces problèmes. Cela ne les résout tout simplement pas très bien, car il y est beaucoup plus concis que nécessaire. Les deux premiers problèmes peuvent être résolus tout en restant dans un langage spécifique au domaine relativement simple et concis. Le troisième, eh bien ... une solution programmatique nécessite un langage de programmation à usage général bien sûr, mais d'après mon expérience, le troisième est de loin le moindre de ces problèmes. Peu de modèles ont suffisamment d'occurrences de la même tâche complexe que le programmeur aspire à la capacité de définir de nouveaux combinateurs. Et lorsque cela est nécessaire, le langage est souvent suffisamment compliqué pour qu'il ne puisse et ne doive de toute façon pas être analysé avec des expressions régulières.

Il existe des solutions pour ces cas. Il y a environ dix mille bibliothèques de combinateurs d'analyseurs qui font à peu près ce que vous proposez, juste avec un ensemble d'opérations différent, souvent une syntaxe différente, et presque toujours avec plus de puissance d'analyse que les expressions régulières (c'est-à-dire qu'elles traitent des langages sans contexte ou certains de taille sous-ensemble de ceux-ci). Ensuite, il existe des générateurs d'analyseurs, qui vont avec l'approche "utiliser une meilleure DSL" décrite ci-dessus. Et il y a toujours la possibilité d'écrire une partie de l'analyse à la main, dans le bon code. Vous pouvez même mélanger et assortir, en utilisant des expressions régulières pour des sous-tâches simples et en faisant les choses compliquées dans le code en invoquant les expressions régulières.

Je ne connais pas assez les premières années de l'informatique pour expliquer comment les expressions régulières sont devenues si populaires. Mais ils sont là pour rester. Il suffit de les utiliser à bon escient, et pas de les utiliser quand c'est plus sage.

62
user7043

Perspective historique

L'article Wikipédia est assez détaillé sur les origines des expressions régulières (Kleene, 1956). La syntaxe d'origine était relativement simple avec seulement *, +, ?, | Et le regroupement (...). Il était laconique ( et lisible, les deux ne sont pas nécessairement opposés), car les langages formels ont tendance à s'exprimer avec des notations mathématiques laconiques.

Plus tard, la syntaxe et les capacités ont évolué avec les éditeurs et ont grandi avec Perl , qui essayait d'être concis par conception ( "les constructions courantes devraient être courtes" ). Cela complexifiait beaucoup la syntaxe, mais notez que les gens sont maintenant habitués aux expressions régulières et sont bons à les écrire (sinon à les lire). Le fait qu'ils soient parfois en écriture seule suggère que lorsqu'ils sont trop longs, ils ne sont généralement pas le bon outil. Les expressions régulières ont tendance à être illisibles lorsqu'elles sont maltraitées.

Au-delà des expressions régulières basées sur des chaînes

En parlant de syntaxes alternatives, jetons un coup d'œil à celle qui existe déjà ( cl-ppcre , dans Common LISP ). Votre expression régulière longue peut être analysée avec ppcre:parse-string Comme suit:

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

... et se présente sous la forme suivante:

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

Cette syntaxe est plus verbeuse, et si vous regardez les commentaires ci-dessous, pas nécessairement plus lisible. Alors ne supposez pas que parce que vous avez une syntaxe moins compacte, les choses seront automatiquement plus claires.

Cependant, si vous commencez à avoir des problèmes avec vos expressions régulières, les transformer en ce format peut vous aider à déchiffrer et déboguer votre code. C'est un avantage par rapport aux formats basés sur des chaînes, où une erreur de caractère unique peut être difficile à repérer. Le principal avantage de cette syntaxe est de manipuler des expressions régulières en utilisant un format structuré au lieu d'un encodage basé sur des chaînes. Cela vous permet de composer et construire de telles expressions comme toute autre structure de données dans votre programme. Lorsque j'utilise la syntaxe ci-dessus, c'est généralement parce que je veux construire des expressions à partir de parties plus petites (voir aussi ma réponse CodeGolf ). Pour votre exemple, nous pouvons écrire 1 :

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

Les expressions régulières basées sur des chaînes peuvent également être composées, en utilisant la concaténation et/ou l'interpolation de chaînes enveloppées dans des fonctions d'assistance. Cependant, il y a des limitations avec les manipulations de chaînes qui ont tendance à encombrer le code (pensez aux problèmes d'imbrication, un peu comme les backticks vs $(...) in bash; aussi , les personnages d'échappement peuvent vous donner des maux de tête).

Notez également que le formulaire ci-dessus autorise les formulaires (:regex "string") Afin que vous puissiez mélanger des notations laconiques avec des arbres. Tout cela conduit à mon humble avis une bonne lisibilité et composabilité; il aborde les trois problèmes exprimés par delnan , indirectement (c'est-à-dire pas dans le langage des expressions régulières lui-même).

De conclure

  • Dans la plupart des cas, la notation laconique est en fait lisible. Il existe des difficultés lorsqu'il s'agit de notations étendues qui impliquent un retour en arrière, etc., mais leur utilisation est rarement justifiée. L'utilisation injustifiée d'expressions régulières peut conduire à des expressions illisibles.

  • Les expressions régulières n'ont pas besoin d'être codées sous forme de chaînes. Si vous avez une bibliothèque ou un outil qui peut vous aider à construire et à composer des expressions régulières, vous éviter beaucoup de bugs potentiels liés aux manipulations de chaînes.

  • Alternativement, les grammaires formelles sont plus lisibles et sont meilleures pour nommer et résumer les sous-expressions. Les terminaux sont généralement exprimés comme de simples expressions régulières.


1. Vous pouvez préférer construire vos expressions au moment de la lecture, car les expressions régulières ont tendance à être des constantes dans une application. Voir create-scanner et load-time-value :

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

Le plus gros problème avec l'expression régulière n'est pas la syntaxe trop laconique, c'est que nous essayons d'exprimer une définition complexe dans une seule expression, au lieu de la composer à partir de blocs de construction plus petits. Ceci est similaire à la programmation où vous n'utilisez jamais de variables et de fonctions et placez plutôt votre code sur une seule ligne.

Comparez regex avec BNF . Sa syntaxe n'est pas beaucoup plus propre que l'expression régulière, mais elle est utilisée différemment. Vous commencez par définir des symboles nommés simples et les composez jusqu'à ce que vous arriviez à un symbole décrivant l'ensemble du motif que vous souhaitez faire correspondre.

Par exemple, regardez la syntaxe URI dans rfc3986 :

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

Vous pouvez écrire presque la même chose en utilisant une variante de la syntaxe regex qui prend en charge l'incorporation de sous-expressions nommées.


Personnellement, je pense qu'une syntaxe abrégée semblable à une expression rationnelle convient très bien pour les fonctionnalités couramment utilisées comme les classes de caractères, la concaténation, le choix ou la répétition, mais pour les fonctionnalités plus complexes et plus rares, comme les noms verbeux prospectifs sont préférables. Assez similaire à la façon dont nous utilisons des opérateurs comme + ou * en programmation normale et passez aux fonctions nommées pour des opérations plus rares.

25
CodesInChaos

selfDocumentingMethodName () est bien meilleur que e ()

est-ce? Il y a une raison pour laquelle la plupart des langues ont {et} comme délimiteurs de blocs plutôt que BEGIN et END.

Les gens aiment la lourdeur, et une fois que vous connaissez la syntaxe, une terminologie courte est meilleure. Imaginez votre exemple d'expression régulière si d (pour chiffre) était "chiffre", l'expression régulière serait encore plus horrible à lire. Si vous le rendiez plus facilement analysable avec des caractères de contrôle, il ressemblerait davantage à XML. Ni l'un ni l'autre ne sont aussi bons une fois que vous connaissez la syntaxe.

Cependant, pour répondre correctement à votre question, vous devez vous rendre compte que l'expression régulière vient du temps où le resserrement était obligatoire.Il est facile de penser qu'un document XML de 1 Mo n'est pas très important aujourd'hui, mais nous parlons de jours où 1 Mo était à peu près toute votre capacité de stockage. Il y avait aussi moins de langues utilisées à l'époque, et regex n'est pas à des millions de kilomètres de Perl ou C, donc la syntaxe serait familière aux programmeurs de l'époque qui seraient heureux d'apprendre la syntaxe. Il n'y avait donc aucune raison de le rendre plus verbeux.

12
gbjbaanb

Regex est comme des pièces lego. À première vue, vous voyez des pièces en plastique de formes différentes qui peuvent être assemblées. Vous pourriez penser qu'il n'y aurait pas trop de choses différentes que vous pouvez façonner, mais vous voyez alors les choses incroyables que font les autres et vous vous demandez simplement à quel point c'est un jouet incroyable.

Regex est comme des pièces lego. Il y a peu d'arguments qui peuvent être utilisés mais les enchaîner sous différentes formes formera des millions de modèles d'expression régulière différents qui peuvent être utilisés pour de nombreuses tâches compliquées.

Les gens utilisaient rarement seuls les paramètres regex. De nombreuses langues vous offrent des fonctions pour vérifier la longueur d'une chaîne ou en diviser les parties numériques. Vous pouvez utiliser des fonctions de chaîne pour découper des textes et les réformer. La puissance de l'expression régulière est remarquée lorsque vous utilisez des formulaires complexes pour effectuer des tâches complexes très spécifiques.

Vous pouvez trouver des dizaines de milliers de questions sur les expressions rationnelles sur SO et elles sont rarement marquées comme doublons. Cela seul montre les cas d'utilisation uniques possibles qui sont très différents les uns des autres.

Et il n'est pas facile d'offrir des méthodes prédéfinies pour gérer ces tâches uniques très différentes. Vous avez des fonctions de chaîne pour ce type de tâches, mais si ces fonctions ne suffisent pas pour votre tâche de spécifix, alors il est temps d'utiliser l'expression régulière

6
FallenAngel

Je reconnais que c'est un problème de pratique plutôt que de puissance. Le problème se pose généralement lorsque les expressions régulières sont directement implémentées, au lieu de supposer une nature composite. De même, un bon programmeur décomposera les fonctions de son programme en méthodes concises.

Par exemple, une chaîne d'expression régulière pour une URL peut être réduite d'environ:

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

à:

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

Les expressions régulières sont des choses astucieuses, mais elles sont sujettes aux abus de ceux qui deviennent absorbés par leur complexité apparente. Les expressions qui en résultent sont de la rhétorique, sans valeur à long terme.

2
toplel32

Comme le dit @cmaster, les regexps ont été initialement conçues pour être utilisées uniquement à la volée, et il est tout simplement bizarre (et légèrement déprimant) que la syntaxe de bruit de ligne soit toujours la plus populaire. Les seules explications auxquelles je peux penser concernent soit l'inertie, le masochisme ou le machisme (ce n'est pas souvent que "l'inertie" est la raison la plus attrayante pour faire quelque chose ...)

Perl fait une tentative plutôt faible pour les rendre plus lisibles en autorisant les espaces et les commentaires, mais ne fait rien d'imaginatif à distance.

Il existe d'autres syntaxes. Une bonne est la syntaxe scsh pour les regexps , qui selon mon expérience produit des regexps qui sont raisonnablement faciles à taper, mais toujours lisibles après coup.

[ scsh est splendide pour d'autres raisons, dont l'une est sa célèbre texte de remerciements ]

0
Norman Gray

Je crois que les expressions régulières ont été conçues pour être aussi "générales" et simples que possible, afin qu'elles puissent être utilisées (à peu près) de la même manière n'importe où.

Votre exemple de regex.isRange(..).followedBy(..) est couplé à la fois à la syntaxe d'un langage de programmation spécifique et peut-être au style orienté objet (chaînage de méthode).

À quoi ressemblerait cette expression régulière exacte en C par exemple? Le code devrait être changé.

L'approche la plus "générale" serait de définir un langage simple et concis qui peut ensuite être facilement intégré dans n'importe quel autre langage sans changement. Et c'est (presque) ce que sont les regex.

0
Aviv Cohn

Expression régulière compatible Perl les moteurs sont largement utilisés, fournissant une syntaxe d'expression régulière laconique que de nombreux éditeurs et langues comprennent. Comme @ JDługosz l'a souligné dans les commentaires, Perl 6 (pas seulement une nouvelle version de Perl 5, mais un langage complètement différent) a tenté de rendre les expressions régulières plus lisibles en les construisant à partir d'éléments définis individuellement . Par exemple, voici un exemple de grammaire pour l'analyse des URL de Wikibooks :

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

Le fractionnement de l'expression régulière comme ceci permet à chaque bit d'être défini individuellement (par exemple, contraindre domain à être alphanumérique) ou étendu par le biais de sous-classes (par exemple FileURL is URL que les contraintes protocol ne doivent être que "file").

Donc: non, il n'y a pas de raison technique à la lourdeur des expressions régulières, mais des façons plus récentes, plus propres et plus lisibles de les représenter sont déjà là! J'espère donc que nous verrons de nouvelles idées dans ce domaine.

0
Gaurav