Coerciones: cuándo Lua convierte por ti y cuándo no
Dónde ocurre exactamente la conversión automática entre números y cadenas, dónde el lenguaje se niega a convertir, qué hacen con precisión tonumber y tostring, y por qué una coerción que funciona en la aritmética y falla como clave de tabla produce los errores más difíciles de encontrar.
Lua convierte cadenas en números y números en cadenas de forma automática, pero solo en dos lugares muy concretos del lenguaje, y esa concreción es justo lo que hace peligrosa la coerción. Un identificador leído como cadena desde un archivo de configuración se suma sin protestar, se concatena sin protestar, se compara con igualdad y devuelve false, y se usa como clave de tabla apuntando a otra entrada distinta. Nada de eso produce un error: produce un programa que hace algo levemente distinto de lo que crees. Esta lección delimita la frontera exacta y explica por qué conviene cruzarla siempre de forma explícita.
- Enumerar los dos contextos donde Lua aplica coerción automática y qué reglas sigue en cada uno.
- Identificar los contextos donde deliberadamente no hay coerción y qué ocurre en ellos.
- Usar
tonumberytostringcon control sobre la base, el subtipo y los metamétodos. - Diagnosticar bugs causados por un valor que atraviesa fronteras de tipo sin fallar.
Los dos lugares donde Lua convierte por ti
El primero es la aritmética: si un operando es una cadena que parece un número, se convierte y la operación continúa. La conversión sigue las reglas del analizador léxico del propio lenguaje, así que acepta espacios alrededor, signo, notación hexadecimal y exponente.
print("10" + 1) --> 11
print(" 7 " * 2) --> 14 (los espacios se ignoran)
print("0x10" + 0) --> 16 (hexadecimal reconocido)
print("1e2" + 0) --> 100.0 (el exponente produce un flotante)
print("-3" // 2) --> -2
Un detalle que distingue a Lua 5.4 de sus versiones anteriores: la coerción de cadena a número salió del núcleo del lenguaje y hoy la implementan metamétodos que instala la biblioteca string. El cambio tiene una consecuencia visible y una invisible. La visible es que el subtipo del numeral se conserva, de modo que "1" + "2" produce el entero 3 y no un flotante. La invisible es que si un intérprete se construye sin la biblioteca de cadenas, esa aritmética deja de existir.
El segundo lugar es la concatenación: el operador .. acepta números además de cadenas y los convierte a su representación textual.
print("id-" .. 42) --> id-42
print(1 .. 2) --> 12 (una cadena, no el numero doce)
print(1.0 .. "") --> 1.0 (el subtipo flotante se nota en el texto)
Ese 1 .. 2 con espacios no es coquetería tipográfica: sin el espacio, el analizador léxico lee 1. como el comienzo de un número flotante y falla. Es la única situación del lenguaje donde un espacio en blanco cambia el significado de una expresión.
flowchart TD S[Cadena en operacion aritmetica] --> M[Metametodo de la biblioteca string] M --> P[Analisis lexico del numeral] P -->|valido| N[Numero conservando su subtipo] P -->|invalido| E[Error de ejecucion] C[Numero en concatenacion] --> R[Conversion a texto] R --> T[Cadena con formato por defecto] style N fill:#a6e3a1,color:#11111b style E fill:#f38ba8,color:#11111b style T fill:#89b4fa,color:#11111b
Los lugares donde Lua se niega a convertir
Fuera de esos dos contextos, no hay coerción alguna. Y los tres sitios donde no la hay son precisamente los que más se dan por supuestos.
En la comparación no hay conversión, y el comportamiento es distinto según el operador. La igualdad entre tipos distintos siempre devuelve false sin más; el orden entre tipos distintos es directamente un error.
print("10" == 10) --> false, sin conversion y sin aviso
print("10" < "9") --> true, orden alfabetico de cadenas
print("10" < 10) --> error: attempt to compare string with number
La segunda línea merece detenerse: comparar dos cadenas es un orden lexicográfico, así que "10" va antes que "9" igual que en un diccionario. Un conjunto de números leídos como texto y ordenados con table.sort sale en un orden que parece aleatorio y no lo es.
En las claves de tabla tampoco hay conversión entre cadena y número. Los índices son entradas distintas y no se comunican.
local t = {}
t[1] = "entero"
t["1"] = "cadena"
print(t[1], t["1"]) --> entero cadena (dos claves diferentes)
print(t[1.0]) --> entero (aqui si: el flotante exacto se normaliza)
Y en los operadores de bits Lua 5.4 eliminó la coerción por completo, precisamente porque una cadena con un numeral flotante no tenía una interpretación entera obvia.
print("3" & 1) --> error: attempt to perform bitwise operation on a string
print(3.0 & 1) --> 1, un flotante con valor entero exacto si vale
print(3.5 & 1) --> error: number has no integer representation
#"10" es 2, la longitud de la cadena, no el número diez. El símbolo es el mismo que usas para el tamaño de una lista, así que una variable que creías numérica y llegó como texto atraviesa esa expresión sin fallar y devuelve un valor plausible pero equivocado.
tonumber y tostring: la conversión bajo control
tonumber intenta convertir su argumento y devuelve nil cuando no puede, lo cual lo convierte en la herramienta correcta para validar entrada externa: no lanza error, informa del fracaso.
print(tonumber("42")) --> 42 (entero)
print(tonumber("42.0")) --> 42.0 (flotante: el punto lo decide)
print(tonumber(" 42 ")) --> 42 (espacios permitidos)
print(tonumber("42abc")) --> nil (sobra texto)
print(tonumber("")) --> nil
Con un segundo argumento acepta bases de 2 a 36, y entonces exige que el primer argumento sea una cadena y la interpreta como entero sin signo ni punto.
print(tonumber("ff", 16)) --> 255
print(tonumber("1010", 2)) --> 10
print(tonumber("z", 36)) --> 35
tostring hace el camino inverso y siempre tiene éxito, porque todo valor tiene representación textual. Para los números respeta el subtipo, y para las tablas y el userdata consulta primero el metamétodo __tostring y, en su defecto, el campo __name de la metatabla.
print(tostring(42)) --> 42
print(tostring(42.0)) --> 42.0
print(tostring(true)) --> true
print(tostring(nil)) --> nil
local p = setmetatable({}, { __tostring = function() return "punto" end })
print(tostring(p)) --> punto
Cuando necesites control fino sobre el formato, string.format es la vía, con la advertencia de que %d exige un valor con representación entera exacta: 3.0 pasa y 3.5 produce un error en tiempo de ejecución. Conviene además saber que math.tointeger convierte un flotante a entero solo si el valor es exacto, y devuelve nil en caso contrario, lo que lo hace ideal para validar índices.
Por qué la coerción implícita produce bugs sutiles
La razón por la que estos errores son caros no es que la coerción exista, sino que existe a medias. Un valor equivocado no falla al entrar en el sistema: falla mucho más tarde, en el primer contexto donde no hay conversión, y para entonces ha pasado por diez funciones.
local id = "42" -- llega asi desde un archivo o desde la red
print(id + 0) --> 42 funciona: aritmetica
print("usuario " .. id) --> ok funciona: concatenacion
print(id == 42) --> false falla en silencio
usuarios[id] = "ada" -- guarda en la clave de texto
print(usuarios[42]) --> nil la busqueda numerica no encuentra nada
Ninguna de las cinco líneas produce un error. Las dos primeras refuerzan la impresión de que el valor «funciona como número», y las tres últimas construyen un estado incoherente que se manifestará en otra parte del programa, probablemente como un dato que desaparece. El mismo patrón aparece con datos leídos de la entrada estándar, donde todo llega como cadena, y con configuraciones donde un puerto o un identificador se escribió entre comillas.
La disciplina que elimina la clase entera de errores es sencilla de enunciar y conviene aplicarla sin excepciones: convierte en la frontera. En el punto exacto donde un dato entra en tu programa desde fuera, pásalo por tonumber y valida el resultado. A partir de ahí, dentro del sistema, los tipos son los que tú decidiste y no los que trajo el formato de origen.
local function leerPuerto(texto)
local n = tonumber(texto)
if not n or math.type(n) ~= "integer" or n < 1 or n > 65535 then
return nil, "puerto invalido: " .. tostring(texto)
end
return n
end
Hay tres posturas coherentes ante la conversión automática. Un lenguaje puede no convertir nunca, y entonces cada operación entre tipos distintos falla en el sitio exacto donde ocurre el desajuste, con un mensaje que apunta al culpable. Puede convertir siempre, definiendo una conversión total entre todos los pares de tipos, y entonces nada falla, todo produce algún resultado y el programador aprende una tabla de conversiones que debe memorizar entera. Lua eligió la tercera vía, la coerción parcial: convierte en dos contextos y en ningún otro. Y esa vía tiene, por construcción, la peor propiedad de diagnóstico de las tres, porque un valor mal tipado sobrevive a los contextos que convierten y solo se delata en los que no lo hacen, es decir, en un punto arbitrariamente lejano de donde entró. El error se manifiesta como resultado equivocado y no como excepción, que es la forma más cara de todas. Ahora bien, la elección no fue un descuido, y para juzgarla hay que recordar para qué se diseñó el lenguaje. Lua nació como lenguaje de configuración y de guiones incrustados, donde la mayor parte de los datos entra como texto desde archivos, formularios y protocolos, y donde exigir una conversión explícita en cada línea habría hecho ilegible precisamente el código que el lenguaje quería hacer trivial. La coerción aritmética y la de concatenación cubren entre las dos casi todo lo que un guión hace con datos externos. Lo que sí se puede afirmar sin ambigüedad es cuál es la consecuencia para quien escribe código serio: como el lenguaje no va a avisarte en la frontera, la frontera tienes que dibujarla tú. Un módulo que valida y convierte toda su entrada en la primera línea de sus funciones públicas recupera enteras las garantías que la coerción parcial disolvió, y lo hace sin renunciar a la comodidad en el interior.
- Comprueba que
"10" + 1funciona, que"10" == 10es falso y que"10" < 10es un error, y explica por qué las tres cosas son coherentes entre sí. - Ordena con
table.sortuna lista de números escritos como cadenas y razona el orden resultante. - Guarda un valor en
t[1]y otro ent["1"], recórrela conpairsy observa que conviven dos claves. - Escribe una función de validación de entrada que use
tonumberymath.typey devuelvanilmás un mensaje ante cualquier texto inválido. - Investiga qué imprime
1 .. 2y qué ocurre si eliminas los espacios alrededor del operador. Explica el papel del analizador léxico.