wandres.dev
PATRONES · no son expresiones regulares

Los límites: sin alternancia, sin grupos, y cuándo rendirse

Las dos carencias estructurales del lenguaje de patrones y sus rodeos, la coincidencia equilibrada y la frontera como excepciones que amplían el poder, los criterios objetivos para abandonar los patrones y pasar a LPeg, y el coste real en tiempo de un patrón mal escrito.

⏱ 20 min

Un capítulo sobre límites no es un capítulo de decepciones: es el que evita las horas perdidas. El lenguaje de patrones carece de dos cosas que quien viene de las expresiones regulares da por descontadas, y ninguna de las dos ausencias tiene remedio dentro del propio lenguaje. A cambio ofrece dos elementos que casi ningún motor de regex clásico tiene. Conocer con precisión ambos lados de esa frontera es lo que separa a quien resuelve un problema de texto en tres líneas de quien construye un monstruo de cuarenta caracteres que funciona con los ejemplos y falla con los datos. Y cuando la frontera se cruza, existe una salida limpia que además la escribió el mismo autor del lenguaje.

🎯 Al terminar esta lección sabrás
  • Reconocer las dos carencias estructurales y aplicar los rodeos idiomáticos de cada una.
  • Usar la coincidencia equilibrada y la frontera para lo que ningún cuantificador puede.
  • Enunciar criterios objetivos para abandonar los patrones y pasar a LPeg.
  • Estimar el coste en tiempo de un patrón y evitar los que degradan a comportamiento cuadrático.

Las dos ausencias

La primera ausencia es la alternancia. No hay forma de escribir «esto o aquello» dentro de un patrón: la barra vertical no tiene ningún significado especial y se busca a sí misma como carácter literal. Los rodeos son tres, y la elección entre ellos depende del tamaño de las alternativas. Si son caracteres sueltos, un conjunto entre corchetes las cubre. Si son cadenas distintas, se prueban en una tabla de patrones con un bucle. Si son variantes de un mismo dato, casi siempre es mejor normalizar la entrada antes de buscar.

-- no existe la alternancia: esto busca el texto literal
print(("gato"):match("gato|perro"))          --> nil

local candidatos = { "^gato", "^perro", "^loro" }
local function primera(s)
  for _, p in ipairs(candidatos) do
    local r = s:match(p)
    if r then return r end
  end
end
print(primera("perro viejo"))                --> perro

La segunda ausencia es la cuantificación de grupos. Un cuantificador se aplica a una clase de un solo carácter y nunca a una secuencia, de modo que no se puede pedir «repite este bloque». La consecuencia es que las estructuras repetitivas no se describen en un patrón: se recorren con gmatch, que aplica el patrón repetidamente y devuelve una coincidencia por vuelta. Ese cambio de mentalidad —del patrón que describe toda la estructura al patrón que describe una pieza y del bucle que las cuenta— es el ajuste más importante que hay que hacer al llegar desde otro lenguaje.

-- imposible: repetir el bloque de clave y valor dentro del patron
-- idiomatico: describir una pieza e iterar
local campos = {}
for k, v in ("a=1;b=2;c=3"):gmatch("(%w+)=(%w+)") do campos[k] = v end

Tampoco existen las aserciones de anticipación ni de retrospección, ni las banderas de modo, ni los grupos con nombre, ni la insensibilidad a la caja. Ninguna de esas carencias tiene rodeo dentro del patrón; todas lo tienen fuera, en el código Lua que lo rodea.

Las dos excepciones

Frente a esas ausencias, el lenguaje ofrece dos elementos que no derivan de las clases ni de los cuantificadores y que amplían su poder por encima de lo que cabría esperar.

La coincidencia equilibrada se escribe con un porcentaje, la letra be y dos caracteres: el de apertura y el de cierre. Encaja con una secuencia que empieza por el primero, termina por el segundo y mantiene el recuento equilibrado por el camino, contando aperturas y cierres anidados. Es el único elemento del lenguaje con memoria no acotada y el que le permite recortar bloques anidados sin escribir un analizador.

local expr = "resultado = calcula(a, suma(b, c), d) + 1"
print(expr:match("%b()"))                --> (a, suma(b, c), d)

print(("cuerpo {a {b} c} fin"):match("%b{}"))   --> {a {b} c}

La frontera se escribe con un porcentaje, la letra efe y un conjunto entre corchetes. No consume ningún carácter: encaja en la posición vacía donde el byte anterior no pertenece al conjunto y el siguiente , considerando que la cadena está rodeada por bytes nulos imaginarios en ambos extremos. Es lo más parecido a un límite de palabra que existe en el lenguaje.

local t = "casa casona descasar casa"
local n = select(2, t:gsub("%f[%w]casa%f[%W]", "X"))
print(n)                                 --> 2   (solo las palabras completas)
🧩

Equilibrada, no libre de contexto entera

La coincidencia equilibrada solo cuenta un par de delimitadores a la vez. No comprueba que las comillas de dentro estén cerradas ni que los corchetes no se crucen con las llaves.

🔍

La frontera es de ancho cero

Como no consume nada, puede aparecer al principio y al final del patrón sin alterar la coincidencia devuelta. Eso es justo lo que la hace útil en las sustituciones.

⚠️

Sin comentarios ni espacios ignorados

Un patrón es una cadena densa sin modo extendido. La única documentación posible es una línea de comentario encima o construirlo por concatenación de trozos con nombre.

🧵

Sin caché de patrones

El motor interpreta la cadena del patrón en cada llamada. No hay objeto compilado que reutilizar, así que el coste de análisis se paga siempre.

Cuándo rendirse

Hay un momento en el que insistir con patrones deja de ser economía y empieza a ser terquedad. Estas señales son objetivas y conviene tratarlas como reglas, no como consejos: cuando el patrón supera los treinta o cuarenta caracteres; cuando necesitas más de tres o cuatro capturas; cuando el problema tiene anidamiento de profundidad arbitraria y no un solo nivel de delimitadores; cuando estás encadenando dos o tres sustituciones para preparar la entrada de la siguiente; cuando necesitas informar del punto exacto en que la entrada es inválida; y, sobre todo, cuando lo que estás escribiendo es un analizador sintáctico disfrazado. Analizar un lenguaje con patrones es posible del mismo modo que es posible clavar un tornillo.

flowchart TB
A[Problema de texto] --> B[Es una busqueda o extraccion simple]
A --> C[Hay anidamiento arbitrario o alternancia real]
A --> D[Hay que informar de errores de sintaxis]
B --> E[Patrones de la biblioteca estandar]
C --> F[LPeg con gramaticas y eleccion ordenada]
D --> F
F --> G[Patrones como valores compuestos con operadores]
E --> H[Una linea sin dependencias externas]

La salida es LPeg, una biblioteca externa escrita por el propio autor de Lua sobre las gramáticas de expresiones de análisis. Sus patrones no son cadenas sino valores de primera clase que se combinan con los operadores del lenguaje: la secuencia, la elección ordenada, la repetición y la negación se escriben con operadores aritméticos sobrecargados, y una gramática recursiva es una tabla de patrones que se refieren entre sí. Todo lo que falta en la biblioteca estándar está ahí: alternancia, cuantificación de cualquier subexpresión, anticipación, recursión y capturas con transformación. A cambio se paga una dependencia binaria, más memoria y una curva de aprendizaje real.

-- el mismo problema en los dos mundos
-- con patrones: hay que iterar porque no se puede repetir un bloque
for k, v in ("a=1;b=2"):gmatch("(%w+)=(%w+)") do print(k, v) end

-- con LPeg: la estructura entera se describe y se compone
local lpeg = require "lpeg"
local P, R, C = lpeg.P, lpeg.R, lpeg.C
local nombre = C(R("az", "AZ") ^ 1)
local numero = C(R("09") ^ 1)
local par    = nombre * P "=" * numero
local lista  = par * (P ";" * par) ^ 0      -- repeticion de un bloque entero
print(lista:match("a=1;b=2"))               --> a  1  b  2

Obsérvese lo que hace posible ese cambio de representación: como los patrones son valores, se les puede poner nombre, guardarlos en variables, pasarlos a funciones y componerlos con operadores. La repetición se aplica a par, que es una subexpresión entera, exactamente lo que el lenguaje de la biblioteca estándar no permite; y la elección ordenada, escrita con el operador de suma, resuelve la alternancia que allí no existe. Cuando alguien dice que Lua «no tiene expresiones regulares decentes» suele estar comparando la biblioteca estándar con el motor de otro lenguaje, sin haber mirado el escalón siguiente, que es más potente que aquello con lo que compara.

La misma filosofía aplicada dos veces: el núcleo mínimo y la biblioteca potente

Conviene leer la pareja formada por los patrones y LPeg como lo que es: una sola decisión de diseño ejecutada en dos tiempos, y probablemente el ejemplo más limpio de la doctrina del tamaño en todo el lenguaje. En el núcleo se puso lo mínimo que resuelve el noventa por ciento de los casos de texto que aparecen en un programa real —encontrar un número, partir por comas, limpiar espacios, expandir una plantilla— con un coste de unos cientos de líneas de C, cero dependencias y ninguna configuración. Lo demás, deliberadamente, se dejó fuera. Y lo que se dejó fuera no acabó siendo una versión mejorada de las expresiones regulares, sino algo estrictamente más potente que ellas: LPeg no compite con los motores de regex, los supera, porque las gramáticas de análisis reconocen todos los lenguajes deterministas libres de contexto y la elección ordenada elimina de raíz la ambigüedad que hace impredecible el retroceso de las regex clásicas. Fíjate en la consecuencia. Si Lua hubiera metido un motor de expresiones regulares en su núcleo, hoy tendría un núcleo diez veces mayor, un formalismo de potencia intermedia incrustado para siempre en la biblioteca estándar, y ninguna presión para que existiera algo mejor; los usuarios habrían resuelto sus problemas de análisis a base de regex monstruosas, como se hace en casi todas partes. Al no meterlo, tiene un núcleo diminuto para lo simple y una biblioteca de análisis de primer nivel para lo complejo, y el salto entre ambos es explícito: cuando el patrón deja de caber en una línea, cambias de herramienta en lugar de escalar la misma hasta lo ilegible. Eso es lo que significa mecanismo en lugar de política aplicado al texto: el lenguaje no te da la solución universal, te da la solución barata y la puerta abierta para la cara, y te obliga a decidir conscientemente cuál necesitas. La decisión que este nivel te pide que tomes de forma explícita —esto es un patrón, esto es una gramática— la mayoría de los lenguajes te la esconden, y ese ocultamiento es el origen de buena parte del código de texto imposible de mantener que existe en el mundo.

El coste

Un patrón no se compila: la cadena se interpreta byte a byte en cada llamada, y el motor busca probando en la posición uno, luego en la dos, y así hasta el final. De ahí se siguen tres costes concretos. El primero es que una búsqueda que falla recorre todas las posiciones del sujeto, de modo que el coste es el producto de la longitud del texto por la del patrón; buscar en un bucle un patrón que casi nunca encaja sobre una cadena grande es la forma más común de perder rendimiento sin darse cuenta. El segundo es que los cuantificadores retroceden, y varios perezosos encadenados en el mismo patrón multiplican los caminos que el motor explora; en casos extremos se agota el contador de profundidad del propio motor y se obtiene el error de patrón demasiado complejo. El tercero es la basura: cada captura construye una cadena nueva, así que un bucle que extrae tres capturas de un millón de líneas crea tres millones de objetos.

-- caro: reconstruye la clave en cada vuelta y recorre todo al fallar
for linea in texto:gmatch("[^\n]+") do
  local k = linea:match("^%s*(%a+)%s*=")
end

-- barato: descarta pronto con una comprobacion literal
for linea in texto:gmatch("[^\n]+") do
  if linea:find("=", 1, true) then
    local p1, p2 = linea:match("^%s*()%a+()")   -- posiciones, sin subcadenas
  end
end
📝
Lo esencial

No hay alternancia ni cuantificación de grupos, y ninguna de las dos tiene rodeo dentro del patrón: se resuelven con una tabla de patrones o con un bucle de iteración. La coincidencia equilibrada y la frontera son las dos excepciones que dan poder extra. Se abandona el patrón cuando aparece anidamiento arbitrario, alternancia real o necesidad de informar errores, y la salida es LPeg. El coste de una búsqueda fallida es proporcional al producto de las dos longitudes, y cada captura construye una cadena.

⚔️ Mide la frontera y crúzala
  1. Escribe la selección entre tres alternativas con una tabla de patrones y compárala con la versión que normaliza la entrada antes de buscar.
  2. Extrae con coincidencia equilibrada los argumentos de una llamada anidada y demuestra con un ejemplo qué caso no cubre.
  3. Reemplaza una palabra completa con la frontera y sin ella sobre un texto donde esa palabra sea prefijo de otra. Cuenta las sustituciones de cada versión.
  4. Mide el tiempo de una búsqueda que siempre falla sobre una cadena de un millón de bytes y comprueba que crece de forma proporcional a su longitud.
  5. Escoge un problema tuyo que hayas resuelto con tres sustituciones encadenadas y reescríbelo con LPeg. Compara líneas, legibilidad y tiempo.