- TRRE es una extensión del lenguaje de expresiones regulares que agrega el operador
:para expresar transformaciones de texto directamente, y se ofrece con la herramienta CLI experimentaltrre, similar agrep -E - La forma básica es un par transductivo como
a:b, que cambia un patrón de entrada por un patrón de salida; la eliminación se expresa comox:y la inserción como:x, es decir, transformaciones con la cadena vacía - Igual que en una expresión regular común, se pueden usar alternativas, repeticiones y transformaciones de rangos de caracteres, con ejemplos como
cat:dog,[a:A-z:Z]y un cifrado César - La implementación interna construye un Finite State Transducer (FST) que maneja pares entrada-salida en lugar del FSA de una expresión regular normal, y también soporta una determinización experimental on-the-fly
- Por ahora no hay binarios precompilados y hay que compilarlo manualmente; entre los TODO pendientes están estabilizar DFT, soporte completo de Unicode, completar funciones ERE y procesar rangos de forma eficiente
El problema que TRRE busca resolver
- Las expresiones regulares comunes son útiles para encontrar patrones en texto, pero en edición de texto la lógica de manejo de grupos puede volverse compleja al funcionar como posprocesamiento
- TRRE extiende el lenguaje de expresiones regulares para poner el matching de patrones y la modificación de texto dentro de una misma expresión
- La sintaxis central tiene la forma
pattern-to-match:pattern-to-generate, y el ejemplo más simple esa:b, que cambiaaporb - La herramienta CLI
trrees una implementación que demuestra esta idea y funciona con una sensación similar agrep -E
Sintaxis básica de transformación
- La sustitución de cadenas se escribe como
cat:dogecho 'cat' | ./trre 'cat:dog'imprimedog- También se puede lograr el mismo resultado con una transformación carácter por carácter como
(c:d)(a:o)(t:g)
- Puede usarse, como
sed, para cambiar todas las coincidencias dentro de una cadena- Si se aplica
lamb:cataMary had a little lamb., el resultado esMary had a little cat.
- Si se aplica
- La eliminación se expresa dejando vacía la parte derecha, con la forma
string_to_delete:(x:)oreliminaxdexory produceora:cambia todas lasapor el símbolo vacío y las elimina en el scan mode por defecto- También se pueden eliminar varios caracteres con corchetes, como en
[aie]:
- La inserción se expresa dejando vacía la parte izquierda, con la forma
:string_to_insert(:x)orinsertaxantes deory producexorhad a (:little )lambinsertalittledentro de ese contexto
Transformaciones sobre expresiones regulares
- TRRE soporta alternativas con
|, igual que una expresión regular normal(c:b)at|(d:h)ogcambiacat dogporbat hog
- Los operadores de repetición también pueden aplicarse a transformaciones
(cat:dog)*cambiacatcatcatpordogdogdog- En el scan mode por defecto,
cat:dogpor sí solo también se aplica repetidamente y logra el mismo resultado
- Si se usa repetición en el patrón izquierdo, se pueden consumir varias entradas y convertirlas en una sola salida
(cat)*:dogcambiacatcatcatpordog
- Si se usan
*o+en el patrón derecho, puede producirse un bucle infinito- Conviene evitar expresiones como
:a* - Si hace falta una repetición finita, se puede indicar explícitamente la cantidad, como en
:(repeat-10-times){10}
- Conviene evitar expresiones como
Transformación de rangos y generadores
- La transformación de rangos de caracteres se escribe como
[a:A-z:Z]- Puede convertir
regular expressionsenREGULAR EXPRESSIONS
- Puede convertir
- Se incluye un ejemplo de cifrado César
[a:b-y:zz:a]cambiacaesar cipherpordbftbs djqifs[a:zb:a-z:y]lo revierte de nuevo acaesar cipher
- También puede actuar como generator, produciendo varias salidas a partir de una sola entrada
- Por defecto se usa la primera coincidencia posible
- Con la opción
-agenera todas las salidas posibles
- Por ejemplo, aplicar
:(0|1){3}a una entrada vacía genera secuencias binarias de 3 bits desde000hasta111 - Si se usa
:(0|1){,3}?junto con-ma, se generan salidas con forma de subconjuntos de longitud 3 o menor
Especificación del lenguaje y precedencia de operadores
- De manera informal, TRRE se define como pares
pattern-to-match:pattern-to-generate - El
pattern-to-matchde la izquierda puede ser una cadena o una expresión regular - El
pattern-to-generatede la derecha suele ser una cadena, pero también puede ser una expresión regular - El operador
:se trata actualmente como no asociativo, por lo que la formaTRRE:TRREno está permitida gramaticalmente- Esa forma tendría el significado natural de componer las relaciones que define TRRE, pero por la complejidad adicional todavía se deja fuera
- La precedencia de operadores, de mayor a menor, es la siguiente
- Carácter de escape
\ - Expresiones entre corchetes
[] - Agrupación
() - Repetición
* + ? {m,n} - Concatenación
- Transducción
: - Alternativa
|
- Carácter de escape
Modos y voracidad
trresoporta dos modos- Scan Mode: modo por defecto, aplica transformaciones secuencialmente
- Match Mode: usa la bandera
-my verifica si toda la cadena coincide con la expresión
- La opción
-agenera todas las salidas posibles - El modificador
?vuelve non-greedy a los operadores*,+y{,}<(.:)*>sobre<cat><dog>produce<><(.:)*?>sobre la misma entrada produce<><>
- También se incluyen ejemplos de cambiar contenido dentro de etiquetas o paréntesis
<(.*?:cat)>cambia<dog> <mouse>por<cat> <cat>
Implementación basada en FST y determinización
- Internamente, TRRE construye un Finite State Transducer (FST)
- Un FST es similar al Finite State Automaton (FSA) usado en expresiones regulares comunes, pero trabaja con pares entrada-salida en lugar de cadenas simples
- Las diferencias clave de TRRE son las siguientes
- Define una relación binaria entre dos lenguajes regulares
- Usa FST en lugar de FSA para la inferencia
- Soporta determinización experimental on-the-fly para mejorar el rendimiento
- En motores de expresiones regulares comunes, la determinización convierte un autómata no determinista en uno determinista para permitir inferencia en tiempo lineal respecto a la longitud de la cadena de entrada
- En TRRE puede aplicarse una idea parecida, pero no todos los transductores no deterministas NFT pueden convertirse en transductores deterministas DFT
- Si hay dos ciclos “malos” con la misma etiqueta de entrada, la generación de estados puede caer en un bucle infinito
- Existen métodos para detectar estos bucles, pero su costo es alto
Rendimiento y estado de instalación
- Se muestra un ejemplo donde la versión no determinista básica es un poco más lenta que
seden una sustitución simple./trre '(vodka):(VODKA)': real0m0.046ssed 's/vodka/VODKA/': real0m0.024s
- En tareas más complejas, hay un ejemplo donde la versión determinista
trre_dftes más rápida quesedsed -e 's/\(.*\)/\U\1/': real0m0.508s./trre_dft '[a:A-z:Z]': real0m0.131s
- Todavía no se ofrecen binarios precompilados
- La instalación consiste en clonar el repositorio y luego compilar y probar con
make && sh test.sh - Entre los TODO pendientes están los siguientes
- Una versión DFT estable
- Soporte completo de Unicode
- Completar funciones ERE
- Negación
^dentro de[] - Clases de caracteres
- Símbolos ancla
$^
- Negación
- Procesamiento eficiente de rangos
Enfoques de referencia
- El enfoque de matching de expresiones regulares está fuertemente inspirado en Regular Expression Matching Can Be Simple And Fast de Russ Cox
- La idea de determinización de transductores proviene de Finitely Subsequential Transducers de Cyril Allauzen y Mehryar Mohri
- El enfoque de parsing usa el Double-E algorithm de Erik Eidt, cercano al clásico Shunting Yard algorithm
1 comentarios
Comentarios de Hacker News
Me intriga ver hacia dónde va este proyecto. Dicho eso, la precedencia de operadores se siente poco natural, y parece que otras personas en este hilo sintieron algo parecido
Uno esperaría naturalmente que
cat:dogfuera equivalente a(cat):(dog), no aca(t:d)ogA mí también me confundió que
cat:dogse interprete comoca(t:d)ogy no como(cat):(dog), pero lo entendí al recordar que todos estamos usando un poco mal las expresiones regulares. Las regex “en realidad” deberían verse como generadores de cadenas, no como matchers, así quecat|dogpuede expandirse formalmente como un conjunto tipo{catog,cadog}En matching, basta con hacer una búsqueda de subcadenas de ese conjunto de cadenas dentro de un texto más grande. El problema es que la mayoría de los motores de regex reales no funcionan así, sino que hacen varias cosas raras para ajustarse a las expectativas o por eficiencia
Si pruebas varias herramientas de regex, aparecen variantes como
(cat)|(dog)o(cat)|(dog)|(ca[td]og). Por eso, desde una perspectiva más formal, me parece correcto quecat:dogproduzcaca(t:d)ogy no(cat):(dog). Pero tras décadas de abusar de las regex como herramientas de matching ajustadas a las expectativas del usuario, ahora todos ponemos paréntesis alrededor de la expresión que queremos reemplazarEsta propuesta es interesante y está bien diseñada, pero al final se siente como un intento de devolver las regex a su modelo generador original. El problema está más del lado de las herramientas que de la gramática
Hace tiempo trabajé en algo cercano a este ámbito; si nunca pensaste en las regex como generadores de conjuntos de cadenas, puedes jugar con esto aquí: https://onlinestringtools.com/generate-string-from-regex
Dicho eso, el comportamiento de estas herramientas de generación también es muy específico. Las herramientas que yo usaba tenían varias formas de limitar el generador especificando restricciones sobre clausuras y cosas similares
Posponerlo hasta después de la concatenación puede traer otros problemas. Por ejemplo, con un
:no asociativo, quizácat:dog:mousedebería ser ilegal, y no estoy seguro de cómo tratarloEn la versión actual se inserta épsilon, es decir, la cadena vacía. Por ejemplo, para eliminar saltando de a un carácter, técnicamente puedes ejecutar
..:, que es.(.:eps)El resultado de
echo 'abcde' | ./trre '..:'es'ace'De hecho, la asociación de
:también podría tener el significado de composición de relaciones regulares, pero por ahora me pareció demasiado complejo[a:A-z:Z], sería mejor[a-z:A-Z], y propondría algo como[a-y:b-z;z:a]en lugar de[a:b-y:zz:a]Si te interesan los transductores de estado finito y herramientas relacionadas, vale la pena ver XFST (Xerox Finite-State Transducer). Se ha usado durante más de 20 años en aplicaciones de lingüística computacional
Un investigador finlandés de PARC vino a una clase en UT y mostró cómo procesar la morfología del finés con FST; incluso visto desde fuera, era algo bastante impresionante
Explica el trabajo que se hizo en PARC
OpenFst es una biblioteca realmente excelente para transductores. También son buenos los tutoriales con casos de uso de Pynini que hicieron en Johns Hopkins y otros lugares en forma de tareas
[1] https://www.openfst.org/twiki/bin/view/GRM/Pynini
[2] https://www.openfst.org/
Si estás buscando una alternativa a las expresiones regulares estándar, especialmente si la lógica de grupos te resulta difícil o quieres expresiones mantenibles, Rosie Pattern Language podría ser lo adecuado
https://gitlab.com/rosie-pattern-language/rosie/-/blob/maste...
https://rosie-lang.org/about/
Genial. Hacia 1997 escribí mi tesis de Diplom en informática sobre transductores de estado finito, y resultó ser bastante menos trivial de lo que esperaba
La tarea era implementar composición y DFA cuando fuera posible, incluidos transductores compuestos. Era “álgebra de transductores de estado finito” y el caso de uso era la morfología. El tema estaba muy subestimado, así que tuve que cerrarlo más o menos a la mitad. Así que, mis respetos
Sobre la gramática, me pregunto si de verdad quieres que
:se asocie con más fuerza que la concatenaciónabEs bueno ver que, 20 años después, el proyecto sigue activo: https://www.openfst.org/twiki/bin/view/FST/WebHome
Todavía no estoy seguro de si
:debería asociarse con más fuerza que la concatenación. Después de ver unos 100 ejemplos, me pareció más natural el enfoque actual, es decir, que:tenga menor precedencia que., pero en el código literalmente se puede cambiar modificando un solo número. Por eso lo publiqué aquí: necesito feedback realEn cuanto se quiere hacer algún tipo de sustitución estructural, este enfoque no parece suficiente. Por ejemplo, a veces uno quiere hacer algo como
s/"([^"]*)"/'$1'/Además, si se pudiera cambiar lo que coincide con
[']dentro de[^"]por\', se vería más útilEn términos más generales, como una expresión regular básicamente define un árbol de parseo sobre el resultado de la coincidencia, sería útil poder realizar transformaciones más generales sobre ese árbol
":'(':(\\')|[^"'])*":'"..."y cambiar las comillas por comillas simples'Se puede hacer con esta expresión:
echo '"hello world" "hello again!"' | ./trre "\":'.+?:-\":'"El resultado es
'-' '-'Es decir, con la expresión
".+?:-"se sustituye el texto dentro de""por el símbolo-y, al mismo tiempo, se cambian las comillas que lo rodean. El signo de interrogación indica modo no codiciosoParece que todo el proyecto depende de la afirmación “las expresiones regulares son una gran herramienta para encontrar patrones en texto, pero siempre me parecieron poco naturales para editar texto”, pero no hay ni un solo ejemplo
No entiendo por qué las expresiones regulares serían poco naturales para editar. Tampoco sé qué significa editar aquí, ni por qué la gente tendría problemas con los grupos
Hay muchos ejemplos de la sintaxis de este proyecto, pero no veo por qué sería mejor que las expresiones regulares normales. Creo que podría entender el proyecto si hubiera algunos ejemplos del tipo “la versión con regex básica es esta, mi versión es esta, y por eso resulta más fácil”
Por ejemplo, para cambiar solo la
yque está entrexyzporY, en Python se haría más o menos así:pattern = r'(x)y(z)'replacement = r'\1Y\2'result = re.sub(pattern, replacement, text)Yo quisiera reemplazar eso por el patrón
xy:Yz:result = re.trre('xy:Yz', text)Si
xyzson patrones más complejos o expresiones regulares en sí mismas, este enfoque podría ser más cómodoBuen proyecto
El código en C es un verdadero gusto de leer. Muy bueno, lo estoy leyendo ahora
Solo un comentario breve: el enlace a
theory.pdfen el README está roto. El PDF está en el directoriodocs/, así que basta con incluirdocs/en la URLSe dice que hay que evitar usar
*o+en la parte derecha porque puede causar un bucle infinito, pero ¿no se podría simplemente prohibirlos?Entiendo que la especificación de la gramática se volvería más difícil, pero no parece haber una buena razón para mantenerlos
La razón original era que quería implementar una operación interesante llamada composición de transductores. Se pueden hacer operaciones simples sobre cadenas y componer trre como filtros, pero todavía no lo terminé. Así que sí, es una observación válida
Es una exploración interesante, pero faltan ejemplos de por qué en la práctica es mejor. Claro que también puede ser que yo lleve demasiado tiempo acostumbrado a las expresiones regulares
Por ejemplo, no veo por qué
(cat):(dog)en trre sería mejor ques/cat/dog, ni qué ventaja tiene(x:)orsobres/xor/or. Casi todos los ejemplos se me traducen mentalmente a expresiones regulares relativamente fácilesSi hay una ventaja central, sospecho que está en la lógica de grupos, así que sería bueno enfocar los ejemplos en eso. Parece mejor explicar primero por qué es una mejor opción, incluso antes de explicar la sintaxis básica
El ejemplo del cifrado César parece pedir a gritos una función de “aplicar esto en sentido inverso”. Es una petición común en muchas sustituciones de texto, y en este ejemplo resulta especialmente clara. La mente de un programador grita de inmediato: “¿por qué tengo que expresar la misma lógica dos veces?”
Todavía no sé si será útil, pero es excelente explorar alternativas a un statu quo muy asentado. Por lo general, esos intentos también tienen muchas probabilidades de no prosperar, pero aun así la exploración en sí da gusto verla
La especificación parece bastante incompleta. El primer ejemplo ya es raro:
$ echo 'cat' | trre 'c:da:ot:g'dogNo sé qué está pasando aquí. La gramática está así:
TRRE <- TRRE* TRRE|TRRE TRRE.TRRETRRE <- REGEX REGEX:REGEX¿Cuál es el árbol de parseo aquí? ¿Por qué
cno se convierte enda? O ¿por qué no se eliminacydase convierte enot?La idea de tener una semántica de búsqueda/reemplazo más intuitiva que un operador de agrupación está buena. En la época de MS-DOS podías hacer algo como
ren .log .txty funcionaba; desde la mentalidad moderna tipo bash no tiene sentido, pero a simple vista la intención era clarísima.Si llamamos explícitamente a ese operador
~, el ejemplo se ve así:$ echo 'cat' | trre 'c:d~a:o~t:g'dogSi agregamos paréntesis innecesarios, queda así:
$ echo 'cat' | trre '(c:d)~(a:o)~(t:g)'dogLo de por qué
cno se convierte endase debe totalmente a la precedencia. Viendo esta discusión, parece que elegí una precedencia equivocada, y eso causa confusión.La tabla de precedencia actual es la siguiente:
| 1 | carácter de escape | \ || 2 | expresión con corchetes | [] || 3 | agrupación | () || 4 | repetición ERE de un solo carácter | * + ? {m,n} || 5 | transformación | : || 6 | concatenación | . (implícita) || 8 | selección | | |Así que
:se asocia con más fuerza que., es decir, que la concatenación implícita.Si en cambio se exige que la expresión regular no esté vacía, el ejemplo de eliminación se rompe, pero la ambigüedad pasa al lado de la concatenación. Es decir, queda ambiguo si es
(((c:d)(a:o))(t:g))o((c:d)((a:o)(d:g))). Si se asume asociatividad, esta diferencia probablemente no importe.c:d,a:o sea nada, yot:g.Pero al releerlo, definitivamente es confuso, y en teoría la observación es válida. Después de leer el repositorio, yo también terminé creyendo que
cdebería convertirse enda, aunque no estoy seguro.