VISITAS:

viernes, 14 de febrero de 2014

Programación funcional en LUA

Las funciones como valores

En LUA, las funciones son valores con las mismas propiedades que el resto de valores (strings, números, tablas, etc). Las funciones se pueden almacenar en variables (globales o locales) y en tablas. Se pueden pasar funciones como argumentos y se pueden retornar funciones.
Una propiedad muy importante de las funciones en LUA es que tienen "lexical scoping". Esto significa que una función puede acceder a las variables de las funciones que le rodean. O sea, si una función f1 contiene internamente a otra función f2(), entonces todas las variables que se declaran en el scope de f1 pueden ser accedidas desde la función f2. Esto que parece algo sin importancia, se convertirá en LUA en algo fundamental para la programación funcional.
Las funciones en LUA son objetos anónimos, como el resto de valores, o sea, no tienen nombre. Cuando hablamos de la función print, realmente estamos hablando de una variable a la que se le ha asignado una función (valor) que muestra texto por consola.
Habitualmente, cuando escribimos una función, lo hacemos con la siguiente sintaxis:
function suma(a,b)
    return a + b;
end
Sin embargo, esto es una forma de escribir lo que realmente estamos haciendo, que es lo siguiente:
suma = function (a,b)
    return a + b;
end
Por consiguiente, una definición de función es realmente una sentencia de asignación que crea un valor de tipo "function" y lo asigna a  una variable.
Aunque lo normal es construir un valor "function" y asignarlo a una variable, hay ocasiones en que nos interesa construir la función y que permanezca anónima. Veamos un ejemplo.
La librería table ofrece una función table.sort() que recibe como argumento una tabla y ordena sus elementos. Esta función debe permitir ordenación ascendente, descendente, numérica, por fecha, etc de los elementos de la tabla. Para ello, en lugar de existir cientos de variantes de esta función, lo que se hace es pasar un segundo argumento (opcional) que es una función para comparar dos valores. Por ejemplo, supongamos la siguiente tabla de alumnos con sus notas:
alumnos = {
    { nombre="Luis", nota=6.3 },
    { nombre="Juan", nota=8.6 },
    { nombre="María", nota=9.8 }
};
Para ordenar esta tabla por nombre de alumno:
table.sort(alumnos, function (a1, a2) return a1.nombre < a2.nombre; end );
También podemos ordenar la tabla por nombre pero de forma descendente:
table.sort(alumnos, function (a1, a2) return a1.nombre > a2.nombre; end );
Las funciones que reciben como argumento otra función se denominan high-order functions.
Otro ejemplo de función high-order es el cálculo de la derivada de una función matemática en un punto. La derivada de una función f() en un punto es ( f(x+d) - f(x) ) / d
function derivada(f, d)
    d = d or 0.0001;
    return function (x)
        return (f(x+d) - f(x)) / d;
    end
end
De esta forma, podemos tener la función derivada de sen(x):
d_seno = derivada(math.sin);
Las funciones se pueden almacenar no sólo en variables globales, sino también en variables locales e incluso en tablas (esto dará lugar a los módulos y programación orientada a objetos en LUA).

Closures

Cuando escribimos una función dentro de otra función, la función interna tiene acceso a todas las variables de la función contenedora, incluidas las variables locales. A esta característica le llamamos lexical-scoping. Aunque esto suena bastante obvio, realmente no lo es, si lo combinamos con el hecho de que las funciones son valores como el resto de valores del lenguaje.
Veamos un ejemplo:
newCounter = function ()
    local i = 0;
    return function ()
        i = i + 1;    -- incrementa la variable local externa i
        return i;
    end
end
Esta función (que se asigna a la variable newCounter) crea y retorna una función anónima que incrementa el valor de la variable i (local a la función contenedora). ¿Qué ocurre cuando utilizamos newCounter para crear nuevas funciones? Cuando invoquemos a la nueva función construida, ¿cómo accederá a la variable i?
Veamos un ejemplo de uso:
func1 = newCounter();
print(func1());     -- 1
print(func1());     -- 2
Lo interesante en este ejemplo es que cuando invocamos a la función construida y retornada por newCounter(), se ejecuta una función que accede a una variable que era interna a su función contenedora, la variable i. Pero entonces, esa variable i, ya ha desaparecido ¿no? Ya que era una variable local a la función newCounter() y newCounter ya no existe. ¿Que ha ocurrido?
LUA maneja esta situación adecuadamente, mediante el concepto de closure. Para simplificar, un closure es una función más todo el entorno que la rodea. En este caso, la función retornada por newCounter() es un closure, que incluye la propia función anónima y la variable i.
Por ejemplo, si creamos otra nueva función anónima volviendo a invocar a newCounter(), se vuelve a retornar un closure nuevo con otra variable i:
func2 = newCounter();
print(func2());    -- 1
print(func1());    -- 3
print(func2());    -- 2
Esto ocurre porque func1 y func2 son dos closures distintos, cada uno con su propia instancia de la variable i.
Hablando técnicamente, los valores son closures, no las funciones.
Si la función newCounter() recibiera por ejemplo un argumento, ese argumento pasaría al closure:
newIncrementer = function (inc)
    local i = 0;
    return function ()
        i = i + inc;
        return i;
    end
end
En este caso, el closure incluye la función anónima, la variable i y el argumento inc.
Un ejemplo aclaratorio de los closures:
a = { };local x = 20;for i = 1, 10 do    local y = 0;    a[i] = function() y = y + 1; return x + y; end;end
En este ejemplo, el bucle for crea 10 closures (10 instancias de una función anónima). Cada uno de estos closures tiene una instancia de la variable y que es distinta. Sin embargo, todos los closures comparten la misma variable x.

Funciones en las tablas

Una consecuencia obvia de que las funciones (realmente closures) son valores como otro cualquiera es que podemos almacenarlas en variables locales, variables globales y en tablas.
Almacenar funciones en tablas es algo muy común en LUA. Por ejemplo, cuando llamamos a math.sin(2.56) estamos invocando a un miembro de la tabla math que se llama sin y es una función.
Para crear una tabla que tenga miembros función hacemos lo siguiente:
util = { };
util.suma = function (x, y) return x + y; end
util.resta = function (x, y) return x - y; end
De igual forma, también podemos utilizar el constructor de la tabla:
util = {
    suma = function (x, y) return x + y; end,
    resta = function (x, y) return x - y; end
};
Además, LUA ofrece una tercera alternativa para tablas con miembros función:
util = { };
function util.suma(x, y) return x + y; end
function util.resta(x, y) return x - y; end
En cualquiera de los casos, la invocación a las funciones de la tabla util sería de la siguiente forma:
util.suma(3, 2);
util.resta(4,7);

Funciones locales

Cuando almacenamos una función en una variable local, dichas funciones sólo pueden ser utilizadas en el scope en que se definen:
local func1 = function (a, b) return a ^ b; end
local func2 = function (x) return f(x, x); end
Esto es muy útil en los packages, donde las funciones del package (scope) pueden utilizar las otras funciones locales








No hay comentarios:

Publicar un comentario