Existe uma razão específica para a fraca legibilidade do design de sintaxe de expressão regular?

159

Os programadores parecem concordar que a legibilidade do código é muito mais importante do que os one-liners de sintaxe curta que funcionam, mas exigem que um desenvolvedor sênior interprete com algum grau de precisão - mas parece ser exatamente assim que as expressões regulares projetado. Houve algum motivo para isso?

Todos concordamos que selfDocumentingMethodName() é muito melhor que e() . Por que isso não se aplica a expressões regulares também?

Parece-me que, em vez de criar uma sintaxe de lógica de uma linha sem organização estrutural:

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

E isso nem é uma análise rigorosa de um URL!

Em vez disso, poderíamos criar uma estrutura de pipeline organizada e legível, para um exemplo básico:

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

Qual é a vantagem da sintaxe extremamente concisa de uma expressão regular além da operação e da sintaxe lógica mais curtas? Em última análise, existe uma razão técnica específica para a fraca legibilidade do design de sintaxe de expressão regular?

    
por Viziionary 29.09.2015 / 18:57
fonte

10 respostas

177

Há uma grande razão pela qual expressões regulares foram projetadas tão concisas quanto: elas foram projetadas para serem usadas como comandos em um editor de código, não como uma linguagem para codificar. Mais precisamente, ed era um dos primeiros programas a usar expressões regulares, e a partir daí expressões regulares iniciaram sua conquista pela dominação mundial. Por exemplo, o ed command g/<regular expression>/p logo inspirou um programa separado chamado grep , que ainda está em uso hoje. Por causa de seu poder, eles foram padronizados e usados em uma variedade de ferramentas como sed e vim

Mas o suficiente para as trivialidades. Então, por que essa origem favorece uma gramática concisa? Porque você não digita um comando de editor para ler mais uma vez. Basta que você se lembre de como juntá-lo e de que possa fazer o que quiser. No entanto, cada caractere que você precisar digitar diminui o andamento da edição do arquivo. A sintaxe da expressão regular foi projetada para escrever pesquisas relativamente complexas de uma forma descartável, e é precisamente isso que dá às pessoas dores de cabeça que as usam como código para analisar alguma entrada em um programa.

    
por 29.09.2015 / 21:09
fonte
62

A expressão regular que você cita é uma bagunça terrível e eu não acho que alguém concorda que é legível. Ao mesmo tempo, muito dessa fealdade é inerente ao problema que está sendo resolvido: Existem várias camadas de aninhamento e a gramática de URL é relativamente complicada (certamente muito complicada para se comunicar sucintamente em qualquer idioma). No entanto, é certamente verdade que existem maneiras melhores de descrever o que este regex está descrevendo. Então, por que eles não são usados?

Um grande motivo é inércia e onipresença. Não explica como eles se tornaram tão populares em primeiro lugar, mas agora que eles são, qualquer um que sabe expressões regulares pode usar essas habilidades (com poucas diferenças entre dialetos) em cem idiomas diferentes e um adicional de mil ferramentas de software ( por exemplo, editores de texto e ferramentas de linha de comando). By the way, este último não e não poderia usar qualquer solução que equivale a escrever programas , porque eles são muito utilizados por não-programadores.

Apesar disso, expressões regulares são freqüentemente usadas em demasia, isto é, aplicadas mesmo quando outra ferramenta seria muito melhor. Eu não acho que a sintaxe de regex seja terrível . Mas é claramente muito melhor em padrões curtos e simples: O exemplo arquetípico de identificadores em linguagens semelhantes a C, [a-zA-Z_][a-zA-Z0-9_]* pode ser lido com um mínimo absoluto de conhecimento de regex e uma vez que a barra é atendida é óbvia e bem sucinta. Exigir menos caracteres não é inerentemente ruim, muito pelo contrário. Ser conciso é uma virtude, desde que você permaneça compreensível.

Existem pelo menos dois motivos pelos quais essa sintaxe se destaca em padrões simples como estes: ela não requer escape para a maioria dos caracteres, por isso é lida de maneira relativamente natural e usa toda a pontuação disponível para expressar uma variedade de combinadores simples de análise. Talvez o mais importante, não requer qualquer coisa para o sequenciamento. Você escreve a primeira coisa, depois a coisa que vem depois. Compare isso com seu followedBy , especialmente quando o seguinte padrão não for literal, mas uma expressão mais complicada.

Então, por que eles ficam aquém em casos mais complicados? Eu posso ver três problemas principais:

  1. Não há recursos de abstração. As gramáticas formais, que se originam do mesmo campo da ciência da computação teórica como regexes, têm um conjunto de produções, de modo que podem dar nomes a partes intermediárias do padrão:

    # 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. Como pudemos ver acima, o espaço em branco sem significado especial é útil para permitir a formatação que é mais fácil para os olhos. Mesma coisa com comentários. Expressões regulares não podem fazer isso porque um espaço é apenas isso, um literal ' ' . Nota: algumas implementações permitem um modo "verboso" onde o espaço em branco é ignorado e os comentários são possíveis.

  3. Não existe uma meta-linguagem para descrever padrões e combinadores comuns. Por exemplo, pode-se escrever uma regra digit uma vez e continuar usando-a em uma gramática livre de contexto, mas não é possível definir uma "função", por assim dizer, que recebe uma produção p e cria uma nova produção que faz algo extra com ele, por exemplo, crie uma produção para uma lista separada por vírgulas de ocorrências de p .

A abordagem que você propõe certamente resolve esses problemas. Simplesmente não os resolve muito bem, porque negocia com muito mais concisão do que é necessário. Os dois primeiros problemas podem ser resolvidos permanecendo dentro de uma linguagem específica do domínio relativamente simples e concisa. A terceira, bem ... uma solução programática requer uma linguagem de programação de propósito geral, é claro, mas na minha experiência a terceira é de longe o menor desses problemas. Poucos padrões têm ocorrências suficientes da mesma tarefa complexa que o programador anseia pela capacidade de definir novos combinadores. E quando isso é necessário, a linguagem geralmente é complicada o suficiente para que não possa e não deva ser analisada com expressões regulares de qualquer maneira.

Soluções para esses casos existem. Existem aproximadamente dez mil bibliotecas de combinadores de analisador que fazem praticamente o que você propõe, apenas com um conjunto diferente de operações, muitas vezes com sintaxe diferente e quase sempre com mais poder de análise que expressões regulares (ou seja, lidam com linguagens livres de contexto ou subconjunto desses). Em seguida, há geradores de analisador, que seguem a abordagem "usar melhor DSL" descrita acima. E há sempre a opção de escrever um pouco da análise manual, no código adequado. Você pode até misturar e combinar, usando expressões regulares para sub-tarefas simples e fazendo as coisas complicadas no código invocando os regexes.

Eu não sei o suficiente sobre os primeiros anos da computação para explicar como expressões regulares se tornaram tão populares. Mas eles estão aqui para ficar. Você só tem que usá-los sabiamente, e não usá-los quando isso é mais sábio.

    
por 29.09.2015 / 19:53
fonte
39

Perspectiva histórica

O artigo da Wikipedia é bastante detalhado sobre as origens das expressões regulares (Kleene, 1956). A sintaxe original era relativamente simples, com apenas * , + , ? , | e agrupamento (...) . Foi conciso ( e legível, os dois não são necessariamente opostos), porque as línguas formais tendem a ser expressas com notações matemáticas concisas.

Mais tarde, a sintaxe e os recursos evoluíram com editores e cresceram com o Perl , que estava tentando ser conciso pelo design ( "construções comuns devem ser curtas" ). Isso complexificou muito a sintaxe, mas observe que as pessoas agora estão acostumadas a expressões regulares e são boas em escrever (se não as lerem). O fato de que às vezes eles são escritos apenas sugere que, quando são muito longos, geralmente não são a ferramenta certa. Expressões regulares tendem a ser ilegíveis quando são abusadas.

Além das expressões regulares baseadas em string

Falando sobre sintaxes alternativas, vamos dar uma olhada em uma que já existe ( cl-ppcre , em Common Lisp ). Sua longa expressão regular pode ser analisada com ppcre:parse-string da seguinte maneira:

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

... e resulta da seguinte forma:

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

Esta sintaxe é mais detalhada e, se você observar os comentários abaixo, não é necessariamente mais legível. Então não assuma que porque você tem uma sintaxe menos compacta, as coisas serão automaticamente mais claras .

No entanto, se você começar a ter problemas com suas expressões regulares, transformá-las nesse formato poderá ajudá-lo a decifrar e depurar seu código. Essa é uma vantagem sobre os formatos baseados em seqüência de caracteres, em que um erro de caractere único pode ser difícil de detectar. A principal vantagem desta sintaxe é manipular expressões regulares usando um formato estruturado em vez de uma codificação baseada em string. Isso permite a você compor e construir expressões como qualquer outra estrutura de dados em seu programa. Quando eu uso a sintaxe acima, geralmente é porque eu quero construir expressões de partes menores (veja também minha resposta ao CodeGolf ). Para o seu exemplo, podemos escrever 1 :

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

Expressões regulares baseadas em strings também podem ser compostas, usando concatenação de strings e / ou interpolações envolvidas em funções auxiliares. No entanto, existem limitações nas manipulações de strings que tendem a a desordem do code (pense em problemas de aninhamento, não diferente de backticks vs. $(...) no bash; além disso, caracteres de escape podem causar dores de cabeça).

Note também que o formulário acima permite (:regex "string") forms para que você possa misturar notações com árvores. Tudo isso leva a IMHO a uma boa legibilidade e composibilidade; ele aborda os três problemas expressos por delnan , indiretamente (ou seja, não na própria linguagem das expressões regulares).

Para concluir

  • Para o propósito, a notação concisa é de fato legível. Há dificuldades ao lidar com notações estendidas que envolvem retrocessos, etc., mas raramente são justificadas. O uso indevido de expressões regulares pode levar a expressões ilegíveis.

  • Expressões regulares não precisam ser codificadas como strings. Se você tiver uma biblioteca ou uma ferramenta que possa ajudá-lo a criar e compor expressões regulares, você evitará muitos bugs em potencial relacionados a manipulações de strings.

  • Como alternativa, as gramáticas formais são mais legíveis e são melhores para nomear e abstrair subexpressões. Os terminais são geralmente expressos como expressões regulares simples.

1. Você pode preferir criar suas expressões em tempo de leitura, porque expressões regulares tendem a ser constantes em um aplicativo. Consulte create-scanner e load-time-value :

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

O maior problema com o regex não é a sintaxe excessivamente concisa, é que tentamos expressar uma definição complexa em uma única expressão, em vez de escrevê-la a partir de blocos de construção menores. Isso é semelhante à programação em que você nunca usa variáveis e funções e, em vez disso, insere seu código em uma única linha.

Compare regex com BNF . Sua sintaxe não é muito mais limpa que a regex, mas é usada de forma diferente. Você começa definindo símbolos nomeados simples e os compõe até chegar a um símbolo que descreve todo o padrão que você deseja combinar.

Por exemplo, veja a sintaxe do URI em rfc3986 :

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

Você pode escrever quase a mesma coisa usando uma variante da sintaxe regex que suporte a incorporação de sub-expressões nomeadas.

Pessoalmente, acho que um regex conciso como a sintaxe é bom para recursos comumente usados, como classes de caracteres, concatenação, escolha ou repetição, mas para recursos mais complexos e mais raros, como nomes verbais de look-ahead são preferíveis. Bastante semelhante a como usamos operadores como + ou * na programação normal e alternamos para funções nomeadas para operações mais raras.

    
por 30.09.2015 / 08:51
fonte
12

selfDocumentingMethodName() is far better than e()

é isso? Há uma razão pela qual a maioria dos idiomas tem {e} como delimitadores de bloco em vez de BEGIN e END.

As pessoas gostam de terseness, e uma vez que você conhece a sintaxe, terminologia curta é melhor. Imagine o seu exemplo de regex se d (para dígito) fosse 'dígito' o regex seria ainda mais horrível de ler. Se você fizesse isso mais facilmente interpretável com caracteres de controle, então seria mais parecido com XML. Nem são tão bons quando você conhece a sintaxe.

Para responder a sua pergunta corretamente, você tem que perceber que a regex vem dos dias em que a clareza era obrigatória. É fácil pensar que um documento XML de 1 MB não é grande coisa hoje, mas estamos falando de dias em que 1 MB foi praticamente toda a sua capacidade de armazenamento. Havia também menos idiomas usados naquela época, e o regex não está a um milhão de milhas de distância de perl ou C, então a sintaxe seria familiar aos programadores do dia que ficariam felizes em aprender a sintaxe. Então não havia razão para torná-lo mais detalhado.

    
por 30.09.2015 / 09:43
fonte
6

O Regex é como peças lego. À primeira vista, você vê algumas peças de plástico de formas diferentes que podem ser unidas. Você pode pensar que não haveria muitas coisas diferentes possíveis que você pode moldar, mas então você vê as coisas incríveis que outras pessoas fazem e você só quer saber como é um brinquedo incrível.

O Regex é como peças lego. Existem poucos argumentos que podem ser usados, mas encadeando-os de formas diferentes formarão milhões de padrões de regex diferentes que podem ser usados para muitas tarefas complicadas.

As pessoas raramente usavam parâmetros de regex sozinho. Muitas linguagens oferecem funções para verificar o tamanho de uma string ou dividir as partes numéricas. Você pode usar funções de string para cortar textos e reformá-los. O poder de regex é percebido quando você usa formulários complexos para executar tarefas complexas muito específicas.

Você pode encontrar dezenas de milhares de perguntas de regex no SO e elas raramente são marcadas como duplicadas. Isso por si só mostra os possíveis casos de uso únicos que são muito diferentes uns dos outros.

E não é fácil oferecer métodos predefinidos para lidar com essas tarefas únicas muito diferentes. Você tem funções de string para esse tipo de tarefa, mas se essas funções não forem suficientes para sua tarefa specifix, então é hora de usar o regex

    
por 30.09.2015 / 09:41
fonte
2

Eu reconheço que isso é um problema da prática e não da potência. O problema geralmente surge quando expressões regulares são implementadas diretamente, ao invés de assumir uma natureza composta. Da mesma forma, um bom programador irá decompor as funções do seu programa em métodos concisos.

Por exemplo, uma string regex para um URL pode ser reduzida de aproximadamente:

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

para:

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

Expressões regulares são coisas bacanas, mas elas são propensas a serem abusadas por aqueles que se tornam absorvidos em sua aparente complexidade. As expressões resultantes são retóricas, ausentes de um valor a longo prazo.

    
por 30.09.2015 / 10:53
fonte
0

Como dizem @cmaster, os regexps foram originalmente projetados para serem usados apenas na hora, e é simplesmente bizarro (e um pouco deprimente) que a sintaxe de ruído de linha ainda seja a mais popular. As únicas explicações que consigo pensar envolvem inércia, masoquismo ou machismo (não é frequente que a "inércia" seja a razão mais atraente para se fazer alguma coisa ...)

Perl faz uma tentativa bastante fraca de torná-los mais legíveis, permitindo espaços em branco e comentários, mas não faz nada remotamente imaginativo.

Existem outras sintaxes. Uma boa é a sintaxe scsh para expressões regulares , que na minha experiência produz expressões regulares que são razoavelmente fáceis para digitar, mas ainda legível após o fato.

[ scsh é esplêndido por outras razões, apenas uma delas é a famosa entender o texto ]

    
por 29.09.2015 / 22:31
fonte
0

Acredito que as expressões regulares foram projetadas para serem tão "gerais" e simples quanto possível, para que possam ser usadas (aproximadamente) da mesma forma em qualquer lugar.

Seu exemplo de regex.isRange(..).followedBy(..) é acoplado à sintaxe de uma linguagem de programação específica e talvez ao estilo orientado a objetos (encadeamento de métodos).

Como é que este "regex" exato aparece em C, por exemplo? O código teria que ser alterado.

A abordagem mais "geral" seria definir uma linguagem simples e concisa, que pode ser facilmente incorporada em qualquer outro idioma sem alteração. E isso é (quase) o que é regex.

    
por 30.09.2015 / 15:07
fonte
0
Os mecanismos

Expressão Perl-Compatible Regular são amplamente utilizados, fornecendo uma sintaxe de expressão regular concisa que muitos editores e idiomas entendem. Como o @ JDługosz apontou nos comentários, Perl 6 (não apenas uma nova versão do Perl 5, mas uma linguagem totalmente diferente) tentou tornar as expressões regulares mais legíveis construindo-as a partir de elementos definidos individualmente. Por exemplo, aqui está um exemplo de gramática para analisar URLs do Wikilivros :

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

A divisão da expressão regular permite que cada bit seja definido individualmente (por exemplo, restringindo domain para ser alfanumérico) ou estendido até a subclasse (por exemplo, FileURL is URL que as restrições protocol sejam apenas "file" ). / p>

Então, não, não há razão técnica para a clareza das expressões regulares, mas formas mais recentes, mais limpas e mais legíveis de representá-las já estão aqui! Por isso esperamos ver algumas novas ideias neste campo.

    
por 07.09.2016 / 23:48
fonte