domingo, 3 de febrero de 2013

Evaluación perezosa de operadores && y || (lazy evaluation)

¿Que es la evaluación perezosa?

La evaluación perezosa consiste, como siempre que hablamos de pereza, en hacer el mínimo trabajo posible. Es decir, una expresión no se evalúa hasta que realmente se necesita.

JavaScript lo utiliza sólo puntualmente, pero hay lenguajes, como Haskell, que lo utilizan siempre. Podemos ver la diferencia con una función que tiene otras funciones como parámetros (función de orden superior):

function ( func1(), func2(), func3() ) {
   ...
}

Un lenguaje sin evaluación perezosa ejecutaría las funciones func1(), func2() y func3() para obtener los parámetros finales y despues continuaría con el cuerpo de la función. En cambio un lenguaje con evaluación perezosa empieza con el código de la función sin evaluar previamente las tres funciones. Las irá evaluando cuando aparezcan en el código y sean realmente necesarias. De esta forma, si un argumento no se utiliza, nunca será evaluado ( puede ocurrir que alguno de los parámetros esté dentro de un if que no se cumple ).

JavaScript no es realmente un lenguaje que presente lazy evaluation , salvo para los operadores && y || como veremos a continuación. Haskell sí es un lenguaje puramente 'perezoso' y en el ejemplo anterior no llamaría a las funciones de los parámetros hasta que fuera necesario.

Lo contrario de lazy evaluation es eager evaluation ( algo así como evaluación ansiosa ) y es lo más habitual en los lenguajes de programación.

Comportamiento de && y || en JavaScript

En JavaScript los operadores || y && funcionan como lazy evaluation, aunque en realidad a este comportamiento con los operadores lógicos se le llama evaluación mínima o evaluación de cortocircuito ( short circuit evaluation ). Veamos un ejemplo:

var test = ("a" === "b") || (1===1) || ("c" === "h");  

//test is true

Las expresiones lógicas se evalúan siempre de izquierda a derecha. Sabemos que con el operador ||, cuando uno de los términos sea true el resultado será true. Esto significa que cuando encontramos uno verdadero ya no es necesario evaluar nada más, podemos asignar el valor a la variable 'test' sin necesidad de hacer más trabajo. Este es exactamente el comportamiento de JavaScript.

En el ejemplo anterior, la primera expresión ("a" === "b") es falsa, por lo que JavaScript evaluará la siguiente. (1===1) es verdadera, con lo que el resultado final ya está claro, no se evaluará ninguna expresión más.

En el caso del operador &&, cuando uno de los términos sea false el resultado será false:

var test = ("a" === "b") && (1===1) && ("c" === "h");

//test is false

Como ("a" === "b") es falsa, no se evalúa nada más, directamente se asigna test = false.

Este comportamiento, unido a la peculiaridad de que estos operadores pueden devolver valores no booleanos, hace que puedan utilizarse como una forma simplificada de ejecución condicional, como veremos a continuación.

&& y || pueden devolver valores no booleanos

Hay una particularidad más de JavaScript con respecto a estos operadores ( && y || ): que devuelven el valor real del término que se evaluó como true or false, aunque no sea booleano. En los ejemplos que vimos antes, todos los términos devolvían al final a un valor booleano, pero puede no ser así. Podemos tener funciones, objetos, strings o valores de otro tipo:

var number = 0;
var test =     ( 12 * number ) || "hola" || getPrice() ; 

//test is "hola"

La clave es que cualquier valor en JavaScript es convertible implícitamente en booleano cuando está en una expresión lógica. Sólo los siguientes valores son false:

  • 0
  • ""
  • false
  • NaN
  • null
  • undefined

Todos los demás valores se convierten en true.

En la expresión de arriba tenemos estos 3 términos:

( 12 * 0 ) //el resultado es 0 que se convierte implícitamente en false

"hola" // true

getPrice() // depende del resultado, pero no se ejecutará nunca

Como JavaScript devuelve el valor real de la última expresión que evalúa y no su conversión implícita a booleano, devuelve "hola".

Este comportamiento se utiliza, por ejemplo, para dar valores por defecto a variables:

var nombre = param1 || "Desconocido";
//Si `param1` no existe se asigna el valor "Desconocido"

o para asegurarnos de que un objeto existe antes de acceder a alguna de sus propiedades:

var nombre = empleado && empleado.getName();
//si el objeto 'empleado' no existe la segunda parte no se ejecuta y así evitamos un error.

¿Porqué no se utiliza siempre la evaluación perezosa?

A pesar del término perezosa, realmente este tipo de funcionamiento puede ser más costoso, porque requiere llevar un control del estado de las expresiones, para saber si están ya evaluadas o es la primera vez que se encuentran. Cuando no se utiliza no tenemos que llevar este tipo de control, porque las expresiones se evaluan siempre.

Además implica una cierta perdida de control porque no es evidente qué parte del código se va a ejecutar en cada caso. Esto puede suponer un problema cuando necesitamos controlar estados y puede llevar a errores difíciles de encontrar.


Fuentes:

Wikipedia: Evaluación de cortocircuito
Lazy evaluation - evaluación perezosa
Lazy evaluation
Why isn't lazy evaluation used everywhere