wandres.dev
FUNCIONES · valores de primera clase

Las funciones son valores: declarar es asignar

En Lua no existe la declaración de funciones como categoría sintáctica propia: existe la creación de un valor de tipo función y su asignación a algún sitio donde puedas volver a encontrarlo. Esta lección desmonta el azúcar de la forma con nombre para mostrar la asignación que hay debajo, estudia la función anónima como la construcción primitiva y verdadera del lenguaje, examina qué significa que una función viva como campo de una tabla y por qué eso hace que módulos, objetos y espacios de nombres sean todos la misma cosa, y analiza el detalle decisivo por el que la forma local con nombre no es equivalente a una asignación local corriente sino a una declaración previa seguida de asignación, matiz sin el cual la recursión directa dejaría de funcionar. Al final se argumenta por qué esta ausencia de una categoría separada para las funciones, lejos de ser una simplificación cosmética, es la condición que permite que el lenguaje entero se sostenga sobre dos ideas.

⏱ 17 min

Casi todos los lenguajes que has usado tratan las funciones como una categoría aparte. Hay declaraciones, que ocurren en tiempo de compilación y viven en un espacio propio, y hay variables, que ocurren en tiempo de ejecución y guardan datos. Lua no hace esa distinción en ningún punto de su semántica. Una función es un valor exactamente en el mismo sentido en que lo es el número siete: se crea evaluando una expresión, se guarda donde quieras, se copia, se pasa, se devuelve, se compara por identidad, se usa como clave de una tabla y se descarta cuando el recolector deja de encontrarle referencias. Esta lección abre el nivel entero desmontando la pieza de azúcar sintáctico que oculta ese hecho, porque hasta que no ves la asignación escondida detrás de la palabra function con nombre, la mitad de los idiomas de Lua parecen magia arbitraria en lugar de lo que realmente son: consecuencias inevitables de que aquí no haya nada especial en una función.

🎯 Al terminar esta lección sabrás
  • Reescribir cualquier declaración con nombre como la asignación equivalente y explicar qué parte es azúcar y qué parte es semántica.
  • Manejar la función anónima como construcción primitiva, y reconocer que la forma con nombre no añade ninguna capacidad.
  • Colocar funciones como campos de tabla y entender por qué módulo, objeto y espacio de nombres son la misma estructura.
  • Explicar por qué local function no equivale a una asignación local y qué se rompería si lo fuera.

Declarar es asignar

La expresión primitiva del lenguaje es la función anónima: la palabra function, una lista de parámetros entre paréntesis, un cuerpo y la palabra end. Eso es una expresión, y evaluarla produce un valor nuevo. Todo lo demás que parece una declaración es una abreviatura de asignar ese valor a algún sitio, y las abreviaturas están definidas en el manual de referencia como transformaciones puramente sintácticas.

-- La forma primitiva: una expresion que produce un valor
local doble = function(n) return n * 2 end

-- El azucar con nombre: identico a la linea de arriba
local function doble(n) return n * 2 end

-- Sin local, la asignacion va a una variable global
function triple(n) return n * 3 end
-- equivale a:  triple = function(n) return n * 3 end

-- Y como es un valor, se comporta como cualquier otro valor
local alias = doble
print(type(doble))        --> function
print(alias == doble)     --> true, es el mismo valor
print(alias(21))          --> 42

-- Dos evaluaciones de la misma sintaxis producen valores distintos
local a = function() end
local b = function() end
print(a == b)             --> false

Merece la pena detenerse en las dos últimas líneas, porque revelan que no estamos ante nombres sino ante objetos. Cada vez que el flujo de ejecución pasa por una expresión function se crea un valor nuevo, con su propia identidad. Si esa expresión está dentro de un bucle, el bucle fabrica una función distinta en cada vuelta. Esto no es un detalle académico: es la base de los closures del nivel siguiente y la causa de que las funciones creadas dentro de un bucle capturen variables distintas.

ℹ️
El azúcar no añade poder, añade legibilidad

Toda la sintaxis de declaración de Lua se puede eliminar del lenguaje sin perder una sola capacidad. Si borras la forma con nombre, sigues pudiendo escribir cualquier programa usando expresiones anónimas y asignaciones. Eso es la definición operativa de azúcar sintáctico, y es la mejor prueba de que las funciones no ocupan un lugar privilegiado en la semántica. Compáralo con un lenguaje donde la palabra clave de declaración hace algo que ninguna asignación puede reproducir: allí la función sí es una categoría aparte.

Campos de tabla: el módulo, el objeto y el espacio de nombres

Si una función es un valor, puede ser el valor asociado a una clave de una tabla. Ahí termina, literalmente, toda la teoría de módulos y objetos de Lua. Un módulo es una tabla cuyos campos son funciones. Un objeto es una tabla cuyos campos son funciones. Un espacio de nombres es una tabla cuyos campos son funciones. No hay tres mecanismos, hay uno, y la diferencia entre los tres es únicamente qué haces con la tabla después.

-- Construida a mano, campo a campo
local geometria = {}
geometria.area = function(r) return math.pi * r * r end
geometria.perimetro = function(r) return 2 * math.pi * r end

-- El azucar con punto: mismo resultado
function geometria.diametro(r) return 2 * r end

-- Tambien en el constructor de tabla, todo de una vez
local vector = {
  cero  = function() return 0, 0 end,
  sumar = function(ax, ay, bx, by) return ax + bx, ay + by end,
}

-- Y anidado a cualquier profundidad
local app = { util = { texto = {} } }
function app.util.texto.recortar(s) return (s:gsub("^%s+", "")) end

-- La tabla no sabe que contiene funciones: son campos como cualquier otro
for clave, valor in pairs(geometria) do
  print(clave, type(valor))
end

Nada de esto requiere una palabra clave de módulo ni un sistema de clases. El fichero de un módulo suele crear una tabla local, rellenarla de funciones y devolverla con return; quien lo carga con require recibe esa tabla y accede a los campos. El resultado es que exportar es asignar y que importar es leer un campo, sin ninguna maquinaria intermedia.

🧮

Una expresión, un valor

Cada evaluación de function fabrica una función nueva con identidad propia. Dos funciones sintácticamente idénticas nunca son iguales.

🍬

Azúcar eliminable

La forma con nombre, la forma con punto y la forma con dos puntos son reescrituras. Ninguna añade una capacidad que la asignación no tenga.

📚

Módulo igual a tabla

No hay sistema de módulos ni de clases. Hay tablas con funciones dentro y convenciones sobre qué hacer con ellas.

🔑

Valor de pleno derecho

Una función se guarda, se pasa, se devuelve, se compara por identidad y sirve como clave de una tabla igual que cualquier dato.

El matiz de local function y la recursión

Aquí aparece la única asimetría real del capítulo, y es importante porque explica un fallo que desconcierta a quien la desconoce. La forma local con nombre no se traduce como una asignación local corriente: se traduce como una declaración de la variable local seguida de la asignación, en dos pasos.

-- Esto NO compila como esperarias
local factorial = function(n)
  if n <= 1 then return 1 end
  return n * factorial(n - 1)   -- factorial aqui es la GLOBAL, aun nil
end

-- La forma con nombre declara primero y asigna despues
local function factorial(n)
  if n <= 1 then return 1 end
  return n * factorial(n - 1)   -- ahora si: la local ya existe
end

-- Equivalencia exacta de la forma con nombre
local factorial
factorial = function(n)
  if n <= 1 then return 1 end
  return n * factorial(n - 1)
end

-- Para recursion mutua hay que declarar las dos antes
local par, impar
function par(n)   if n == 0 then return true  else return impar(n - 1) end end
function impar(n) if n == 0 then return false else return par(n - 1)   end end

El motivo es de ámbito léxico, no de funciones. En la primera versión, el cuerpo se compila antes de que la variable local exista, de modo que el nombre se resuelve como global y en tiempo de ejecución vale nil. La forma con nombre existe precisamente para que el caso más común, la recursión directa, funcione sin obligarte a escribir la declaración previa a mano.

flowchart LR
A[Expresion function] --> B[Valor de tipo funcion]
B --> C[Variable local]
B --> D[Variable global]
B --> E[Campo de tabla]
B --> F[Argumento o retorno]
style B fill:#a6e3a1,color:#11111b
style E fill:#89b4fa,color:#11111b
Quitar la declaración es lo que permitió que el lenguaje cupiera en una tarde

Conviene medir bien lo que se gana renunciando a que la función sea una categoría del lenguaje, porque la decisión parece un ahorro de gramática y en realidad es la viga que sostiene casi todo lo demás. Si una función es solo un valor, entonces no hace falta inventar nada para pasarla como argumento: el mecanismo de paso de argumentos ya existía. No hace falta inventar los punteros a función, ni las clases anónimas, ni las interfaces de un solo método, ni los delegados, ni ninguna de las construcciones que otros lenguajes añaden justamente para recuperar lo que perdieron al declarar que las funciones eran especiales. Tampoco hace falta un sistema de módulos, porque exportar es escribir un campo en una tabla y las tablas ya sabían guardar cualquier valor. Tampoco hace falta un sistema de clases, porque un método es una función guardada en una tabla y el despacho es la búsqueda de una clave, que la tabla ya sabía hacer. Tampoco hacen falta las callbacks como concepto separado, ni los decoradores como sintaxis, ni un mecanismo de reemplazo en caliente: si la función es un valor, sustituir el comportamiento de un programa en marcha es asignar a un campo, y ese es literalmente el modelo de extensión de Neovim, de los motores de juego que embeben Lua y de los servidores que lo usan como capa de configuración. La ganancia acumulada es de una naturaleza distinta a la de ir añadiendo características: cada cosa que el lenguaje no tiene es también una interacción que no hay que especificar, un caso especial que no hay que documentar y una excepción que no hay que aprender. Por eso el manual de referencia de Lua se lee en una tarde y por eso la palabra function aparece en él con la misma naturalidad que un literal numérico. La potencia no vino de darle a la función un estatuto propio, vino de negárselo.

⚔️ Desmonta el azúcar hasta que no quede ninguno
  1. Escribe un fichero con cuatro funciones declaradas en las cuatro formas: local con nombre, global con nombre, con punto sobre una tabla y anónima asignada. Reescríbelas todas como asignaciones puras y comprueba que el programa se comporta igual.
  2. Crea dos funciones vacías idénticas y compáralas con el operador de igualdad. Explica el resultado en términos de identidad de valores.
  3. Fabrica funciones dentro de un bucle de tres vueltas, guárdalas en una tabla y comprueba con comparaciones que son tres valores distintos.
  4. Provoca a propósito el fallo de la recursión escribiendo la versión con asignación local corriente. Lee el error, identifica que el nombre se resolvió como global y arréglalo de las dos maneras posibles.
  5. Construye un módulo de tres funciones que se devuelva a sí mismo como tabla, cárgalo desde otro fichero y sustituye en caliente una de sus funciones asignando al campo. Razona por qué eso no rompe nada.
  6. Usa una función como clave de una tabla y guarda ahí una descripción. Explica qué está usando la tabla para localizarla.