Lazy Function Definition Pattern

Design Patterns Éste artículo tiene como fin dar a conocer uno de los patrones de diseño en JavaScript, el cual ha demostrado ser muy eficiente en escenarios en donde necesitamos inicializar objetos, o hacer que una función ejecute una acción sólo la primera vez que sea invocada, o almacenar resultados de operaciones que se efectúan con cada llamado a la función (Memoization)

Una de las ventajas que logramos al implementar éste patrón, es conseguir un mejor rendimiento ya que se puede reducir el costo computacional si mantenemos los datos en cache (mediante un closure), y además podemos lograr que sólo se carguen los objetos cuando sean requeridos (lazy load / lazy evaluation)

Primero que todo vamos a repasar algunos conceptos que nos ayudarán a entender mejor el patrón Lazy Function Definition.

CLOSURE

Un closure es un tipo especial de objeto que combina dos cosas: una función, y el entorno en que se creó esa función. Es decir que una función definida dentro del closure “recuerda” el entorno en el que se ha creado y tiene acceso a las variables de ese entorno (el scope de la función padre).

Recordemos algunas propiedades de un closure (tomado del artículo JavaScript: Closures)

  • El closure permite encapsular el código.
  • El contexto de una función anidada incluye el scope de la función externa.
  • El entorno está formado por las variables locales dentro del ámbito (scope) cuando se creó el closure (variables libres).
  • Una función anidada sigue teniendo acceso al contexto de la función externa, incluso después de que ésta haya retornado.

Vamos a crear una función que rellene con ceros a la izquierda el texto proporcionado y nos permita configurar el total de caracteres a ser retornados por la función interna.

Code: closure
Image: closure

Lazy evaluation

Se conoce también como call-by-need y es una estrategia que retrasa la evaluación de una expresión hasta que su valor es requerido evitando realizar cálculos innecesarios. En ocasiones, el corto-circuito en la evaluación de expresiones boleanas también es llamado lazy, en donde el segundo argumento es evaluado sólo si el primer argumento resulta satisfactorio para el operador lógico.

    function test(nombre, edad) {
        //lazy evaluation
        if (nombre && edad && canVote(edad)) {
            //ejecuta alguna tarea compleja
            console.log("%c ejecuta alguna tarea compleja", "color: green");
        }
    }

    function canVote(edad) {
        //se realiza algún cálculo
        console.log("%c ejecuta canVote()", "color: blue");
        return edad > 17;
    }

Lazy initialization

También conocido como inicialización perezosa o inicialización tardía, es una técnica utilizada para retrasar la creación de un objeto, el cálculo de una operación, o algún otro proceso costoso hasta que sea necesitado la primera vez. La forma tradicional de lograr el objetivo, es mantener un flag (indicador) que nos permita saber si el objeto es requerido.

    function foo() {

        //t es el flag que determina
        //si el objeto fue requerido
        if (foo.t) {
            return foo.t;
        }

        console.log("inicializar");
        foo.t = new Date();
        return foo.t;
    }

Memoization

La evaluación tardía es a menudo combinada con la técnica memoization, en donde luego de calcular un valor según los parámetros de entrada de una función, el resultado se almacena internamente en una tabla que es indexada con los parámetros de la función; de manera que la próxima vez que se llame a la función, primero se consulta la tabla para verificar si el valor ya fue calculado anteriormente. Recomiendo leer el artículo One-Line JavaScript Memoization de Oliver Steele, el cual nos ayudará a comprender mejor este concepto.


Lazy Function Definition

Primeramente debemos tener en cuenta que éste es un patrón de diseño con el que conseguiremos reducir la ejecución de nuestro código haciendo que una porción del código sea ejecutada solo una vez y de allí que recuerde el resultado de una operación o el estado del closure.

Como todo patrón de diseño, debemos usarlo en el contexto adecuado, y una función puede implementar el patrón de función perezosa si requiere solucionar alguno de los siguientes problemas:

  • la función requiere inicializar objetos.
  • la función una vez evaluada, retornará el mismo valor en las próximas llamadas.
  • mantener el estado de objetos (closure) que serán utilizados por la función principal.

Veamos un ejemplo tonto para ilustrar el concepto:

//declaramos una función
function pushButton() {

    //redefinimos la función
    pushButton = function() {
        alert("pushButton() has been redefined");
    };

    //ésto será ejecutado sólo la primera vez
    //que se invoque la función pushButton()
    alert("First call to pushButton()");
}

pushButton(); //primer llamado
pushButton(); //segundo llamado

En el ejemplo anterior vemos que al ser llamada por primera vez, la función ejecutó el alert de la línea 11, pero en los siguientes llamados siempre ejecutará el alert de la línea 6, ésto ocurre porque en la línea 5 redefinimos el objeto almacenado en la variable pushButton

Teniendo en cuenta que en JavaScript las funciones crean un scope de privacidad, podemos crear objetos que determinen el estado interno de la función que vamos a redefinir, veamos ésto con más claridad en el siguiente ejemplo:

//declaramos una función
function pushButton() {

    //mantenemos el estado interno
    //de las veces que invoquemos la función
    var _calls = 1;

    //redefinimos la función
    pushButton = function() {
        _calls += 1;
        alert(_calls + " call to pushButton()");
    };

    //ésto será ejecutado sólo la primera vez
    //que se invoque la función pushButton()
    alert("First call to pushButton()");
}

pushButton(); //primer llamado
pushButton(); //2
pushButton(); //3

De manera sencilla hemos visto lo que es una función que se sobrescribe así misma. Sin embargo, en el patrón Lazy Function Definition, una función perezosa esta conformada de tres partes:

  1. Inicializar. Evaluar y hacer una serie de cálculos que determinan el valor a retornar.
  2. Redefinirse a sí misma. La función se sobrescribe a sí misma para evitar realizar de nuevo las operaciones efectuadas en el paso anterior.
  3. Autoinvocarse. La función se llama así misma para retornar el valor después de sobrescribirse.
function foo() {

    //1. inicializar
    console.log("inicializar");
    var t = new Date();

    //2. sobrescribirse
    console.log("sobrescribir");
    foo = function() {
        return t;
    };

    //3. autoinvocarse
    console.log("autoinvocar");
    return foo();
}

Hay escenarios en donde una función siempre va a retornar el mismo valor, por ejemplo cuando necesitamos verificar las capacidades del navegador. En este caso vamos a crear una función siguiendo el patrón Lazy Function Definition para determinar la propiedad de texto de un nodo DOM:

var getText = function (DOMNode) {
 
    //inicializar y redefinir condicionalmente
    getText =
        typeof DOMNode.innerText !== "undefined"
          ? function (DOMNode) {
                return DOMNode.innerText }
          : function (DOMNode) {
                return DOMNode.textContent };
 
    //autoinvocar
    return getText(DOMNode);
}

En el ejemplo anterior notamos que la función se sobrescribe dependiendo de una condición, y como la condición sólo es evaluada la primera vez que se invoque la función, entonces se define de acuerdo al resultado de esa condición.

A tener en cuenta

Si bien con este patrón podemos tardar la inicialización de objetos hasta que sean requeridos, esta ventaja tiene un precio, y el precio es que la función al ser redefinida pierde la transparencia referencial y deja de ser un objeto de primera clase.

Si quisiéramos pasar una función perezosa como argumento de otra función (callback), o la queremos asignar como método de un objeto, cada vez que sea invocada ejecutará siempre los tres pasos: inicializar, redefinirse, autoinvocarse, así que la ganancia en velocidad y rendimiento se verá afectada. Para solucionar este problema podemos optar por la inicialización perezosa (lazy initialization) mediante un closure condicional:

//begin IIFE
var foo = (function() {
    var some_complex;

    return function() {
        if (some_complex) {
            return some_complex;
        }

        //lazy initialization
        some_complex = new Date();
        return some_complex;
    }
})();
//end IIFE

A continuación vamos a trabajar un caso para demostrar porqué no se debe utilizar una función perezosa como método de un objeto. Consideremos que tenemos una función que recibe un número (1-12), y retorna un texto con el mes correspondiente:

function getMonth (n) {

    console.log("incializar");
    var months = [
        "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
        "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];
 
    console.log("redefinir");
    getMonth = function(n) {
        if (n < 1 || n > 12)
            throw new RangeError("Mes incorrecto");
        return months[n-1];
    };

    console.log("autoinvocar");
    return getMonth(n);
 }
 
console.log("%c1. " + getMonth(11), "color: green");
console.log("%c2. " + getMonth(12), "color: green");

Si ejecutamos esté código, podemos ver que en la consola sólo aparece los textos “incializar”, “redefinir”, “autoinvocar” la primera vez que se ejecuta la función. Hasta aquí nuestro patrón Lazy Function Definition funciona perfectamente.

Caso #1

Ahora vamos a modificar el código para que la función getMonth sea un método de un objeto (para crear el objeto vamos a utilizar el patrón Revealing Module)

//REVEALING MODULE PATTERN
var mylib = (function() {

    //LAZY FUNCTION DEFINITION PATTERN
    function getMonth (n) {

        console.log("incializar");
        var months = [
            "Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio",
            "Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
     
        console.log("redefinir");
        getMonth = function(n) {
            if (n < 1 || n > 12)
                throw new RangeError("Mes incorrecto");
            return months[n-1];
        };

        console.log("autoinvocar");
        return getMonth(n);
     }

     //API pública
     return {
        "getMonth": getMonth
     };

}());

console.log("%c1. " + mylib.getMonth(5), "color: green");
console.log("%c2. " + mylib.getMonth(9), "color: green");

Si ejecutaron este código en la consola, podrán observar que en los dos llamados al método se imprime los textos “incializar”, “redefinir”, “autoinvocar”, lo que significa que cada vez que invoquemos el método éste se inicializará de nuevo, perdiendo las virtudes que ofrecía el patrón Lazy Function Definition. Ésto ocurre porque al ser redefinida la función pierde las propiedades de ser un objeto de primera clase, y no va a conservar la misma dirección de memoria cuando fue creada por primera vez, sino que al sobrescribirse la función, un nuevo objeto es creado con una dirección de memoria diferente. Veámoslo mejor en la siguiente gráfica.

asignación de memoria

Bueno, ¿y porqué entonces getMonth sí funciona cuando es creado como una función, y en cambio no funciona cuando es asignado como método de un objeto?

La respuesta es, porque al ser asignado como método de un objeto, éste siempre va a conservar la referencia de la función cuando se creó por primera vez, es decir que esa referencia no se va a actualizar cuando se sobrescriba la función.

Caso #2

Lo mismo ocurre con el siguiente caso:

function getMonth (n) {

    console.log("incializar");
    var months = [
        "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio",
        "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"];
 
    console.log("redefinir");
    getMonth = function(n) {
        if (n < 1 || n > 12)
            throw new RangeError("Mes incorrecto");
        return months[n-1];
    };

    console.log("autoinvocar");
    return getMonth(n);
 }

var pointer = getMonth;
 
console.log("%c1. " + pointer(3), "color: green");
console.log("%c2. " + pointer(8), "color: green");

En esta ocasión sólo queríamos una “copia” de la función, pero al asignar la función a otra variable, lo que realmente hacemos es pasar su referencia en memoria, y al igual que en el caso #1, la variable pointer mantiene la referencia de getMonth cuando se creó por primera vez y esa referencia no se actualiza cuando getMonth se sobrescribe.


NOTA: Por eso es importante tener en cuenta no utilizar este patrón de diseño cuando queremos asignar la función perezosa por referencia, ya sea como callback, como método de un objeto, o simplemente asignarla a otra variable!


Lazy Function como método de un objeto

Para efectos académicos voy a demostrar cómo podemos utilizar el patrón Lazy Function Definition como método de un objeto, y vale la pena conocerlo, ya que algunas librerías hacen una buena implementación de ésta técnica, enfocada a sobrescribir métodos en la instancia de un objeto, conservando el método original en el prototipo del constructor de dicho objeto.

//define the method at prototype level
Geolocation.prototype.getNearSuggestions = function() {
    var suggestions = []; //expensive computation
 
    //override the method at instance level
    this.getNearSuggestions = function() {
        return suggestions;
    };
 
    return suggestions;
}
Ahora veamos el siguiente ejemplo: definiremos un método en el prototipo de un objeto, y redefiniremos el método en la instancia del objeto, y una vez se cumpla cierta condición, eliminaremos la referencia al método redefinido, restableciendo el método original del prototipo.
//define the method at prototype level
System.prototype.download = function (file) {
 
    //override the method at instance level
    this.download = downloadInProgress;
 
    //execute some actions
    requestDownload(file, {
        callback: function() {
            //the condition is fulfilled,
            //then deletes the method in the instance,
            //restoring the method in the prototype
            delete this.download;
        }
    });
}

function downloadInProgress() {
     alert("still downloading...");
}

En los ejemplos anteriores, la palabra reservada this es la clave que nos permite sobrescribir el método en la instancia actual y no en el prototipo.

Ahora vamos a modificar el código del caso #1 con el objetivo de lograr implementar el patrón Lazy Function Definition como método público de un objeto. No obstante les recuerdo que éste patrón de diseño no se debería usar para exponer métodos de un objeto porque de no manejarse adecuadamente podría ocasionar errores de lógica.

//Module pattern:
//Loose augmentation
var mylib = (function(context) {

    //Lazy function definition
    function getMonth (n) {

        console.log("incializar");
        var months = [
            "Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio",
            "Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
     
        console.log("redefine el método del objeto");
        context.getMonth = function(n) {
            if (n < 1 || n > 12)
                throw new RangeError("Mes incorrecto");
            return months[n-1];
        };

        console.log("invoca el método redefinido");
        return context.getMonth(n);
     }

     //API pública
     context.getMonth = getMonth;
     return context;

}(mylib || {})); //loose augmentation

console.log("%c1. " + mylib.getMonth(5), "color: green");
console.log("%c2. " + mylib.getMonth(9), "color: green");

Si ejecutan el código en la consola, verán que a pesar de estar expuesto como método de un objeto, el patrón Lazy Function Definition trabaja correctamente. Para lograrlo, se utilizó el patrón Module: loose augmentation en el cual pasamos como argumento el objeto mylib que será el módulo (líneas 3 y 28). Ahora dentro del módulo, en vez de usar this para referirnos al objeto, emplearemos la variable context.

Para lograr que la función perezosa opere correctamente con un objeto, debemos rescribir la propiedad del objeto que contiene la referencia a la función original, es decir context.getMonth (línea 14) y retornamos el método redefinido (línea 21). Finalmente asignamos la función original al objeto (línea 25) y retornamos el módulo.

Conclusión

El patrón Lazy Function Definition es una técnica que nos permite retrasar la creación de objetos hasta cuando sean requeridos, permitiendo una ganancia en el rendimiento de la aplicación, y como la función es redefinida por una versión mas corta que accede a los objetos ya inicializados en el closure, entonces la función perezosa se convierte en una versión óptima y más rápida de la función original.

Éste es un patrón de diseño que vale la pena trabajar; cada vez que pensemos en mejorar el desempeño de una función, podemos recurrir a él, y si de algún modo queremos evitar el problema de referencia en memoria, podemos optar por la solución de un closure condicional o utilizar el patrón Module: loose augmentation para asegurar el contexto de ejecución de la función perezosa, o redefinir el método en la instancia de un objeto, preservando el método original en el prototipo.

Hay muchas formas en las que se puede aplicar este patrón de diseño, lo importante es siempre tener en cuenta el contexto o problemas que soluciona, sus limitaciones, y como cualquier patrón de diseño, puede ser combinado con otros para construir soluciones más robustas.

Como siempre y para finalizar, dejo un artículo recomendado: One-Line JavaScript Memoization de Oliver Steele.

Happy Coding!

2 thoughts on “Lazy Function Definition Pattern

Comentarios

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s