Aritmética: dos divisiones, un módulo con carácter
La división que siempre devuelve flotante, la división entera con redondeo hacia abajo, el módulo que hereda el signo del divisor, la potencia que nunca es entera, y las reglas exactas de coerción entre los dos subtipos.
Una vez que el número se parte en dos subtipos, cada operador aritmético tiene que responder una pregunta que antes no existía: dado un entero y un flotante, ¿qué devuelvo? Lua responde con un puñado de reglas cortas, deliberadamente memorizables, y con dos decisiones que sorprenden a casi todo el mundo la primera vez: la división normal siempre produce un flotante aunque el resultado sea exacto, y la potencia también. Detrás de esas rarezas aparentes hay un principio de diseño mucho más defendible que la alternativa, y aquí vas a verlo entero.
- Dominar la diferencia entre la división flotante y la división entera con
//. - Predecir el signo del resto con
%y contrastarlo con el módulo de C. - Aplicar la regla de coerción que decide el subtipo de cualquier operación aritmética.
- Convertir entre entero y flotante de forma explícita y sin pérdidas ocultas.
Las dos divisiones
Lua ofrece dos operadores de división que responden a dos preguntas distintas. El operador / responde “cuántas veces cabe, con toda la precisión que pueda”; el operador //, introducido en 5.3, responde “cuántas veces cabe entera, redondeando hacia abajo”.
print(7 / 2) --> 3.5
print(6 / 2) --> 3.0 flotante, aunque sea exacto
print(7 // 2) --> 3
print(-7 // 2) --> -4 hacia abajo, no hacia cero
print(7.0 // 2) --> 3.0 conserva el subtipo flotante
Dos observaciones que valen todo el nivel. La primera: / siempre devuelve flotante, sin excepción, aunque los dos operandos sean enteros y el resultado sea exacto. Esa es la decisión que más protestas generó y la más sólida de todas, porque garantiza que el subtipo del resultado se pueda determinar leyendo el código, sin conocer los valores. Si 6 / 2 diera un entero y 7 / 2 un flotante, el subtipo dependería de los datos en tiempo de ejecución y ningún análisis estático —ni el de tu cabeza— podría anticiparlo.
La segunda: // no trunca hacia cero, redondea hacia menos infinito. Por eso -7 // 2 vale menos cuatro y no menos tres. Esta es la definición matemática de división euclídea por defecto y la que hace que la identidad con el módulo se sostenga con números negativos, como verás en un momento. La // de Lua es la de Python, no la de C ni la de Java.
Un matiz que se olvida: // conserva el subtipo. Si algún operando es flotante, el resultado es un flotante de valor entero, no un entero. 7.0 // 2 da 3.0, y math.type de eso responde float. La división entera describe una operación, no un tipo de retorno.
El módulo y el signo del divisor
El resto en Lua está definido por una identidad, no por el comportamiento del hardware:
a % b == a - math.floor(a / b) * b
De ahí se deduce todo lo demás sin memorizar nada. Como el redondeo es hacia abajo, el resto tiene siempre el signo del divisor, no el del dividendo.
print( 7 % 3) --> 1
print(-7 % 3) --> 2 signo del divisor, positivo
print( 7 % -3) --> -2 signo del divisor, negativo
print(-7 % -3) --> -1
Compáralo con C, donde -7 % 3 vale menos uno porque la división trunca hacia cero. Las dos convenciones son defendibles, pero la de Lua tiene una propiedad práctica enorme: el resto de un divisor positivo siempre está en el rango de cero a b menos uno, lo que convierte a % en la operación correcta para envolver índices circulares sin escribir una sola condición.
-- rotar un indice en un anillo de n elementos, con desplazamiento negativo
local n = 5
for d = -2, 2 do
io.write(((3 - 1 + d) % n) + 1, " ") --> 1 2 3 4 5
end
Con el módulo de C ese mismo cálculo necesitaría una comprobación de signo. Si necesitas explícitamente el comportamiento truncado de C, ahí está math.fmod, que trunca hacia cero y por tanto hereda el signo del dividendo:
print(-7 % 3) --> 2 signo del divisor
print(math.fmod(-7, 3)) --> -1.0 signo del dividendo, estilo C
print(7 % -3) --> -2
print(math.fmod(7, -3)) --> 1.0
Las dos funciones responden preguntas distintas, no son variantes de estilo: % responde “en qué posición del ciclo estoy”, y math.fmod responde “cuánto sobra tras truncar la división”. La primera es la que quieres para índices, calendarios y relojes; la segunda, para reproducir el comportamiento de una biblioteca de C que ya lo asume.
Con flotantes, el módulo sigue siendo consistente pero hereda los problemas de precisión de la resta, y con divisor cero cambia de personalidad: 5 % 0 lanza un error de intento de realizar la operación con cero, mientras que 5.0 % 0 devuelve not a number silenciosamente. Lo mismo ocurre con //: la división entera por cero es un error, la flotante da infinito.
La potencia, la coerción y el cero
El operador ^ es el segundo que siempre devuelve flotante, y por la misma razón que /: el resultado de una potencia con exponente arbitrario no es entero en general, y hacer depender el subtipo del valor del exponente reintroduciría la imprevisibilidad.
print(2^10) --> 1024.0
print(math.type(2^10)) --> float
print(2^-1) --> 0.5
print(1 << 10) --> 1024 esta si es entera
Si quieres una potencia entera, el desplazamiento de bits sirve para potencias de dos, y para el resto necesitas un bucle o una conversión explícita. La regla general de coerción que gobierna todos los operadores restantes cabe en tres líneas:
flowchart TD A[Operacion aritmetica con dos operandos] --> B[Es division normal o potencia] B -->|si| R1[Resultado flotante siempre] B -->|no| C[Algun operando es flotante] C -->|si| R2[Ambos se convierten a flotante] C -->|no| R3[Aritmetica entera con envolvimiento] R2 --> S[Resultado flotante] R3 --> T[Resultado entero] style R1 fill:#89b4fa,color:#11111b style S fill:#89b4fa,color:#11111b style T fill:#a6e3a1,color:#11111b
Suma, resta, multiplicación, división entera, módulo y menos unario preservan el subtipo si los dos operandos son enteros, y promocionan a flotante en cuanto uno lo es. La promoción de entero a flotante no es gratuita: un entero de 64 bits con más de 53 bits significativos pierde bits bajos al convertirse, y ese detalle es el tema del nivel siguiente.
También conviene saber que las cadenas se convierten a números en contextos aritméticos, siguiendo las reglas del analizador léxico. Es decir, la cadena se lee como si fuera un literal del fuente, y por tanto conserva el subtipo que ese literal tendría.
print(math.type("10" + 0)) --> integer
print(math.type("10.0" + 0)) --> float
print("0x10" + 0) --> 16
print(math.type("1e2" + 0)) --> float
Esta coerción es cómoda y es una trampa: convierte errores de tipo en resultados plausibles. En código serio, convierte tú con tonumber y comprueba el nil.
Convertir entre subtipos a propósito
Subir de entero a flotante es trivial y siempre posible, aunque no siempre exacto: basta sumar cero coma cero o multiplicar por uno coma cero.
local f = 7 + 0.0
print(math.type(f)) --> float
Bajar de flotante a entero tiene cuatro caminos y ninguno es intercambiable con los demás:
print(math.tointeger(3.0)) --> 3 exacto o falla
print(math.tointeger(3.5)) --> nil no hay representacion entera
print(math.floor(3.7)) --> 3 redondea hacia abajo
print(math.ceil(3.2)) --> 4 redondea hacia arriba
print(3.7 // 1) --> 3.0 redondea pero sigue flotante
La distinción crítica es entre math.tointeger, que es una conversión y solo tiene éxito si el valor ya era un entero disfrazado, y math.floor, que es un redondeo y siempre da una respuesta. Confundirlas produce el error clásico de aceptar 3.9 donde solo debía valer 3.
Hay además conversiones implícitas que exigen representación entera y lanzan error si no la hay: los operadores de bits, string.format con %d, y las funciones de biblioteca que esperan índices, como string.sub o table.insert.
print(string.rep("ab", 3.0)) --> ababab
-- string.rep("ab", 3.5) --> error: number has no integer representation
La regla que más incomoda al recién llegado —que 6 / 2 devuelva 3.0 y no 3— es en realidad el compromiso más maduro de todo el sistema numérico de Lua, y merece la pena entender contra qué se está defendiendo. La alternativa evidente, devolver entero cuando la división es exacta, tiene un coste que no se ve hasta que el programa es grande: el subtipo del resultado pasaría a depender de los valores en tiempo de ejecución y no de la forma del código. Una función que divide dos parámetros devolvería a veces un entero y a veces un flotante según qué le pasen, y esa incertidumbre se propaga: quien indexe una tabla con ese resultado tendrá o no la clave normalizada, quien lo pase a string.format con %d tendrá o no un error, quien lo serialice escribirá o no un punto decimal, y todo ello solo con ciertos datos de entrada. Es exactamente la clase de defecto que pasa las pruebas y explota en producción. Lua prefiere una regla que puedas aplicar leyendo, sin ejecutar: la división normal y la potencia devuelven flotante, punto; el resto preserva entero solo si ambos operandos lo son. Con esa disciplina, el subtipo de cualquier expresión es una propiedad sintáctica, calculable con la vista, y la conversión —cuando la quieres— la escribes tú, visible en el código, con math.tointeger o con //. Diseñar así es renunciar a la comodidad del caso fácil para comprar previsibilidad en el caso difícil, y es la misma elección que verás en el modelo de errores y en la semántica de las metatablas.
- Escribe un guion que, para cada operador de
+,-,*,/,//,%y^, imprima elmath.typedel resultado con las cuatro combinaciones de entero y flotante. Son veintiocho casos: tenlos todos delante. - Verifica numéricamente la identidad del módulo con las cuatro combinaciones de signo de siete y tres.
- Implementa
dividir_seguro(a, b)que devuelvanily un mensaje cuandobsea cero, distinguiendo el caso entero del flotante. - Escribe
potencia_entera(base, exp)que devuelva un entero de verdad usando multiplicación repetida, y compara sumath.typecon el debase^exp. - Busca en tu configuración de Neovim alguna expresión que use
/donde debería usar//, y razona qué pasaría si ese valor acabara indexando una tabla.