Funciones puras en javascript y sus beneficios

haciendo puro a JavaScript

Una vez que su sitio web o aplicación va mas alla de un par de numeros de lineas, se hace inevitable que puedan contener errores de algun tipo. Esto no es especifico de JavaScript pero si es compartido por casi todos los lenguajes- es muy complicado, si no imposible, poder descartar completamente la chance de que nuestro codigo no tenga bugs de ningún tipo. Sin embargo, esto no quiere decir que no podamos tomar precauciónes programando de una manera que disminuya nuestra vulnerabilidad a los errores.

Funciónes puras e impuras.

Una función pura se define como una función que no depende de variables externas a su ambito. Esto es un poco duro de entender al comienzo, por lo que vamos a sumergirnos en un poco de codigo para un ejemplo mas practico.

Tomemos esta función que calcula si el mouse esta en el lado izquierdo de una pagina, y muestra por consola usando console.log si es true (esta al lado izquierdo) o false (no esta en el lado izquierdo). En realidad su función seguramente seria mas compleja y un poco mas trabajada, pero este ejemplo hace un gran trabajo para la demostración:

1
2
3
4
5
6
7
function mouseOnLeftSide(mouseX) {
return mouseX < window.innerWidth / 2;
}
document.onmousemove = function(e) {
console.log(mouseOnLeftSide(e.pageX));
};

mouseOnLeftSide() toma la coordenada x y comprueba si es menos de la mitad del ancho de la ventana- lo que la colocaria en el lado izquierdo. Sin embargo, mouseOnLeftSide() no es una función pura. Lo sabemos porque dentro del cuerpo de la función hace referencia a un valor que no se le paso de forma explicita:

1
return mouseX < window.innerWidth / 2;

Se le da a la función mouseX, pero no window.innerWidth. Esto significa que la función esta pudiendo acceder a los datos que no se le dio, y por lo tanto no es pura.

El problema con las funciónes impuras.

Seguramente usted se estara preguntando porque esto es un problema- esta pieza de codigo trabaja bien y hace el trabajo que se espera de ella. Imaginese que usted obtenga un informe de error de un usuario de que cuando la ventana esta a menos de 500px de ancho la función es incorrecta. Como probaria esto? Tiene dos opciones:

  • Se puede probar manualmente mediante el navegador, cargando la pagina y redimencionandola y moviendo el mouse hasta que haya encontrado el problema.

  • Podria escribir algunas pruebas unitarias (Rebecca Murphey’s Writing Testable JavaScript es una gran introducción) no solo para rastrear los errores, sino para asegurarse de que nunca vuelvan a suceder.

Interesados en tener una prueba en el lugar para evitar este error recurrente, elegimos la segunda opción y escribimos el test. Ahora nos enfrentamos a un nuevo problema, porque: ¿como podeos configurar nuestra prueba correctamente?. Sabemos que necesitamos para configurar nuestra prueba con la anchura de la ventana esablecida a menos de 500px, pero ahora?. La función se basa en window.innerWidth, y asegurarse de que eso pasa solamente en un valor concreto va a ser un dolor.

Beneficios de las funciónes puras.

La prueba mas simple

Con esa cuestión de como probar nuestra función en mente, imaginemos que nos decidimos a reescribir nuestra función de esta manera:

1
2
3
4
5
6
7
function mouseOnLeftSide(mouseX, windowWidth) {
return mouseX < windowWidth / 2;
}
document.onmousemove = function(e) {
console.log(mouseOnLeftSide(e.pageX, window.innerWidth));
};

La diferencia clave aqui es que mouseOnLeftSide() ahora toma 2 argumentos: La posicion X del mouse y el ancho de la ventana. Esto significa que mouseOnLeftSide() es ahora una función pura; todos los datos que necesita para funcionar les son pasados explicitamente a trabes de sus parametros de entrada, y nunca tiene que llegar a acceder a los datos fuera de su ambito.

En terminos de funcionalidad, esto es identico al ejemplo anterior, pero hemos mejorado drasticamente su capacidad de mantenimiento y su capacidad de prueba. Ahora no tenemos que usar un mock, o hacer un fake alrededor de window.innerWidth para poder probarlo, ahora simplemente podemos llamar a mouseOnLeftSide() con los argumentos exactos que necesitamos:

1
mouseOnLeftSide(5, 499) // nos aseguramos que funciona con un ancho < 500px

Auto documentada.

Ademas de ser mas facil para testear, las funciónes puras tienen otras caracteristicas que hace que valga la pena usarlas siempre que sea posible. Por su propia naturaleza, las funciónes puras son auto-documentadas. Si usted sabe que una función no llega a salir de scope para obtener los datos, usted sabe que los unicos datos que puede tocar son los pasados como argumentos. Considere la siguiente declaración de función:

1
function mouseOnLeftSide(mouseX, windowWidth)

Usted sabe con solo ver la declaración, que esta función solo ocupa 2 piezas de datos, y si los argumentos estan bien nombrados, debe de quedar claro que son con solo ver la declaración. Todos tenemos que lidiar con el dolor de tener que volver a tocar codigo que yace sin tocar durante 6 meses o mas tiempo, y ser capaces de familiarizare con el codigo rapidamente es una habilidad clave.

Evitar variables globales en el codigo.

El problema de las variables globales esta bien documentado en JavaScript. El lenguaje hace que sea trivial almacenar datos de forma global, donde todas las fnuciones pueden acceder a ellos. Esto es una fuente comun de errores, tambien, cualquier cosa podria haber cambiado el valor de una variable global, y por lo tanto las funciones pueden comportarse de manera diferente a lo esperado sin que nos percatemos de ello.

Una propiedad adicional de las funciónes puras es la transparencia referencial. Este termino que parece bastante complejo, tiene un significado muy simple: dadas las mismas entradas, la salida de la función es siempre la misma. Volviendo a mouseOnLeftSide echemos un vistazo a la definicion de la primera función que teniamos:

1
2
3
function mouseOnLeftSide(mouseX) {
return mouseX < window.innerWidth / 2;
}

Esta función no es referencialmente transparente. Por ejemplo podriamos llamarla con 5 como entrada varias veces, redimencionar la ventana entre llamadas, y el resultado seria diferente cada vez. Este es un ejemplo un poco artificial, pero las funciones que devuelven valores diferentes incluso cuando sus entradas son las mismas siempre resultan mas dificiles de trabajar. El razonamiento acerca de ellas es mas dificil puesto que no se puede garantizar su comportamiento. Por el mismo motivo, probar este tipo de funciones es mucho mas complicado porque no tenemos un control total sobre los datos con los cuales la función opera.

Por otra parte, nuestra función mejorada mouseOnLeftSide es referencialmente transparenteporque todos sus datos llegan a trabes de sus parametros y nunca llega fuera de su scope u ambito.

1
2
3
function mouseOnLeftSide(mouseX, windowWidth) {
return mouseX < windowWidth / 2;
}

Se obtiene transparencia referencial de forma gratuita cuando seguimos la regla de declarar todos sus datos como argumentos, y al hacer esto, se elimina toda una clase de bugs alrededor de los efectos secundarios y las funciones que actuan de forma inesperada. Si usted tiene el control total sobre los datos, se puede cazar y replicar los errores de forma mucho mas rapida y fiable sin necesidad de tener que cambiar toda la loteria de variables globales que pueden llegar a interferir.

La eleccion de las funciónes que deben ser puras.

Lamentablemente es imposible tener funciones puras consistentemente- siempre habra un momento que usted necesitara buscar los datos fuera del ambito de las funciones, el ejemplo mas común de esto es cuando tenemos que acceder al DOM para agarrar un elemento para interactuar con él. Es un echo de que en JavaScript vamos a tener que hacer eso. Y usted no tiene porque sentirse mal por llegar fuera del ambito de su función. En su lugar, debe considerar como reestructurar el codigo de manera que las funciónes impuras se puedan aislar. Impedirles tener amplios efectos a trabes de su codigo fuente, y tratar de usar funciones puras siempre que sea apropiado.

Vamos a echar un vistazo al codigo de abajo, que toma un elemento de DOM y cambia su color de fondo a rojo:

1
2
3
4
5
6
7
function changeElementToRed() {
var foo = document.getElementById('foo');
foo.style.backgroundColor = "red";
}
changeElementToRed();

Hay 2 problemas con este pedazo de codigo, pero solventable al hacer la transición a una función pura:

  • Esta función no es reutilizable en lo absoluto, ya que esta directamente vinculada a un elemento especifico de DOM, si quisieramos volver a utilizarla para cambiar otro elemento, no podremos hacerlo.

  • Esta función es dificil de probar debido a que no es pura. Para testearla, tendriamos que crear un elemento con un ID especifico en lugar de cualquier elemento generico.

Teniendo en cuenta los 2 puntos anteriores, me gustaria volver a reescribir esta funcion para:

1
2
3
4
5
6
7
8
9
10
function changeElementToRed(elem) {
elem.style.backgroundColor = "red";
}
function changeFooToRed() {
var foo = document.getElementById('foo');
changeElementToRed(foo);
}
changeFooToRed();

Ahora hemos cambiado changeElementToRed() para no estar atado a un elemento especifico del DOM y que sea mas generica. Al mismo tiempo, hemos echo que sea pura, dado que nos trae todos los beneficios discutidos anteriorente.

Es importante señalar, sin embargo, que todavia tengo algo de codigo impuro-changeFooToRed() es impuro. Nunca se puede evitar del todo esto. Pero esto es acerca de detectar la oportunidad de volver una funcion pura para incrementar la legibilidad, reusabilidad, y testeabilidad. Manteniendo los lugares donde operan las funciones impuras al mimimo y creando funciones mas puras y reutilizables como sea posible, se ahorrara una gran cantidad de dolor en un futuro y su codigo mejorara considerablemente.

Conclusión

“Funciónes puras”, “transparencia referencial”, “efectos secundarios”, son terminos comunmente asociados con los lenguajes de programacion funcionales puros, pero eso no significa que no podemos tomar esos principios y asociarlo a nuestros programas en JavaScript tambien. Al ser consciente de estos principios y aplicarlos con prudencia el código puede beneficiarse de ellos y obtener más fiabilidad, auto-documentacion que hace que sea mas facil de trabajar y se rompe con menos frecuencia. Les animo a tener todo esto en cuenta la proxima vez que esteos escribiendo nuevo codigo JavaScript o incluso cuando volvamos a visitar algun codigo ya existente. Puede tomar un poco de tiempo acostumbrarse a estas ideas, pero pronto se encontrara aplicandolas casi sin darse cuenta, y el resto de los desarrolladores e incluso usted mismo se lo agradeceran.

Comentarios