Funciones Puras en Javascript
Ricardo Vega / 24 septiembre 2024
⏰ 7 minutos
Ricardo Vega / 24 septiembre 2024
⏰ 7 minutos
Una de las prácticas de programación que más seguridad me dan en mi día a día, es el uso de las llamadas funciones puras, mediante las cuales tengo la certeza de no estar modificando nada fuera del contexto de la función.
En el post de hoy, quiero acercarte este concepto partiendo prácticamente desde cero para que tu también te puedas aprovechar de sus ventajas si no lo haces ya y para ello, emplearé mi lenguaje de programación principal: Javascript.
Pero empecemos por el principio:
Una función es un proceso que recibe unos valores (argumentos) y devuelve un valor en función de la lógica de la función. Podemos usarlas para "mapear" o convertir unos valores en otros con el formato deseado o como fragmentos de lógica independiente que ejecutan determinados procedimientos.
const double = (x) => x * 2;
console.log(double(3)); // 6
En este primer ejemplo se usar la sintaxis de "arrow function" pero double lo
podríamos escribir así:
o así:
const double = (x) => {
return x * 2;
};
const double = function (x) {
return x * 2;
};
o así:
function double(x) {
return x * 2;
}
Otro ejemplo de función sería:
Math.min(2, 8, 5, 1); // 1
Como puedes ver, podemos crear nuestras propias funciones o utilizar las ya provistas por el lenguaje, en este caso JavaScript.
En este punto es importante hacer hincapié que, en Javascript, una función siempre devolverá un valor, aunque no sea de forma explícita. Es decir, que hasta funciones de este tipo:
const printDate = () => {
const now = new Date();
console.log(now);
};
o esta:
let state = { years: 23 };
const happyBithday = () => {
state += 1; // state = 24
};
donde no hay ningún return explícito devolverían algo, en este caso
undefined. Las funciones printDate y happyBirthday pueden ser buenos
ejemplos donde se usan esta clase de funciones que aparentemente no devuelven
nada:
Ahora que ya sabemos qué define una función, veamos que características deben cumplirse para considerarlas "puras":
Estas dos características, hacen que las funciones puras sean predecibles (debido a la primera propiedad) y seguras (debido a la segunda); también fácilmente testeables (debido a ambas propiedades).
En la práctica, estas características hacen que casi siempre requieran unos valores de entrada que se usarán para calcular la salida, sin modificar la entrada (es decir, no muta los valores de entrada).
Ni printDate ni happyBirthday son funciones puras. Ya que si bien la primera
no produce efectos secundarios (no cambia nada fuera del ámbito de la
aplicación), no siempre devuelve el mismo valor (depende del tiempo). La segunda
es más clara puesto que sí tiene efectos secundarios al modificar una variable
externa state y además, cada vez que se llame, devolverá un valor diferente
(precisamente por esta dependencia del estado global).
Sin embargo, podíamos haberla escrito como función pura:
const happyBithday = (currentYears) => {
return currentYears + 1;
};
Tanto double como Math.min son funciones puras.
Las propiedades de las funciones puras hacen que sea simple reutilizarlas ya que son independientes del lugar donde se ejecutan y son uno de los principios de la programación funcional.
De las dos propiedades de las funciones puras se pueden sacar dos aprendizajes prácticos adicionales:
Las funciones que usan estado global, inmediatamente dejan de ser puras ya que dependen de valores que no son pasados como argumentos. Además, realizar cambios en el estado global dentro de una función produce efectos secundarios ya que lleva a cabo modificaciones fuera de su propio contexto.
Los efectos secundarios se consideran peligrosos ya que es fácil que cambien comportamientos de nuestra aplicación de formas que pasen desapercibidas para nosotros como programadores, lo cual lleva a minimizar su uso y, en caso de ser necesarios, vigilar mucho cómo y dónde se producen y qué posibles repercusiones tienen.
Mi recomendación al respecto es clara:
Si tu aplicación necesita estado global, pasa los valores necesarios en la
función como argumentos (lo que hemos hecho con happyBithday) para convertir
ese estado global en local de la función. Posteriormente, emplea el valor
devuelto (que no ha cambiado el estado global) en una utilidad concreta de
cambio de estado global (método, función o lo que aplique según tu caso) que te
permita tener controlado ese cambio.
Si conoces React, seguramente estas "buenas prácticas" te resulten familiares, sobre todo si has usado librerías como Redux.
Decimos que se mantiene la inmutabilidad cuando no se cambia (muta) una variable. Las funciones puras deben mantener los parámetros de entrada inmutables para evitar efectos secundarios (desde la función pura, no sabemos si los parámetros que se nos pasan como entrada la función se usan en otro lugar de la aplicación por lo que no sabemos si produciríamos cambios en otro sitio si los cambiamos directamente).
Esto hace que los parámetros de entrada nunca se modifiquen de forma directa, sino que se acuda a copias modificadas de la entrada. Mantener esta táctica en toda la aplicación es una buena práctica para tener una excelente trazabilidad y evitar efectos secundarios que se vuelven especialmente peligrosos si estamos trabajando con procesos asíncronos en los que no podemos asegurar el orden en el que se ejecutan las cosas.
Aunque muy básico (e irreal) este sería un ejemplo de lo que queremos evitar:
const myAge = 20;
const calculateNewYears = async (currentYears) => {
// very long async process
return currentYears + 1;
};
const calulateNumberOfCandlesToBuy = async (currentYears) => {
// very long async process
return currentYears + 1;
};
calculateNewYears(myAge).then((newAge) => {
console.log(`Happy ${newAge} birthday!!`);
});
calulateNumberOfCandlesToBuy(myAge).then((newAge) => {
buyCandles(newAge);
});
Este proceso siempre va a generar un mensaje "Happy 21 birthday" y una compra de
21 velas independientemente de si el proceso calculateNewYears es más rápido o
lento que calulateNumberOfCandlesToBuy ya que las variables de entrada de
nuestro proceso son inmutables.
Ahora bien, si lo hubiésemos escrito así:
let myAge = 20;
const calculateNewYears = async () => {
// very long async process
myAge = myAge + 1;
};
const calulateNumberOfCandlesToBuy = async () => {
// very long async process
myAge = myAge + 1;
};
calculateNewYears(myAge).then(() => {
console.log(`Happy ${myAge} birthday!!`);
});
calulateNumberOfCandlesToBuy(myAge).then(() => {
buyCandles(myAge);
});
Muy posiblemente habríamos o bien comprado 22 velas o bien generado el mensaje "Happy 22 birthday" dependiendo de qué proceso es más rápido de los dos.
Aunque todos los ejemplos de este post están escritos en JavaScript, los conceptos son aplicables de forma directa a otros lenguajes de programación. La programación funcional ha ganado mucho espacio en la última década y es cada vez más importante. Dentro de ese paradigma, el uso de conceptos como inmutabilidad y funciones puras es cada vez más habitual en el día a día de mucha gente.
Personalmente, intento aplicar estos conceptos siempre que puedo, este o no en un paradigma funcional, puesto que creo que nos acercan muchas ventajas tanto en mi día a día, como para mi futuro yo cuando tenga que corregir un bug o simplemente leer / reutilizar un fragmento de código.
Creo que una vez te acostumbras, su aplicación no es complicada y prácticamente implantas estos principios por defecto. Por supuesto, pueden existir casos para los que traiga más problemas que ventajas (aunque yo la verdad que no me he encontrado con ellos) o simplemente personas que consideren que no es necesario este cambio de mentalidad para su dominio / negocio / aplicación.
Tanto si este es tu caso como si no, me encantaría poder discutirlo contigo por lo que no dudes en iniciar una conversación mencionándome en Redes Sociales.
Un saludo y hasta la próxima,
Ricardo