Continuemos con nuestra serie de programación funcional, y en este caso seguiremos hablando de sus caracteristicas con Javascript.
En el post anterior hablamos de Inmutabilidad, una característica donde no se mutaban o modificaban los objetos si no que creabamos otros con los cambios aplicados.
Esto es básico por ejemplo para el caso de lo siguiente que queremos hablar que es Funciones puras
Funciones puras
Las funciones puras son aquellas donde los cambios que queramos hacer solo se producen dentro del scope de la función (en su interior básicamente), en ningún caso modifican variables externas. Si tenemos una función que no devuelve nada, que usa this o que no tiene argumentos podemos empezar a sospechar que no es pura. OJO aunque digamos que debemos cumplir este punto en la programación funcional muchas veces no es posible por ejemplo en funciones de IO donde por ejemplo trabajamos con bases de datos, pero en general debemos poder cumplir con este principio.
Veamos un ejemplo del problema y como lo podemos solucionar, seguiremos la teoría del ejemplo anterior
const oldNinjas = ['Ninja1', 'Ninja2']
const newNinja = 'Ninja3'
const addNinja = (actualNinjas, newNinja) => {
const ninjas = actualNinjas
ninjas.push(newNinja)
return ninjas
}
const totalNinjas = addNinja(oldNinjas, newNinja)
console.log(totalNinjas) // result <--["Ninja1","Ninja2","Ninja3"]
console.log(oldNinjas) // result <--["Ninja1","Ninja2","Ninja3"]
Es un ejemplo muy forzado lo sé, pero creo que se puede captar el concepto, aunque le estemos enviando por parámetro los ninjas actuales al ser realmente una referencia modificamos el valor de oldNinjas queramos o no, en este caso vemos como nuestra función no es pura, tiene efectos colaterales que no deseamos. En este caso una posible solución podía ser esta:
const oldNinjas = ['Ninja1', 'Ninja2']
const newNinja = 'Ninja3'
const addNinja = (actualNinjas, newNinja) => {
return actualNinjas.concat(newNinja)
}
const totalNinjas = addNinja(oldNinjas, newNinja)
console.log(totalNinjas) // result <--["Ninja1","Ninja2","Ninja3"]
console.log(oldNinjas) // result <--["Ninja1","Ninja2"]
Concat es un método de los arrays que realmente no modifica el objeto original, si no que devuelve uno nuevo, con esto cumpliriamos siempre con la funcionalidad en cuestión y no modificamos nada que realmente no queremos. Esta solo es una forma de cumplir con lo que estamos buscando sé que tenemos otras muchas opciones esto es solo a nivel de ejemplo.
Vamos a por la siguiente
Funciones de orden superior
Esto viene también de la mano de las funciones como ciudadanos de primera clase, es decir, las funciones se pueden tratar como cualquier otra variable u objeto del lenguaje y hacen uso de otras funciones.
Estas características estamos usandolas habitualmente en javascript aunque puede que no seamos conscientes del todo, por ejemplo cuando hacemos usos de callbacks o trabajamos con promesas ya estamos haciendo uso de estas características. Veamos un ejemplo sencillo que hace uso de las 2 características
function calculator (number, operation) {
return operation (number)
}
const plus2 = (number) => {
return number + 2
}
const squared = (number) => {
return number**2
}
const twoPlusTwo = calculator(2, plus2)
const threeSquared = calculator(3, squared)
console.log(twoPlusTwo) // 4
console.log(threeSquared) // 9
Como vemos estamos pasando por parámetro las operaciones que queremos realizar y luego ya calculator hace uso de el directamente, creo que más o menos se entiende.
Veamos el siguiente punto.
Currificación y aplicación parcial
A menudo estos conceptos se separan porque puramente hablando no son lo mismo, pero hacen uso de la misma funcionalidad realmente lo que cambia es la manera de aplicarla.
Para hablar de esto (y aunque no quería meterme en tecnicismos) tengo que introducir el concepto de aridad. La aridad de una función básicamente es la cantidad de parámetros que tiene un método:
function calculator (number, operation) // <-- aridad 2
Bien viendo lo que es la aridad ahora veamos una funcionalidad muy util que tenemos con javascript, que viene de la mano de la de las funciones puras. Como hemos comentado ya, es posible que devolvamos funciones y las recibamos por parámetro ¿no?... pues en esto es en lo que se basa tanto la aplicación parcial como la currificación, veamos un ejemplo primero:
function calculator (number) {
return function (operation) {
return operation(number)
}
}
const plus2 = (number) => {
return number + 2
}
const squared = (number) => {
return number**2
}
const baseNumber = calculator(2)
console.log(baseNumber) // function (operation) { return operation(number); }
const twoPlusTwo = baseNumber(plus2)
const twoSquared = baseNumber(squared)
console.log(twoPlusTwo) // 4
console.log(twoSquared) // 4
Lo importante ahora mismo es la función calculator, como vemos lo primero que hace la función es devolver otra función, si nos fijamos en el console.log de baseNumber vemos como realmente es una función que espera una operación y simplemente lo que tenemos es ya un número preparado para realizar la operación.
Visto esto veamos lo que es aplicación parcial con un ejemplo
const createPlayer = (name, health) => (weapon) => {
return {
name,
health,
weapon
}
}
const playerBase = createPlayer('DevNinja', 1000)
const availableWeapons = ['SuperGun', 'Katana', 'Smoke Grenade']
const playerWeapon = availableWeapons[Math.floor(Math.random() * availableWeapons.length)]
const player1 = playerBase(playerWeapon)
console.log(player1) // { name:"DevNinja", health:1000, weapon:"Katana" }
Vuelvo a comentar que esto es un poco forzado, pero nos vale para el concepto. Si nos fijamos ahora usamos arrow functions que son más comodas para hacer estas cosas.
Pues bien en este caso createPlayer lo primero recibe el nombre y el valor de salud y se queda "a la espera" de que el sistema le aplique un arma al jugador, esto es lo que seria la aplicación parcial, podemos definirlo como una ejecución de una función por pasos, pero como vemos en este caso tenemos una aridad inicial de 2 y luego de 1, esta situación de aridad es la diferencia principal entre aplicación parcial y currificación. La currificación tendría siempre aridad 1, es decir:
const createPlayer = (name) => (health) => (weapon) => {
return {
name,
health,
weapon
}
}
const playerBase = createPlayer('DevNinja')
const playerHealh = Math.floor(Math.random() * 1000)
const playerWithInitialHealh = playerBase(playerHealh)
const availableWeapons = ['SuperGun', 'Katana', 'Smoke Grenade']
const playerWeapon = availableWeapons[Math.floor(Math.random() * availableWeapons.length)]
const player1 = playerWithInitialHealh(playerWeapon)
console.log(player1) // { name:"DevNinja", health:428, weapon:"Smoke Grenade" }
Ahora tenemos que siempre trabajamos con una aridad de 1, lo que modulariza bastante el uso de nuestro método createPlayer, haciendo posible que hagamos cualquier operación entre cada paso de la función.
Bueno creo que más o menos se entienden estos conceptos, voy un poco rápido con esto porque existen multitud de ejemplos y explicaciones al respecto por internet, si algo no esta claro recomiendo investigar un poco más, en esta serie nos detendremos un poco en conceptos más complejos como los functores y las monadas que son algo más duros para entenderlos.
Nos vemos en el siguiente donde hablaremos de la Composición de funciones un abrazoooooo Ninjaaas
