wandres.dev
VALORES Y TIPOS · ocho tipos y ya

La verdad en Lua: solo nil y false son falsos

La regla de verdad más corta de cualquier lenguaje moderno, por qué el cero y la cadena vacía son verdaderos, qué devuelven realmente los operadores and y or, y qué idiomas muy extendidos se rompen en silencio cuando false es un valor legítimo de tu dominio.

⏱ 17 min

Cada lenguaje decide qué valores cuentan como verdaderos en una condición, y esa decisión, aparentemente menor, determina cómo se escribe todo el código de control del ecosistema durante décadas. Lua eligió la regla más corta posible: son falsos nil y false, y todo lo demás es verdadero. Sin excepciones, sin listas de valores vacíos, sin conversiones definidas por el usuario. El cero es verdadero, la cadena vacía es verdadera y la tabla vacía es verdadera. Esta lección explica qué se gana con esa brevedad, qué se paga por ella, y por qué los dos idiomas condicionales más comunes de Lua fallan exactamente en el mismo punto.

🎯 Al terminar esta lección sabrás
  • Enunciar la regla de verdad de Lua y aplicarla a valores frontera como cero, cadena vacía y tabla vacía.
  • Explicar por qué and y or devuelven operandos y no valores booleanos.
  • Detectar los dos idiomas clásicos que se rompen cuando false es un valor válido.
  • Elegir con criterio entre comprobar verdad y comparar explícitamente con nil.

La regla completa cabe en una línea

Solo hay dos valores falsos en todo el lenguaje: nil y false. Cualquier otro valor, sea cual sea su tipo y su contenido, hace que una condición se cumpla.

if 0 then print("el cero es verdadero") end           --> se imprime
if "" then print("la cadena vacia tambien") end       --> se imprime
if "false" then print("y esta cadena tambien") end    --> se imprime
if {} then print("y la tabla vacia") end              --> se imprime
if 0/0 then print("y hasta NaN") end                  --> se imprime

La ausencia de conversión numérica a booleano no es un olvido, es la corrección deliberada de un problema conocido. En los lenguajes donde el cero es falso, el valor que representa «cantidad nula» y el que representa «respuesta negativa» colisionan, y una función que devuelve un contador legítimo de cero se vuelve indistinguible de una que ha fallado. Lua separa esos dos conceptos por construcción: el cero es un número, no una respuesta.

flowchart TD
X[Cualquier valor de Lua] --> Q[Es nil o es false]
Q -->|si| F[Rama falsa]
Q -->|no| T[Rama verdadera]
T --> Z1[Incluye el cero]
T --> Z2[Incluye la cadena vacia]
T --> Z3[Incluye la tabla vacia]
T --> Z4[Incluye NaN]
style F fill:#f38ba8,color:#11111b
style T fill:#a6e3a1,color:#11111b

La contrapartida directa es que una tabla nunca es falsa por estar vacía. La comprobación de vacuidad hay que escribirla, y la forma correcta depende de si la tabla es una secuencia o un diccionario.

local t = {}
if not t then print("nunca se imprime") end   -- t es una tabla, luego verdadera

print(#t == 0)          --> true, valido solo si t es una secuencia sin agujeros
print(next(t) == nil)   --> true, valido siempre, tambien para claves de texto

and, or y not no devuelven booleanos

Este es el segundo pilar y el que más código idiomático explica. En Lua, and y or no producen true ni false: devuelven uno de sus dos operandos, y lo hacen con evaluación en cortocircuito.

La regla exacta es que a and b evalúa a y, si es falso, lo devuelve sin mirar b; en caso contrario devuelve b. Y a or b evalúa a y, si es verdadero, lo devuelve sin mirar b; en caso contrario devuelve b.

print(1 and 2)          --> 2       (1 es verdadero, se devuelve el segundo)
print(nil and 2)        --> nil     (corto: el segundo ni se evalua)
print(false or "x")     --> x
print(nil or false)     --> false   (ninguno es verdadero, se devuelve el ultimo)
print(not 0)            --> false   (not si devuelve siempre un booleano)

not es la excepción: siempre produce un booleano de verdad, y por eso not not x es el modismo canónico para normalizar cualquier valor a true o false.

La precedencia importa y es fácil de recordar: not liga más fuerte que and, y and liga más fuerte que or. Además, ninguno de los tres consulta metamétodos, así que una tabla con metatabla sigue siendo simplemente verdadera por ser tabla.

De estas reglas nacen los dos idiomas más frecuentes del lenguaje. El primero es el valor por defecto.

local function saludar(nombre)
  nombre = nombre or "invitado"
  return "hola, " .. nombre
end

El segundo es el condicional en expresión, el sustituto del operador ternario que Lua no tiene.

local estado = (n == 0) and "vacio" or "con datos"

Ambos son legítimos, ambos aparecen en toda la biblioteca estándar, y ambos tienen exactamente el mismo punto ciego.

Los dos idiomas que se rompen en silencio

El idioma del valor por defecto colapsa cuando false es un valor legítimo del dominio, porque or no distingue «no me lo pasaron» de «me lo pasaron valiendo false».

local function crearVentana(opciones)
  opciones = opciones or {}
  local visible = opciones.visible or true   -- BUG: siempre true
  return { visible = visible }
end

print(crearVentana({ visible = false }).visible)   --> true, se ignoro la peticion

La forma correcta compara explícitamente con nil, que es la única pregunta que separa presencia de ausencia.

local visible = opciones.visible
if visible == nil then visible = true end

El condicional en expresión falla por la misma razón, pero de forma más traicionera: se rompe cuando la rama verdadera produce false o nil, porque entonces el and la descarta y el or entrega la rama falsa.

local activo = true
print(activo and false or "por defecto")   --> por defecto, no false

Aquí no hay parche elegante: si la rama verdadera puede ser falsa, hay que usar un if de verdad. La regla operativa es escribir el idioma en expresión solo cuando puedas demostrar que el segundo operando nunca es nil ni false.

⚠️
El caché memorizado es la trampa más cara

El patrón local v = cache[k] or calcular(k) parece impecable hasta que calcular devuelve legítimamente false o nil: a partir de ahí el caché nunca acierta y recalcula en cada llamada, sin fallar, sin avisar y con un coste que solo aparece bajo carga. Cuando el valor almacenado puede ser falso, hay que preguntar if cache[k] == nil then.

Verdad o comparación explícita con nil

De todo lo anterior sale una disciplina sencilla y muy rentable. Pregunta por verdad cuando lo que te interesa es «hay algo utilizable aquí», y compara explícitamente con nil cuando lo que te interesa es «existe esta clave o llegó este argumento».

-- correcto: quiero saber si hay un manejador que invocar
local h = manejadores[evento]
if h then h(datos) end

-- correcto: quiero saber si la clave existe, aunque valga false
if config.depurar ~= nil then aplicar(config.depurar) end

-- casi siempre incorrecto: comparar con true
if config.depurar == true then end   -- descarta 1, "si" y cualquier otro verdadero

Comparar con true merece una advertencia aparte porque parece más explícito y en realidad es más frágil: no comprueba verdad, comprueba identidad con un valor booleano concreto, y por tanto rechaza todos los demás valores verdaderos. Salvo que estés validando que alguien te pasó literalmente un booleano, es casi siempre un error.

Queda un caso raro pero instructivo: NaN es verdadero como cualquier número, y sin embargo no es igual a sí mismo. Un valor puede pasar la condición y fallar la comparación, lo que demuestra que verdad e igualdad son preguntas independientes.

local n = 0/0
if n then print("verdadero") end   --> se imprime
print(n == n)                      --> false
Dos valores falsos es una decisión sobre el sistema de tipos, no sobre las condiciones

Conviene ver la regla de verdad de Lua como lo que realmente es: una postura sobre cuántos significados puede tener un valor a la vez. En los lenguajes donde el cero, la cadena vacía y la lista vacía son falsos, cada tipo trae consigo un elemento privilegiado que además significa «no». Eso convierte la condición en un mecanismo de despacho dependiente del tipo, y con ello introduce dos problemas permanentes. El primero es la colisión de dominios: un contador que vale cero, una consulta que devuelve cero filas y una cadena vacía leída de un formulario son datos perfectamente válidos que el lenguaje interpreta como negación, y para recuperarlos hay que escribir comparaciones explícitas con el valor vacío justo en el código que quería ser breve. El segundo es que la regla deja de ser memorizable: hay que saber si el objeto vacío de cada tipo, y de cada tipo definido por el usuario, cuenta como falso. Lua elimina las dos cosas de un plumazo al declarar que la falsedad no pertenece a ningún tipo de datos, sino a un tipo dedicado y a la ausencia. La condición vuelve a ser una pregunta uniforme que no consulta el tipo del valor, ni su tamaño, ni su metatabla, y por tanto puede compilarse a una comparación constante y describirse en una frase. El precio se paga en un solo sitio y hay que pagarlo consciente: como false y nil son ambos falsos pero solo uno de ellos significa ausencia, todo idioma construido sobre or está preguntando por falsedad cuando el programador quería preguntar por ausencia. No es un defecto de la regla, es la consecuencia exacta de haber separado bien los conceptos y luego haberlos unido otra vez en el operador.

⚔️ Rompe los idiomas a propósito
  1. Escribe una función con un parámetro booleano opcional cuyo valor por defecto sea verdadero e implementa la versión con or. Demuestra que ignora el false explícito.
  2. Corrige la función comparando con nil y verifica los tres casos: sin argumento, con true y con false.
  3. Construye un ejemplo donde el condicional en expresión devuelva la rama equivocada y razona qué operador la descartó.
  4. Comprueba que not 0, not "" y not {} son todos false, y explica en un comentario por qué eso es coherente con la regla.
  5. Implementa un caché que almacene correctamente valores false y demuestra con un contador de llamadas que no recalcula.