Los ocho tipos: el inventario cerrado de Lua
Los únicos ocho tipos que existen en Lua, qué devuelve exactamente la función type, por qué las variables carecen de tipo mientras los valores lo llevan encima, y qué consecuencias tiene que math.type revele un subtipo que type oculta.
Casi todos los lenguajes que has usado responden a la pregunta «cuántos tipos hay» con una lista abierta que crece cada vez que alguien declara una clase. Lua responde con una lista cerrada de ocho entradas que lleva dos décadas sin moverse y que no puedes ampliar desde el propio lenguaje: nil, boolean, number, string, table, function, userdata y thread. Todo lo demás —objetos, clases, módulos, colas, conjuntos, excepciones— se construye encima, casi siempre con tablas. Aprender esta lista no es memorizar ocho palabras: es entender qué decidieron los diseñadores no incluir, y por qué esa ausencia es la fuente de la flexibilidad de Lua.
- Enumerar los ocho tipos y describir la función que cumple cada uno en el lenguaje.
- Usar
typecon precisión, sabiendo que devuelve una cadena y no un tipo. - Explicar por qué las variables no tienen tipo y los valores sí, y qué se sigue de ello.
- Distinguir los subtipos del número con
math.typey prever cuándo esa distinción importa.
El inventario cerrado
nil
Un tipo con un único valor, también llamado nil. Significa «ausencia de valor útil» y es lo que obtienes al leer cualquier cosa que no fue asignada. Tiene lección propia porque no es tan simple como parece.
boolean
Exactamente dos valores, true y false. No es el tipo de las condiciones: en Lua cualquier valor puede aparecer en un if, así que este tipo es menos central de lo que su nombre sugiere.
number
Números reales representados por dos subtipos internos, entero de 64 bits y coma flotante de doble precisión. Desde el punto de vista de type ambos son un solo tipo.
string
Secuencias inmutables de bytes de longitud arbitraria, con el cero incluido. No son cadenas de caracteres: son cadenas de bytes, y esa distinción reaparece en cuanto trabajas con texto no ASCII.
table
El único mecanismo de estructuración de datos que existe. Arrays asociativos con parte secuencial optimizada. Sobre este tipo se construyen módulos, objetos, herencia, espacios de nombres y el entorno global.
function
Valores de primera clase, con cierres léxicos reales. Una función se guarda en una variable, se pasa como argumento y se devuelve como resultado sin ninguna sintaxis especial.
userdata
Un bloque de memoria en bruto gestionado por código C. Lua no puede crearlo ni inspeccionarlo: solo guardarlo, pasarlo y, si tiene metatabla, operarlo. Es la puerta al mundo anfitrión.
thread
Una corrutina: un hilo de ejecución independiente y colaborativo, sin paralelismo real. No es un hilo del sistema operativo y no comparte nada con el planificador del anfitrión.
Esta lista es cerrada en un sentido fuerte: no existe ninguna construcción del lenguaje que produzca un valor cuyo tipo esté fuera de ella. Cuando escribes lo que parece una clase, estás creando una tabla; cuando escribes lo que parece un objeto, estás creando otra tabla enlazada a la primera. La consecuencia práctica es que el vocabulario semántico de Lua es minúsculo, y todo el poder expresivo se desplaza a un solo tipo, la tabla, y a un solo mecanismo de extensión, las metatablas.
flowchart TD V[Todo valor de Lua] --> A[Semantica de valor] V --> B[Semantica de referencia] A --> A1[nil] A --> A2[boolean] A --> A3[number] A --> A4[string] B --> B1[table] B --> B2[function] B --> B3[userdata] B --> B4[thread] style A fill:#a6e3a1,color:#11111b style B fill:#89b4fa,color:#11111b
Esa división en dos columnas no es decorativa y volverá en la cuarta lección de este nivel: los cuatro tipos de la izquierda se copian al asignarse, los cuatro de la derecha se comparten.
La función type y lo que devuelve de verdad
type acepta cualquier valor y devuelve una cadena. Este detalle, que parece anecdótico, gobierna la forma de todo el código defensivo que escribirás en Lua.
print(type(nil)) --> nil (la cadena, no el valor)
print(type(true)) --> boolean
print(type(10)) --> number
print(type("10")) --> string
print(type({})) --> table
print(type(print)) --> function
print(type(io.stdout)) --> userdata
print(type(coroutine.create(print))) --> thread
print(type(type)) --> function
Como el resultado es una cadena, la comprobación de tipos se hace comparando cadenas, y ahí aparece el error más silencioso del principiante: una errata en el literal no produce ningún fallo, solo una condición que nunca se cumple.
local function esTabla(v)
return type(v) == "tabla" -- errata: siempre false, nunca avisa
end
No hay defensa del lenguaje contra esto porque, para Lua, "tabla" es una cadena tan legítima como "table". La disciplina práctica consiste en no esparcir literales de tipo por el código: centralizarlos en una función de validación o en una tabla de constantes, de forma que la errata exista en un solo lugar y sea localizable.
Conviene además saber qué no distingue type. No separa el entero del flotante, no separa el userdata ligero del completo, y no dice nada sobre la metatabla del valor. Dos tablas que representan conceptos completamente distintos son ambas "table". Si necesitas una noción de clase, tendrás que fabricarla tú, normalmente guardando un campo identificador o comparando metatablas con getmetatable.
Las variables no tienen tipo, los valores sí
En Lua no se declara el tipo de una variable porque la variable no es el sitio donde vive el tipo. Una variable es un nombre asociado a una ranura que puede contener cualquier valor, y el tipo viaja dentro del valor, no en la ranura. Esa es la razón de que el siguiente código sea perfectamente legal y no genere ni un aviso.
local x = 42 -- ahora x contiene un number
x = "cuarenta y dos" -- ahora contiene un string
x = { 42 } -- ahora una table
x = print -- ahora una function
x = nil -- ahora nada, y el valor anterior queda recolectable
Esto se llama tipado dinámico, pero conviene precisar en qué sentido Lua es dinámico y en cuál no. Lua sí es fuertemente tipado en tiempo de ejecución: nunca reinterpreta los bits de un valor como si fuera de otro tipo, y una operación aplicada a un tipo que no la admite produce un error inmediato en lugar de un resultado basura. Lo que no hay es una fase de comprobación estática, así que el error aparece cuando la línea se ejecuta, no cuando se carga el archivo.
local t = {}
print(#t) -- 0, la longitud de una tabla vacia
print(#true) -- error de ejecucion: attempt to get length of a boolean value
La implicación de diseño es que, en Lua, la fiabilidad no la aporta el compilador sino las pruebas y las guardas explícitas en las fronteras de tus módulos. Escribir una validación de argumentos al principio de una función pública no es paranoia: es el sustituto directo de la firma de tipos que el lenguaje no te ofrece.
El subtipo que type no te enseña
Desde Lua 5.3 el tipo number tiene dos representaciones internas: entero de 64 bits con complemento a dos y flotante de doble precisión IEEE 754. type devuelve "number" para ambas y math.type es la función que revela cuál es cuál.
print(math.type(3)) --> integer
print(math.type(3.0)) --> float
print(math.type(3 / 1)) --> float (la division siempre produce float)
print(math.type("3")) --> nil (no es un numero, es una cadena)
print(3 == 3.0) --> true (iguales como valores matematicos)
Que 3 y 3.0 sean iguales pero de subtipo distinto tiene efectos observables en tres sitios concretos: la representación textual, donde tostring produce 3 frente a 3.0; el desbordamiento, porque los enteros dan la vuelta en silencio mientras los flotantes se van a infinito; y las operaciones que exigen representación entera, como los operadores de bits, los índices de string.sub o el formato %d, que fallan con un flotante no integral.
La división / y la potencia ^ devuelven siempre flotante, incluso con operandos enteros. Si quieres un entero, usa la división entera // con operandos enteros, o math.tointeger cuando necesites convertir un flotante que representa exactamente un entero.
La tentación es leer el inventario de ocho tipos como una limitación de un lenguaje pequeño, hecho para incrustarse y por tanto obligado a renunciar. La lectura correcta es la contraria, y se ve en cuanto cuentas lo que falta. No hay array, no hay diccionario, no hay conjunto, no hay clase, no hay objeto, no hay módulo, no hay excepción, no hay enumeración, no hay registro. Todas esas nociones existen en programas Lua reales, pero ninguna existe en el lenguaje: todas son tablas con una convención encima. Esa decisión traslada el poder de decisión desde el diseñador del lenguaje hasta el autor de la biblioteca, y explica por qué en Lua conviven media docena de sistemas de objetos incompatibles sin que ninguno sea el oficial. El coste es real y hay que nombrarlo: sin tipos nominales no hay comprobación estática que aproveche, los mensajes de error hablan de tablas anónimas en lugar de conceptos de tu dominio, y dos bibliotecas pueden implementar la misma idea de forma irreconciliable. El beneficio, también real, es que el intérprete completo cabe en unos cientos de kilobytes, que la semántica entera se explica en un manual de cien páginas, y que puedes construir el sistema de objetos que tu dominio necesita en lugar de deformar tu dominio para que quepa en el que alguien eligió por ti. Un lenguaje con ocho tipos y metatablas no es un lenguaje sin tipos de usuario: es un lenguaje donde los tipos de usuario son código tuyo y no gramática del compilador.
- Escribe una función que reciba un valor y devuelva una descripción legible que incluya el resultado de
typey, si es número, el demath.type. - Consigue en una sola sesión interactiva un valor de cada uno de los ocho tipos y comprueba el resultado de
typesobre todos ellos. - Provoca deliberadamente el error del literal mal escrito y razona por qué el intérprete no puede detectarlo.
- Verifica que
3 == 3.0es verdadero y que sin embargotostringdevuelve textos distintos para cada uno. Explica en un comentario por qué ambas cosas son coherentes. - Investiga con qué operación consigues que un entero desborde y observa que el resultado da la vuelta en lugar de convertirse en flotante.