Lo que no trae y por qué
El catálogo razonado de las ausencias: sin JSON, sin sockets, sin sistema de ficheros completo, sin expresiones regulares, sin hebras y sin criptografía; la restricción de C portable que las explica todas y el precio real que se paga por ellas.
Todo lo dicho hasta aquí describe lo que Lua incluye. La parte interesante, y la que de verdad define al lenguaje, es la lista de lo que decidió no incluir. No hay serialización, no hay red, no hay directorios, no hay expresiones regulares, no hay hebras del sistema, no hay funciones de resumen criptográfico ni de fecha inversa. Cada una de esas ausencias tiene una justificación técnica concreta, todas ellas descienden de una única restricción autoimpuesta, y ninguna es un descuido pendiente de corregir. Esta lección expone la lista, reconstruye el razonamiento y, sobre todo, es honesta con lo que cuesta.
- Enumerar las ausencias significativas de la biblioteca estándar y agruparlas por causa.
- Distinguir los patrones de Lua de las expresiones regulares en potencia expresiva y en coste de implementación.
- Reconstruir la restricción de C portable y el papel de lenguaje anfitriado que explican el conjunto.
- Evaluar el precio de las ausencias en fragmentación, duplicación y riesgo de dependencias.
El catálogo de las ausencias
Conviene verlas juntas, porque separadas parecen olvidos y juntas dibujan una frontera nítida.
Sin serialización
Nada de JSON, XML, CSV ni ningún formato de intercambio. El lenguaje no sabe convertir una tabla en texto ni al revés, aunque su propia sintaxis de constructores sea un formato de datos excelente.
Sin red
Ni sockets, ni cliente de protocolo alguno, ni resolución de nombres. Un programa Lua puro no puede abrir una conexión, y ni siquiera puede saber que existe una tarjeta de red.
Sin sistema de ficheros
No hay listar directorio, crear carpeta, comprobar existencia, consultar tamaño o fecha, distinguir fichero de enlace ni leer permisos. Solo abrir, leer, escribir, posicionar, borrar y renombrar.
Sin expresiones regulares
Hay un motor de patrones propio, más pequeño y menos potente, que comparte notación superficial con las expresiones regulares y no es compatible con ninguna de sus variantes.
Hay una ausencia menos evidente y particularmente reveladora: Lua no trae serialización ni siquiera de sus propios datos, pese a que su sintaxis de constructores de tabla es un formato de intercambio excelente y a que la función load puede leerlo de vuelta en dos líneas. Es decir, el lenguaje tiene ya todas las piezas y aun así no ensambla la función, porque hacerlo obligaría a fijar decisiones —ciclos, claves no textuales, precisión de los flotantes, funciones— sobre las que no hay una única respuesta correcta.
-- Serializar el subconjunto que tu programa realmente usa: veinte lineas
local function volcar(v, sangria)
sangria = sangria or ""
if type(v) ~= "table" then return string.format("%q", v) end
local partes = { "{\n" }
for k, x in pairs(v) do
partes[#partes + 1] = string.format("%s [%q] = %s,\n",
sangria, k, volcar(x, sangria .. " "))
end
partes[#partes + 1] = sangria .. "}"
return table.concat(partes)
end
local texto = "return " .. volcar({ nombre = "ada", puntos = 42 })
local datos = assert(load(texto))()
print(datos.nombre, datos.puntos) --> ada 42
Ese fragmento no trata ciclos, ni referencias compartidas, ni claves numéricas mezcladas, y por eso mismo ilustra la tesis: una serialización general es un problema difícil con muchas respuestas defendibles, y la particular que tu programa necesita cabe en veinte líneas que entiendes por completo.
La lista continúa y merece enunciarse sin adornos: no hay hebras del sistema operativo, solo corrutinas colaborativas. No hay entrada y salida asíncrona. No hay funciones de resumen ni de cifrado. No hay generador aleatorio apto para uso criptográfico. No hay enteros de precisión arbitraria ni decimales exactos. No hay registro de diagnóstico, ni marco de pruebas, ni analizador de argumentos de línea de órdenes, ni servidor de nada. No hay función inversa de os.date: el lenguaje formatea fechas pero no las analiza. No hay normalización ni comparación de texto sensible al idioma, pese a existir una biblioteca utf8, porque esa biblioteca solo codifica y decodifica puntos de código.
Patrones no son expresiones regulares
El caso de los patrones es el más instructivo porque no es una ausencia pura, sino una sustitución consciente por algo más pequeño.
Un motor de expresiones regulares completo, con alternancia, agrupamiento con cuantificadores, retroceso y las extensiones que hoy se dan por supuestas, ocupa varios miles de líneas de C y arrastra un coste de mantenimiento perpetuo. El motor de patrones de Lua ocupa unos pocos cientos de líneas y cabe holgadamente en el presupuesto de tamaño del intérprete.
Lo que se pierde es concreto: no hay barra de alternancia, no se pueden aplicar cuantificadores a un grupo entero y no existen las miradas hacia delante o hacia atrás. Lo que se gana también lo es, y casi nunca se menciona: el patrón %b empareja delimitadores equilibrados, algo que las expresiones regulares clásicas son incapaces de expresar por razones teóricas, y el patrón %f detecta fronteras entre clases de caracteres. Además, la ausencia de retroceso exponencial elimina de raíz toda una familia de ataques de denegación de servicio que afectan a los motores tradicionales.
local s = "func(a, g(b, c), d)"
print(s:match("%b()")) --> (a, g(b, c), d)
for clave, valor in ("x=1; y=22; z=333"):gmatch("(%w+)=(%w+)") do
print(clave, valor)
end
-- Sin alternancia: lo que en una expresion regular seria una barra vertical
-- aqui se resuelve con dos intentos sucesivos, y suele leerse mejor
local fecha = s:match("%d%d%d%d%-%d%d%-%d%d") or s:match("%d%d/%d%d/%d%d%d%d")
La confusión más frecuente al venir de otro lenguaje es escapar con barra invertida. En los patrones de Lua el carácter de escape es el signo de porcentaje, y las clases se escriben con él. Esto tiene una ventaja lateral notable: como la barra invertida no es especial para el motor, los patrones no sufren la doble capa de escapes que hace ilegibles las expresiones regulares embebidas en cadenas de otros lenguajes.
La restricción que lo explica todo
Detrás de la lista entera hay una sola decisión, tomada al principio y jamás relajada: el intérprete de Lua se compila con un compilador de C conforme al estándar del lenguaje, sin una sola llamada fuera de la biblioteca estándar de C. Ni POSIX, ni interfaces del sistema, ni extensiones de compilador.
Esa regla determina mecánicamente qué puede estar en la biblioteca. Los sockets son POSIX o son de Windows, nunca de C estándar: fuera. Listar directorios es POSIX: fuera. Las hebras son POSIX o de Windows: fuera. Los ficheros, en cambio, sí están en C estándar, y por eso existe io, con exactamente las capacidades que C garantiza y ninguna más. El reloj está en C estándar con resolución de segundos, y por eso os.time da segundos.
flowchart TD A[Se propone una funcionalidad] --> B[Se puede implementar solo con C estandar] B -->|no| Z[Queda fuera de la biblioteca] B -->|si| C[Se puede escribir razonablemente en Lua puro] C -->|si| Z C -->|no| D[Es util para casi todos los programas] D -->|no| Z D -->|si| E[Cabe en el presupuesto de tamano y mantenimiento] E -->|no| Z E -->|si| F[Entra en la biblioteca estandar]
A esa criba se suman tres presiones que empujan en la misma dirección. La primera es el tamaño: el intérprete completo, con todas sus bibliotecas, produce un binario del orden de unos pocos cientos de kilobytes, lo que le permite vivir dentro de un microcontrolador, de un firmware o de un ejecutable de juego sin discusión presupuestaria. Cada función añadida es peso que pagan todos los usuarios, incluidos los que jamás la llamarán.
La segunda es el mantenimiento. Lua lo desarrolla un equipo minúsculo, del orden de tres personas. Una función que entra en la biblioteca hay que mantenerla, documentarla, probarla en todas las plataformas y sostenerla durante décadas.
Hay además un efecto de retirada que suele pasar inadvertido: lo que entra ya no sale. Las pocas funciones que Lua ha eliminado a lo largo de su historia costaron años de aviso, un ciclo de compatibilidad y una fractura del ecosistema. Sabiendo eso, el listón para admitir una función nueva sube tanto que la respuesta por defecto pasa a ser la negativa, y esa negativa por defecto es precisamente el mecanismo que ha mantenido pequeño el catálogo durante tres décadas.
La tercera, y la más definitoria, es que Lua se diseñó como lenguaje anfitriado: su caso de uso central no es escribir programas autónomos sino extender aplicaciones escritas en C. En ese modelo, la aplicación anfitriona ya tiene red, gráficos, ficheros y concurrencia, y lo que necesita del lenguaje es que se integre limpiamente con ellos mediante la interfaz en C, no que traiga su propia versión incompatible de cada cosa. Desde esa perspectiva, la verdadera biblioteca estándar de Lua no son las nueve tablas, sino la interfaz de programación en C que permite añadir cualquiera de las ausencias en unas decenas de líneas.
El precio, sin rebajas
Sería deshonesto terminar aquí. Las ausencias tienen un coste real y quien programe Lua a diario lo paga.
El coste más visible es la fragmentación. Como no hay un JSON oficial, hay más de una decena de bibliotecas de JSON, con interfaces distintas, tratamientos distintos de los valores ausentes y de las tablas vacías, y niveles de mantenimiento que van de lo excelente a lo abandonado. Lo mismo ocurre con los sockets, con el sistema de ficheros y con las pruebas. Dos programas Lua que hagan lo mismo pueden no compartir ni una sola dependencia.
El segundo coste es la duplicación silenciosa. En un proyecto mediano acaban conviviendo tres funciones de copia profunda, dos de división de cadenas y cuatro criterios distintos para representar la ausencia de valor en datos serializados. Ninguna biblioteca estándar impone el criterio, así que cada autor inventa el suyo.
El tercero es de seguridad y es el más serio. Cuando las operaciones más comunes viven en dependencias de terceros, la superficie de confianza del programa crece y se vuelve difusa. Un analizador de JSON escrito por un desconocido procesa datos externos en el corazón de tu proceso, y no hay un equipo de mantenimiento del lenguaje detrás de él.
Y hay un cuarto coste, puramente humano, que no aparece en ninguna discusión técnica: la carga de decisión. En un lenguaje con biblioteca grande, la pregunta de cómo leer un fichero de configuración tiene una respuesta y se tarda un minuto en encontrarla. En Lua tiene cinco respuestas, ninguna oficial, y elegir entre ellas exige evaluar mantenimiento, licencias, portabilidad y compatibilidad de versiones. Multiplicado por las quince decisiones de un proyecto mediano, ese trabajo es real y hay que presupuestarlo.
Todo lo que falta se puede añadir, y añadirlo es sorprendentemente barato. Un módulo en C que exponga una función del sistema a Lua ocupa unas decenas de líneas, y esa es la razón por la que el ecosistema pudo cubrir los huecos sin que el lenguaje tuviera que hacerlo. Cuando se dice que Lua no trae red, lo exacto es decir que trae el mecanismo con el que se le añade red en una tarde.
Reconocer estos cuatro costes no debilita el argumento del diseño; lo completa. Un lenguaje que renuncia a decidir por ti te entrega libertad y factura, y quien solo cuenta la primera mitad no está describiendo Lua, está haciendo propaganda.
Hay una manera de leer estas ausencias que rara vez se explicita y que las convierte de defecto en estrategia. Una biblioteca estándar es, ante todo, una promesa perpetua: lo que entra en ella no puede cambiar de comportamiento sin romper programas ajenos, y por tanto queda congelado en la forma que se creyó correcta el día en que se aceptó. Los ejemplos abundan en todos los lenguajes con biblioteca grande: interfaces de red diseñadas antes de que existiera el cifrado ubicuo, capas de fecha anteriores a la comprensión moderna de las zonas horarias, marcos de concurrencia previos a los procesadores con muchos núcleos, codificaciones de texto que suponían que un carácter era un byte. Todo eso sigue ahí, imposible de retirar, y compite con el sustituto correcto que llegó después. Lua evita ese destino por el procedimiento de no comprometerse. Al no incluir un cliente de red, jamás tuvo que mantener uno obsoleto. Al no incluir un JSON, no quedó atado a las decisiones que en 2005 parecían sensatas sobre cómo representar valores ausentes. Al no incluir hebras, no arrastra un modelo de concurrencia anterior a la generalización del paralelismo. El resultado es un lenguaje cuyo núcleo envejece extraordinariamente bien: código Lua de hace quince años sigue ejecutándose y sigue pareciendo idiomático, algo que muy pocos lenguajes pueden decir. El precio lo paga el ecosistema, que reinventa cada solución varias veces y no siempre bien, y ese precio es genuino, no una molestia menor. Pero la operación es un intercambio explícito y consciente: se sacrifica comodidad inmediata a cambio de que el lenguaje no tenga nada de lo que arrepentirse. Quien elija Lua debe entender que está comprando exactamente eso, y que la tarea de escoger dependencias con criterio —el asunto de la lección siguiente— no es un accidente del ecosistema, sino una responsabilidad que el diseño del lenguaje le transfiere de forma deliberada.
- Escribe la lista de todo lo que usa tu último proyecto y clasifícalo en lo que trae Lua y lo que hubo que traer de fuera.
- Implementa en Lua puro una función que recorra un directorio; comprueba que necesitas
io.popeno una dependencia y razona por qué. - Traduce a patrones de Lua tres expresiones regulares que uses habitualmente e identifica cuál de ellas es intraducible.
- Usa
%bpara extraer el cuerpo de una función anidada y explica por qué una expresión regular clásica no puede hacerlo. - Enumera tres cosas que agradeces que no estén en la biblioteca estándar y justifica cada una con un argumento de mantenimiento.