C'è una ragione specifica per la scarsa leggibilità della progettazione della sintassi delle espressioni regolari?

159

I programmatori sembrano tutti d'accordo sul fatto che la leggibilità del codice sia molto più importante delle one-liner con sintassi breve che funzionano, ma richiedono uno sviluppatore senior di interpretare con qualsiasi grado di precisione - ma sembra essere esattamente il modo in cui le espressioni regolari sono state progettato. C'era una ragione per questo?

Siamo tutti d'accordo sul fatto che selfDocumentingMethodName() è di gran lunga migliore di e() . Perché non dovrebbe valere anche per le espressioni regolari?

Mi sembra piuttosto che progettare una sintassi della logica a una riga senza organizzazione strutturale:

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

E questo non è nemmeno l'analisi rigorosa di un URL!

Potremmo invece rendere una struttura della pipeline organizzata e leggibile, per un esempio di base:

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

Quale vantaggio offre la sintassi estremamente concisa di un'espressione regolare diversa dall'operazione e dalla sintassi logica più brevi possibili? In definitiva, esiste una ragione tecnica specifica per la scarsa leggibilità della progettazione della sintassi delle espressioni regolari?

    
posta Viziionary 29.09.2015 - 18:57
fonte

10 risposte

177

C'è una grande ragione per cui le espressioni regolari sono state progettate in modo così preciso: sono state progettate per essere usate come comandi per un editor di codice, non come linguaggio da codificare. Più precisamente, ed era uno dei i primi programmi ad usare espressioni regolari, e da lì le espressioni regolari hanno iniziato la loro conquista per il dominio del mondo. Ad esempio, il comando ed g/<regular expression>/p ha presto ispirato un programma separato chiamato grep , che è ancora in uso oggi. A causa del loro potere, in seguito sono stati standardizzati e utilizzati in una varietà di strumenti come sed e vim

Ma abbastanza per la curiosità. Quindi, perché questa origine favorirebbe una grammatica concisa? Perché non scrivi un comando di editor per leggerlo ancora una volta. È sufficiente che tu ricordi come metterlo insieme e che tu possa fare quello che vuoi fare. Tuttavia, ogni carattere che devi digitare rallenta i tuoi progressi modificando il tuo file. La sintassi delle espressioni regolari è stata progettata per scrivere ricerche relativamente complesse in modalità throw-away, ed è proprio questo che dà alle persone mal di testa che le usano come codice per analizzare alcuni input di un programma.

    
risposta data 29.09.2015 - 21:09
fonte
62

L'espressione regolare che citi è un pasticcio terribile e non credo che nessuno sia d'accordo sul fatto che sia leggibile. Allo stesso tempo, gran parte di questa bruttezza è inerente al problema da risolvere: ci sono diversi livelli di nidificazione e la grammatica dell'URL è relativamente complicata (certamente troppo complicata per comunicare in modo succinto in qualsiasi lingua). Tuttavia, è certamente vero che ci sono modi migliori per descrivere cosa sta descrivendo questa regex. Quindi perché non vengono utilizzati?

Una grande ragione è l'inerzia e l'ubiquità. Non spiega come siano diventati così popolari in primo luogo, ma ora che lo sono, chiunque conosca le espressioni regolari può usare queste abilità (con pochissime differenze tra i dialetti) in un centinaio di lingue diverse e altri mille strumenti software ( ad esempio, editor di testo e strumenti da riga di comando). Tra l'altro, quest'ultimo non avrebbe né potuto utilizzare alcuna soluzione che equivalesse a scrivere programmi , perché sono pesantemente utilizzati dai non programmatori.

Nonostante ciò, le espressioni regolari sono spesso abusate, cioè applicate anche quando un altro strumento sarebbe molto meglio. Non credo che la sintassi regex sia terribile . Ma è chiaramente molto meglio nei pattern brevi e semplici: l'archetipo di identificatori nei linguaggi C-like, [a-zA-Z_][a-zA-Z0-9_]* può essere letto con un minimo assoluto di conoscenza regex e una volta che la barra è soddisfatta, è ovvia e ben sintetica. Richiedere meno caratteri non è intrinsecamente cattivo, anzi. Essere concisi è una virtù purché tu rimanga comprensibile.

Ci sono almeno due ragioni per cui questa sintassi eccelle per schemi semplici come questi: Non richiede l'escape per la maggior parte dei caratteri, quindi si legge in modo relativamente naturale e utilizza tutta la punteggiatura disponibile per esprimere una varietà di semplici combinatori di parsing. Forse, cosa più importante, non richiede nulla per il sequenziamento. Scrivi la prima cosa, poi la cosa che viene dopo. Contrasta con il tuo followedBy , specialmente quando il seguente schema è non un'espressione letterale ma più complicata.

Quindi perché non riescono a ricorrere a casi più complicati? Posso vedere tre problemi principali:

  1. Non ci sono capacità di astrazione. Le grammatiche formali, che provengono dallo stesso campo dell'informatica teorica come regex, hanno una serie di produzioni, in modo che possano dare nomi alle parti intermedie del modello:

    # 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. Come abbiamo visto sopra, lo spazio bianco non ha un significato speciale è utile per consentire una formattazione più semplice per gli occhi. Stessa cosa con i commenti. Le espressioni regolari non possono farlo perché uno spazio è proprio questo, un valore letterale ' ' . Nota però: alcune implementazioni consentono una modalità "verbose" in cui gli spazi vengono ignorati e i commenti sono possibili.

  3. Non esiste un meta-linguaggio per descrivere schemi e combinatori comuni. Ad esempio, si può scrivere una regola digit una volta e continuare ad usarla in una grammatica libera dal contesto, ma non si può definire una "funzione" per così dire che viene data una produzione p e crea una nuova produzione che fa qualcosa in più con esso, ad esempio, crea una produzione per un elenco separato da virgole di occorrenze di p .

L'approccio che proponi risolve certamente questi problemi. Semplicemente non li risolve molto bene, perché commercia in una concisione molto più concreta del necessario. I primi due problemi possono essere risolti rimanendo all'interno di un linguaggio specifico del dominio relativamente semplice e laconico. La terza, beh ... una soluzione programmatica richiede ovviamente un linguaggio di programmazione generico, ma nella mia esperienza il terzo è di gran lunga l'ultimo di questi problemi. Pochi modelli hanno abbastanza occorrenze dello stesso compito complesso che il programmatore anela alla capacità di definire nuovi combinatori. E quando ciò è necessario, la lingua è spesso abbastanza complicata da non poter e non deve essere analizzata con espressioni regolari comunque.

Esistono soluzioni per questi casi. Ci sono circa diecimila librerie di combinatori di parser che fanno grosso modo ciò che proponi, solo con un diverso insieme di operazioni, spesso sintassi diversa, e quasi sempre con più potere di parsing rispetto alle espressioni regolari (cioè si occupano di linguaggi context-free o di dimensioni considerevoli sottoinsieme di quelli). Poi ci sono i generatori di parser, che vanno con l'approccio "usa una migliore DSL" descritto sopra. E c'è sempre la possibilità di scrivere parte del parsing a mano, nel codice corretto. Puoi persino mescolare e abbinare, usando espressioni regolari per semplici sotto-attività e facendo le cose complicate nel codice che richiama le regex.

Non ne so abbastanza dei primi anni dell'informatica per spiegare come le espressioni regolari siano diventate così popolari. Ma sono qui per restare. Devi solo usarli con saggezza e non usarli quando è più saggio.

    
risposta data 29.09.2015 - 19:53
fonte
39

Prospettiva storica

L'articolo di Wikipedia è abbastanza dettagliato sulle origini delle espressioni regolari (Kleene, 1956). La sintassi originale era relativamente semplice con solo * , + , ? , | e il raggruppamento (...) . Era terso ( e leggibile, i due non sono necessariamente opposti), perché i linguaggi formali tendono ad essere espressi con notazioni matematiche concise.

Successivamente, la sintassi e le funzionalità si sono evolute con gli editor e sono cresciute con Perl , che cercava di essere conciso in base al design ( "le costruzioni comuni dovrebbero essere brevi" ). Questo ha complicato molto la sintassi, ma si noti che le persone sono ora abituate alle espressioni regolari e sono brave a scrivere (se non a leggerle). Il fatto che siano a volte solo di scrittura suggeriscono che quando sono troppo lunghi, in genere non sono lo strumento giusto. Le espressioni regolari tendono a essere illeggibili quando vengono utilizzate abusivamente.

Al di là delle espressioni regolari basate su stringhe

Parlando di sintassi alternative, diamo un'occhiata a quella già esistente ( cl-ppcre , in Common Lisp ). La tua lunga espressione regolare può essere analizzata con ppcre:parse-string come segue:

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

... e restituisce il seguente modulo:

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

Questa sintassi è più prolissa e se si guardano i commenti sotto, non necessariamente più leggibili. Quindi non dare per scontato che, poiché hai una sintassi meno compatta, le cose saranno automaticamente più chiare .

Tuttavia, se inizi a riscontrare problemi con le espressioni regolari, trasformarle in questo formato potrebbe aiutarti a decifrare e eseguire il debug del codice. Questo è uno dei vantaggi rispetto ai formati basati su stringhe, in cui un errore di singolo carattere può essere difficile da individuare. Il vantaggio principale di questa sintassi è di manipolare le espressioni regolari utilizzando un formato strutturato anziché una codifica basata su stringhe. Ciò ti consente di comporre e creare tali espressioni come qualsiasi altra struttura di dati nel tuo programma. Quando uso la sintassi sopra, questo è generalmente perché voglio creare espressioni da parti più piccole (vedi anche mia risposta CodeGolf ). Per il tuo esempio, potremmo scrivere 1 :

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

Le espressioni regolari basate su stringhe possono anche essere composte, usando la concatenazione di stringhe o l'interpolazione racchiusa nelle funzioni di supporto. Tuttavia, ci sono limitazioni con le manipolazioni di stringhe che tendono a ingombrare il code (pensa ai problemi di annidamento, non diversamente dai backtick rispetto a $(...) in bash; inoltre, i caratteri di escape possono darti mal di testa).

Nota anche che il modulo sopra permette forme (:regex "string") in modo da poter mescolare le notazioni concise con gli alberi. Tutto ciò porta IMHO a una buona leggibilità e componibilità; indirizza i tre problemi espressi da delnan , indirettamente (cioè non nella lingua delle stesse espressioni regolari).

Per concludere

  • Per la maggior parte degli scopi, la notazione tersa è in realtà leggibile. Ci sono difficoltà quando si ha a che fare con notazioni estese che implicano il backtracking, ecc., Ma il loro uso è raramente giustificato. L'uso ingiustificato di espressioni regolari può portare a espressioni illeggibili.

  • Le espressioni regolari non devono essere codificate come stringhe. Se hai una libreria o uno strumento che ti può aiutare a costruire e comporre espressioni regolari, eviti molti potenziali bug relativi alle manipolazioni delle stringhe.

  • In alternativa, le grammatiche formali sono più leggibili e sono migliori nel nominare e astrarre le sotto-espressioni. I terminali sono generalmente espressi come semplici espressioni regolari.

1. Potresti preferire creare le espressioni in fase di lettura, poiché le espressioni regolari tendono a essere costanti in un'applicazione. Vedi create-scanner e load-time-value :

'(:sequence :start-anchor #.(protocol) #.(slashes) ... )
    
risposta data 29.09.2015 - 20:07
fonte
25

Il più grande problema con regex non è la sintassi troppo concisa, è che cerchiamo di esprimere una definizione complessa in una singola espressione, invece di comporla da blocchi più piccoli. Questo è simile alla programmazione in cui non si usano mai variabili e funzioni e invece si incorpora il proprio codice in un'unica riga.

Confronta regex con BNF . La sua sintassi non è molto più pulita della regex, ma è usata in modo diverso. Si inizia definendo semplici simboli con nome e componendoli fino ad arrivare a un simbolo che descrive l'intero pattern che si desidera abbinare.

Ad esempio, guarda la sintassi URI in rfc3986 :

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

Potresti scrivere quasi la stessa cosa usando una variante della sintassi regex che supporta l'incorporamento di sottoespressioni nominate.

Personalmente ritengo che una regex tergente come la sintassi vada bene per le funzioni comunemente utilizzate come classi di caratteri, concatenazione, scelta o ripetizione, ma per le caratteristiche più complesse e rare come i nomi verbali di look-ahead sono preferibili. Abbastanza simile a come usiamo operatori come + o * nella normale programmazione e passiamo a funzioni con nome per operazioni più rare.

    
risposta data 30.09.2015 - 08:51
fonte
12

selfDocumentingMethodName() is far better than e()

è? C'è un motivo per cui molti linguaggi hanno {e} come delimitatori di blocco piuttosto che BEGIN e END.

Alle persone piace la chiarezza e una volta che conosci la sintassi, la terminologia breve è migliore. Immagina il tuo esempio di regex se d (per digit) fosse 'digit' la regex sarebbe ancora più orribile da leggere. Se lo rendessi più facilmente analizzabile con i caratteri di controllo, sarebbe più simile all'XML. Nessuno dei due è buono una volta che conosci la sintassi.

Per rispondere correttamente alla tua domanda, devi capire che la regex viene dai giorni in cui la tersezza era obbligatoria. È facile pensare che un documento XML da 1 MB non sia un grosso problema oggi, ma stiamo parlando di giorni in cui 1 MB era praticamente la tua intera capacità di archiviazione. C'erano anche meno lingue usate allora, e regex non è un milione di miglia lontano da Perl o C, quindi la sintassi sarebbe familiare ai programmatori del giorno che sarebbero felici di apprendere la sintassi. Quindi non c'era motivo di renderlo più dettagliato.

    
risposta data 30.09.2015 - 09:43
fonte
6

Regex è come pezzi di lego. A prima vista, si vedono alcune parti in plastica di forma diversa che possono essere unite. Potresti pensare che non ci siano troppe cose possibili che puoi modellare ma poi vedi le cose incredibili che fanno gli altri e ti chiedi solo come sia un giocattolo incredibile.

Regex è come pezzi di lego. Esistono pochi argomenti che possono essere utilizzati, ma il concatenarli in forme diverse formerà milioni di pattern regex diversi che possono essere utilizzati per molte attività complicate.

Le persone usavano raramente i parametri regex da soli. Molte lingue offrono funzioni per controllare la lunghezza di una stringa o dividere le parti numeriche da essa. È possibile utilizzare le funzioni di stringa per tagliare i testi e riformarli. La potenza della regex viene notata quando si utilizzano forme complesse per svolgere compiti complessi molto specifici.

Puoi trovare decine di migliaia di domande regex su SO e raramente contrassegnati come duplicati. Solo questo mostra i possibili casi d'uso unici che sono molto diversi l'uno dall'altro.

E non è facile offrire metodi predefiniti per gestire compiti molto diversi. Hai funzioni di stringa per questo tipo di attività, ma se quelle funzioni non sono sufficienti per l'attività specifica, allora è il momento di usare regex

    
risposta data 30.09.2015 - 09:41
fonte
2

Riconosco che questo è un problema di pratica piuttosto che di potenza. Il problema di solito sorge quando le espressioni regolari sono direttamente implementate, invece di assumere una natura composita. Allo stesso modo, un buon programmatore decomporrà le funzioni del suo programma in metodi concisi.

Ad esempio, una stringa di espressioni regolari per un URL può essere ridotta da circa:

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

a:

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

Le espressioni regolari sono cose belle, ma sono soggette ad abusi da parte di coloro che diventano assorbiti nella loro apparente complessità. Le espressioni risultanti sono retoriche, assenti in un valore a lungo termine.

    
risposta data 30.09.2015 - 10:53
fonte
0

Come dice @cmaster, le regex sono state originariamente progettate per essere utilizzate solo al volo, ed è semplicemente bizzarra (e leggermente deprimente) che la sintassi del rumore di linea è ancora la più popolare. Le uniche spiegazioni che riesco a pensare riguardano sia l'inerzia, il masochismo o il machismo (non è spesso che "l'inerzia" sia la ragione più interessante per fare qualcosa ...)

Perl fa un tentativo piuttosto debole di renderli più leggibili consentendo spazi e commenti, ma non fa nulla di lontanamente fantasioso.

Ci sono altre sintassi. Una buona è la sintassi scsh per le espressioni regolari , che nella mia esperienza produce espressioni regolari ragionevolmente facili digitare, ma comunque leggibile dopo il fatto.

[ scsh è splendido per altri motivi, uno dei quali è il suo famoso testo dei commenti ]

    
risposta data 29.09.2015 - 22:31
fonte
0

Credo che le espressioni regolari siano state progettate per essere il più possibile "generali" e semplici, in modo che possano essere utilizzate (approssimativamente) nello stesso modo ovunque.

Il tuo esempio di regex.isRange(..).followedBy(..) è abbinato sia alla sintassi di un linguaggio di programmazione specifico che a uno stile orientato agli oggetti (concatenamento di metodi).

Come sarebbe esattamente questa espressione "regex" in C? Il codice dovrebbe essere cambiato.

L'approccio più "generale" sarebbe quello di definire un linguaggio semplice e conciso che possa essere facilmente incorporato in qualsiasi altra lingua senza modifiche. E questo è (quasi) ciò che è regex.

    
risposta data 30.09.2015 - 15:07
fonte
0

Espressione regolare compatibile con Perl I motori sono ampiamente utilizzati, fornendo una sintassi di espressione regolare e concisa che molti editori e lingue comprendono. Come @ JDługosz ha sottolineato nei commenti, Perl 6 (non solo una nuova versione di Perl 5, ma una lingua completamente diversa) ha tentato di rendere più leggibili le espressioni regolari costruendole da elementi definiti individualmente. Ad esempio, ecco un esempio di grammatica per l'analisi degli URL di Wikibooks :

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

Suddividere l'espressione regolare come questa consente a ciascun bit di essere definito individualmente (ad esempio vincolando domain ad essere alfanumerico) o esteso tramite sottoclasse (ad esempio FileURL is URL che costringe protocol a essere solo "file" ).

Quindi: no, non c'è una ragione tecnica per la tenacia delle espressioni regolari, ma i modi più nuovi, più puliti e più leggibili per rappresentarli sono già qui! Quindi speriamo di vedere alcune nuove idee in questo campo.

    
risposta data 07.09.2016 - 23:48
fonte

Leggi altre domande sui tag