¿Existe alguna razón específica para la poca capacidad de lectura del diseño de sintaxis de expresiones regulares?

159

Todos los programadores parecen estar de acuerdo en que la legibilidad del código es mucho más importante que las frases cortas de sintaxis que funcionan, pero requieren un desarrollador senior para interpretar con cierto grado de precisión, pero parece ser exactamente la forma en que se expresaban las expresiones regulares. diseñado. ¿Hubo una razón para esto?

Todos estamos de acuerdo en que selfDocumentingMethodName() es mucho mejor que e() . ¿Por qué no debería aplicarse eso también a las expresiones regulares?

Me parece que en lugar de diseñar una sintaxis de lógica de una línea sin organización estructural:

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

¡Y esto ni siquiera es un análisis estricto de una URL!

En cambio, podríamos hacer una estructura de canalización organizada y legible, para un ejemplo básico:

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

¿Qué ventaja ofrece la sintaxis extremadamente concisa de una expresión regular que no sea la sintaxis lógica y de operación más corta posible? En última instancia, ¿existe una razón técnica específica para la poca legibilidad del diseño de sintaxis de expresiones regulares?

    
pregunta Viziionary 29.09.2015 - 18:57

10 respuestas

177

Hay una gran razón por la que las expresiones regulares se diseñaron tan concisas como lo son: se diseñaron para ser utilizadas como comandos a un editor de código, no como un lenguaje para codificar. Más precisamente, ed fue una de las Primeros programas para usar expresiones regulares, y desde allí las expresiones regulares comenzaron su conquista para dominar el mundo. Por ejemplo, el comando ed g/<regular expression>/p pronto inspiró un programa separado llamado grep , que todavía está en uso hoy en día. Debido a su poder, posteriormente se estandarizaron y se utilizaron en una variedad de herramientas como sed y vim

Pero suficiente para la trivia. Entonces, ¿por qué este origen favorecería una gramática concisa? Porque no escribes un comando de editor para leerlo una vez más. Basta con que puedas recordar cómo armarlo, y que puedes hacer las cosas con él que quieras hacer. Sin embargo, cada carácter que tiene que escribir ralentiza su progreso al editar su archivo. La sintaxis de expresiones regulares se diseñó para escribir búsquedas relativamente complejas de forma desechable, y eso es precisamente lo que les da a las personas los dolores de cabeza que las usan como código para analizar algunas entradas de un programa.

    
respondido por el cmaster 29.09.2015 - 21:09
62

La expresión regular que citas es un desastre terrible y no creo que nadie esté de acuerdo en que sea legible. Al mismo tiempo, gran parte de esa fealdad es inherente al problema que se está resolviendo: hay varias capas de anidamiento y la gramática de la URL es relativamente complicada (ciertamente demasiado complicada para comunicarse de manera sucinta en cualquier idioma). Sin embargo, es cierto que hay mejores maneras de describir lo que describe esta expresión regular. Entonces, ¿por qué no se usan?

Una gran razón es la inercia y la ubicuidad. No explica por qué se hicieron tan populares en primer lugar, pero ahora que lo son, cualquiera que conozca las expresiones regulares puede usar estas habilidades (con muy pocas diferencias entre dialectos) en cien idiomas diferentes y mil herramientas de software adicionales ( Por ejemplo, editores de texto y herramientas de línea de comando). Por cierto, este último no podría ni podría usar ninguna solución que equivalga a escribir programas , porque son muy utilizados por no programadores.

A pesar de eso, las expresiones regulares a menudo se usan en exceso, es decir, se aplican incluso cuando otra herramienta sería mucho mejor. No creo que la sintaxis de expresiones regulares sea terrible . Pero es evidente que es mucho mejor en patrones cortos y simples: el ejemplo arquetípico de identificadores en lenguajes tipo C, [a-zA-Z_][a-zA-Z0-9_]* se puede leer con un mínimo absoluto de conocimiento de expresiones regulares y una vez que se cumple ese límite es obvio y, a la vez, conciso. Requerir menos caracteres no es inherentemente malo, sino todo lo contrario. Ser conciso es una virtud siempre que sigas siendo comprensible.

Hay al menos dos razones por las que esta sintaxis se destaca en patrones simples como estos: no requiere que la mayoría de los caracteres se escapen, por lo que se lee de forma relativamente natural, y usa toda la puntuación disponible para expresar una variedad de combinadores de análisis simples. Quizás lo más importante es que no requiere nada en absoluto para la secuenciación. Escribes lo primero, luego lo que viene después. Compare esto con su followedBy , especialmente cuando el siguiente patrón no es no una expresión literal sino una expresión más complicada.

Entonces, ¿por qué se quedan cortos en casos más complicados? Puedo ver tres problemas principales:

  1. No hay capacidades de abstracción. Las gramáticas formales, que se originan en el mismo campo de la informática teórica que las expresiones regulares, tienen un conjunto de producciones, por lo que pueden dar nombres a las partes intermedias del patrón:

    # 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 pudimos ver más arriba, los espacios en blanco que no tienen un significado especial son útiles para permitir un formateo más fácil a la vista. Lo mismo con los comentarios. Las expresiones regulares no pueden hacer eso porque un espacio es solo eso, un literal ' ' . Sin embargo, tenga en cuenta que algunas implementaciones permiten un modo "detallado" donde los espacios en blanco se ignoran y los comentarios son posibles.

  3. No hay meta-lenguaje para describir patrones comunes y combinadores. Por ejemplo, uno puede escribir una regla digit una vez y seguir usándola en una gramática libre de contexto, pero no puede definir una "función", por así decirlo, se le da una producción p y crea una nueva producción que hace algo extra con él, por ejemplo, cree una producción para una lista separada por comas de ocurrencias de p .

El enfoque que usted propone sin duda resuelve estos problemas. Simplemente no los resuelve muy bien, ya que se comercializa en una mayor concisión por lo que es necesario. Los dos primeros problemas se pueden resolver mientras se mantienen dentro de un lenguaje específico de dominio relativamente simple y conciso. El tercero, bueno ... una solución programática requiere un lenguaje de programación de propósito general, por supuesto, pero en mi experiencia, el tercero es, con mucho, el menor de esos problemas. Pocos patrones tienen suficientes apariciones de la misma tarea compleja que el programador anhela para la capacidad de definir nuevos combinadores. Y cuando esto es necesario, el lenguaje a menudo es lo suficientemente complicado como para que no pueda y no deba analizarse con expresiones regulares de todos modos.

Existen soluciones para esos casos. Hay aproximadamente diez mil bibliotecas de combinadores de analizadores que hacen aproximadamente lo que usted propone, solo con un conjunto diferente de operaciones, a menudo con una sintaxis diferente y casi siempre con más poder de análisis que las expresiones regulares (es decir, tratan con idiomas libres de contexto o algo de tamaño considerable). subconjunto de esos). Luego están los generadores de analizadores, que van con el enfoque de "usar un mejor DSL" descrito anteriormente. Y siempre existe la opción de escribir algunos de los análisis a mano, en el código correcto. Incluso puede mezclar y combinar, utilizando expresiones regulares para tareas secundarias simples y haciendo las cosas complicadas en el código invocando las expresiones regulares.

No sé lo suficiente sobre los primeros años de computación para explicar cómo las expresiones regulares llegaron a ser tan populares. Pero están aquí para quedarse. Solo tienes que usarlos sabiamente, y no usarlos cuando sea más sabio.

    
respondido por el user7043 29.09.2015 - 19:53
39

Perspectiva histórica

El artículo de Wikipedia es bastante detallado sobre los orígenes de las expresiones regulares (Kleene, 1956). La sintaxis original era relativamente simple con solo * , + , ? , | y agrupación (...) . Era conciso ( y legibles, los dos no son necesariamente opuestos), porque los lenguajes formales tienden a expresarse con notaciones matemáticas concisas.

Más tarde, la sintaxis y las capacidades evolucionaron con los editores y crecieron con Perl , que intentaba ser concisa por diseño ( "las construcciones comunes deben ser cortas" ). Esto complejizó mucho la sintaxis, pero tenga en cuenta que las personas ahora están acostumbradas a las expresiones regulares y son buenas para escribirlas (si no leerlas). El hecho de que a veces son de solo escritura sugiere que cuando son demasiado largos, generalmente no son la herramienta adecuada. Las expresiones regulares tienden a ser ilegibles cuando se abusa de ellas.

Más allá de las expresiones regulares basadas en cadenas

Hablando de sintaxis alternativas, echemos un vistazo a una que ya existe ( cl-ppcre , en Common Lisp ). Su expresión regular larga se puede analizar con ppcre:parse-string de la siguiente manera:

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

... y da como resultado el siguiente formulario:

(: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 sintaxis es más detallada y, si observa los comentarios a continuación, no necesariamente será más legible. Así que no asumas que debido a que tienes una sintaxis menos compacta, las cosas se aclararán automáticamente .

Sin embargo, si comienza a tener problemas con sus expresiones regulares, convertirlas en este formato podría ayudarlo a descifrar y depurar su código. Esta es una ventaja sobre los formatos basados en cadenas, donde un error de un solo carácter puede ser difícil de detectar. La principal ventaja de esta sintaxis es manipular expresiones regulares mediante un formato estructurado en lugar de una codificación basada en cadenas. Eso le permite componer y construir tales expresiones como cualquier otra estructura de datos en su programa. Cuando uso la sintaxis anterior, esto generalmente se debe a que quiero compilar expresiones de partes más pequeñas (vea también mi respuesta de CodeGolf ). Para su ejemplo, podemos escribir 1 :

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

Las expresiones regulares basadas en cadenas también se pueden componer, usando concatenación de cadenas o interpolación envueltas en funciones auxiliares. Sin embargo, existen limitaciones con la manipulación de cadenas que tienden a clutter the code (piense en los problemas de anidamiento, no a diferencia de backticks vs. $(...) en bash; también, los caracteres de escape pueden provocarle dolores de cabeza).

Tenga en cuenta también que la forma anterior permite que (:regex "string") forme para que pueda mezclar notaciones concisas con árboles. Todo eso lleva a IMHO a una buena legibilidad y composibilidad; aborda los tres problemas expresados por delnan , indirectamente (es decir, no en el lenguaje de las expresiones regulares en sí).

Para concluir

  • Para la mayoría de los propósitos, la notación concisa es, de hecho, legible. Hay dificultades cuando se trata de notaciones extendidas que involucran el retroceso, etc., pero su uso rara vez se justifica. El uso injustificado de expresiones regulares puede llevar a expresiones ilegibles.

  • Las expresiones regulares no necesitan codificarse como cadenas. Si tienes una biblioteca o una herramienta que te puede ayudar a construir y componer expresiones regulares, evitarás muchos posibles errores relacionados con la manipulación de cadenas.

  • Alternativamente, las gramáticas formales son más legibles y son mejores para nombrar y abstraer las sub-expresiones. Los terminales generalmente se expresan como simples expresiones regulares.

1. Es posible que prefieras construir tus expresiones en tiempo de lectura, porque las expresiones regulares tienden a ser constantes en una aplicación. Consulte create-scanner y load-time-value :

'(:sequence :start-anchor #.(protocol) #.(slashes) ... )
    
respondido por el coredump 29.09.2015 - 20:07
25

El mayor problema con las expresiones regulares no es la sintaxis demasiado concisa, es que intentamos expresar una definición compleja en una sola expresión, en lugar de componerla a partir de bloques de construcción más pequeños. Esto es similar a la programación en la que nunca usas variables y funciones y, en cambio, incrusta su código en una sola línea.

Compare la expresión regular con BNF . Su sintaxis no es mucho más limpia que las expresiones regulares, pero se usa de manera diferente. Comience por definir símbolos con nombre simples y compóngalos hasta que encuentre un símbolo que describa todo el patrón que desea hacer coincidir.

Por ejemplo, mire la sintaxis de URI en rfc3986 :

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

Puede escribir casi lo mismo usando una variante de la sintaxis de expresiones regulares que admite la incorporación de sub-expresiones con nombre.

Personalmente, creo que una sintaxis de expresiones regulares concisas está bien para las funciones de uso común como las clases de caracteres, la concatenación, la elección o la repetición, pero para las funciones más complejas y raras como los nombres verbales de anticipación son preferibles. Muy similar a cómo usamos operadores como + o * en la programación normal y cambiamos a funciones con nombre para operaciones más raras.

    
respondido por el CodesInChaos 30.09.2015 - 08:51
12
  

selfDocumentingMethodName () es mucho mejor que e ()

es? Existe una razón por la que la mayoría de los idiomas tienen {y} como delimitadores de bloque en lugar de BEGIN y END.

A la gente le gusta la terseness, y una vez que conoce la sintaxis, la terminología corta es mejor. Imagine su ejemplo de expresión regular si d (por dígito) fuera 'dígito' la expresión regular sería aún más horrible de leer. Si lo hiciera más fácilmente analizable con caracteres de control, entonces se vería más como XML. Tampoco son tan buenos una vez que conoces la sintaxis.

No obstante, para responder correctamente a su pregunta, debe darse cuenta de que las expresiones regulares provienen de los días en que la terseness era obligatoria. Es fácil pensar que un documento XML de 1 MB no es un gran problema hoy en día, pero estamos hablando de días en que 1 MB Fue prácticamente toda su capacidad de almacenamiento. También se utilizaron menos idiomas en ese entonces, y las expresiones regulares no están a un millón de kilómetros de Perl o C, por lo que la sintaxis sería familiar para los programadores de la época que estarían contentos con el aprendizaje de la sintaxis. Así que no había ninguna razón para hacerlo más detallado.

    
respondido por el gbjbaanb 30.09.2015 - 09:43
6

Regex es como piezas de lego. A primera vista, verá algunas piezas de plástico de formas diferentes que se pueden unir. Podría pensar que no podría haber muchas cosas diferentes posibles que pueda moldear, pero luego verá las cosas asombrosas que otras personas hacen y simplemente se pregunta cómo es un juguete increíble.

Regex es como piezas de lego. Hay pocos argumentos que se pueden usar, pero encadenarlos en diferentes formas formarán millones de patrones de expresiones regulares diferentes que se pueden usar para muchas tareas complicadas.

La gente raramente usa los parámetros de expresiones regulares solo. Muchos idiomas le ofrece funciones para verificar la longitud de una cadena o dividir las partes numéricas de ella. Puede usar funciones de cadena para cortar textos y reformarlos. El poder de las expresiones regulares se nota cuando se utilizan formularios complejos para realizar tareas complejas muy específicas.

Puede encontrar decenas de miles de preguntas de expresiones regulares en SO y rara vez se marcan como duplicadas. Solo esto muestra los posibles casos de uso únicos que son muy diferentes entre sí.

Y no es fácil ofrecer métodos predefinidos para manejar estas tareas únicas tan diferentes. Tiene funciones de cadena para ese tipo de tareas, pero si esas funciones no son suficientes para su tarea de specifix, entonces es hora de usar expresiones regulares

    
respondido por el FallenAngel 30.09.2015 - 09:41
2

Reconozco que este es un problema de práctica más que de potencia. El problema generalmente surge cuando las expresiones regulares se implementan directamente , en lugar de asumir una naturaleza compuesta. De manera similar, un buen programador descompondrá las funciones de su programa en métodos concisos.

Por ejemplo, una cadena de expresiones regulares para una URL podría reducirse de aproximadamente:

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

a:

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

Las expresiones regulares son cosas ingeniosas, pero son propensas al abuso por parte de aquellos que se vuelven absorbidos en su complejidad aparente . Las expresiones resultantes son retórica, sin un valor a largo plazo.

    
respondido por el toplel32 30.09.2015 - 10:53
0

Como dice @cmaster, las expresiones regulares fueron diseñadas originalmente para usarse solo sobre la marcha, y es simplemente extraño (y un poco deprimente) que la sintaxis de ruido de línea sigue siendo la más popular. Las únicas explicaciones que puedo pensar tienen que ver con la inercia, el masoquismo o el machismo (no es frecuente que la "inercia" sea la razón más atractiva para hacer algo ...)

Perl hace un intento bastante débil de hacerlos más legibles al permitir espacios en blanco y comentarios, pero no hace nada remotamente imaginativo.

Hay otras sintaxis. Una buena es la sintaxis de scsh para expresiones regulares , que en mi experiencia produce expresiones regulares que son razonablemente fáciles para escribir, pero todavía legible después del hecho.

[ scsh es espléndido por otras razones, solo una de ellas es su famosa texto del acuse de recibo ]

    
respondido por el Norman Gray 29.09.2015 - 22:31
0

Creo que las expresiones regulares fueron diseñadas para ser tan "generales" y simples como sea posible, por lo que se pueden usar (aproximadamente) de la misma manera en cualquier lugar.

Su ejemplo de regex.isRange(..).followedBy(..) está acoplado a la sintaxis de un lenguaje de programación específico y quizás al estilo orientado a objetos (encadenamiento de métodos).

¿Cómo se vería este 'regex' exacto en C, por ejemplo? El código tendría que ser cambiado.

El enfoque más "general" sería definir un lenguaje simple y conciso que luego pueda integrarse fácilmente en cualquier otro idioma sin cambios. Y eso es (casi) lo que son expresiones regulares.

    
respondido por el Aviv Cohn 30.09.2015 - 15:07
0
Los motores de

Expresión regular compatible con Perl se utilizan ampliamente, lo que proporciona una sintaxis concisa de expresiones regulares que muchos editores e idiomas entienden. Como @ JDługosz señaló en los comentarios, Perl 6 (no solo una nueva versión de Perl 5, sino un lenguaje completamente diferente) ha intentado hacer que las expresiones regulares sean más legibles al construirlas a partir de elementos definidos individualmente. Por ejemplo, aquí hay un ejemplo de gramática para analizar las URL de Wikilibros :

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

La división de la expresión regular como esta permite que cada bit se defina individualmente (por ejemplo, restringiendo que domain sea alfanumérico) o extendido a través de subclases (por ejemplo, FileURL is URL que las restricciones protocol sean solo "file" ).

Entonces: no, no hay una razón técnica para la tersura de las expresiones regulares, ¡pero ya existen formas más nuevas, más limpias y más legibles de representarlas! Así que espero que veamos algunas ideas nuevas en este campo.

    
respondido por el Gaurav 07.09.2016 - 23:48

Lea otras preguntas en las etiquetas