Últimamente he estado empapándome cosillas sobre conceptos de programación funcional y tratando de aplicarlos a mi modo de escribir código. Para ser sincero, tengo que reconocer que al principio rechazaba un poco la idea ya que siempre acababa pensando en soluciones basadas en orientación a objetos. Pero gracias a Javascript empiezo a verlo de otro modo y a entender el potencial que tiene, sobre todo cuando necesitamos transformar cosas o reutilizar código.

Ya mencioné algunas de sus características en el capítulo Funciones I de Javascript, como las funciones recursivas y las funciones de orden superior y primera clase. Hoy profundizaré un poco más.

Introducción

La programación funcional (FP de ahora en adelante) no es algo nuevo, existe desde los inicios de la computación y deriva del cálculo lambda.

Básicamente, y a modo resumido, diríamos que en FP un valor puede ser descrito como el resultado de una función. A su vez, una función puede estar compuesta de otras funciones.

const add = (x, y) => x + y  
const mul = (x, y) => x * y  
const addMul = (x, y, z) => mul(add(x, y), z))

console.log(add(3, 4))       // 7  
console.log(mul(2, 5))       // 10  
console.log(addMul(3, 4, 5)) // 35  

Sin embargo hay ciertos aspectos que debemos respetar con el propósito de garantizar el resultado correcto.

Funciones puras y mutaciones

La primera es que una función siempre debe ser pura, es decir, no debe tener efectos secundarios. Para ello debe cumplir con dos requisitos:

  • Dados los mismos argumentos siempre tiene que producir el mismo resultado
  • El resultado siempre debe ser un valor nuevo y no la mutación de los argumentos
let x = 2  
let y = 5

const addPure = (a, b) => a + b  
const addNotPure = (a, b) => a += b

let c = addPure(x, y)  
let d = addNotPure(x, y)

console.log(c) // 7  
console.log(d) // 7

console.log(x) // 7 !Wrong  
console.log(y) // 5

console.log(addPure(x, y)) // 12 !Wrong  

Como vemos, la función addNotPure modifica una de nuestras variables pasadas como argumento, haciendo que la siguiente llamada a la función addPure ya no nos dé el mismo resultado. Estamos provocando un efecto secundario alterando el resultado.

¿Que pasa cuando trabajamos con objetos? Exactamente lo mismo, y además las cosas se complican aún más ya que los objetos se asignan por referencia y no por valor, es decir, asignar un objeto a otra variable no copia su contenido sino que asigna una nueva referencia a ese objeto.

const peter = { name: 'Peter' }  
const john = peter      // "john" apunta al contenido de "peter"  
john.name = 'John'      // modifica la propiedad "name" de "peter"

console.log(peter.name) // 'John'  

Haciéndolo desde una función provocamos el mismo comportamiento.

const proto = { specie: 'dog', name: 'dog' }

const newDog = (dog, name) => {  
  dog.name = name
  return dog
}

const max = newDog(proto, 'Max')  
const tom = newDog(proto, 'Tom')

console.log(max.name) // 'Tom' !wrong  
console.log(tom.name) // 'Tom'  

En el caso de objetos siempre debemos producir un nuevo objeto y no alterar las propiedades de otro. Podemos ayudarnos de Object.assign para garantizar que esto no ocurra.

const proto = { specie: 'dog', name: 'dog' }  
const newDog = (dog, name) => Object.assign({}, dog, { name })

const max = newDog(proto, 'Max')  
const tom = newDog(proto, 'Tom')

console.log(proto.name) // 'dog' !Good  
console.log(max.name)   // 'Max'  
console.log(tom.name)   // 'Tom'  

En este caso garantizamos que el objeto no mute produciendo un nuevo objeto.

Composición

Como dije anteriormente, una función puede ser a su vez la composición de varias funciones. Pero en lugar de anidarlas, podríamos tener una función compose que haga el trabajo por nosotros.

function compose () {  
  const fns = Array.prototype.slice.call(arguments).reverse()
  return function (x) {
    return fns.reduce(function (value, fn) {
      return fn(value)
    }, x)
  }
}

const addTwo = (x) => x + 2  
const mulSix = (x) => x * 6  
const addOne = (x) => x + 1  
const myFunc = compose(addTwo, mulSix, addOne)  
console.log(myFunc(3)) // 26 = (2 + (6 * (3 + 1)))  

compose crea una nueva función que aplicará en orden inverso todas las funciones que le hemos dicho al argumento que le pasemos. ¿Y por qué en orden inverso? ¿No es más claro y sencillo al revés? Puede, pero luego explicaré por qué lo hacemos así.

El número de funciones que le podemos pasar es variable y pueden ser tantas como queramos. Incluso podríamos crear composiciones de otras funciones compuestas.

const myFunc = compose(addTwo, mulSix, addOne)  
const toText = (x) => `The result is: ${x}`  
const anotherFunc = compose(console.log, toText, myFunc)

anotherFunc(5)  
>> "The result is: 38"

Currying

Aunque el nombre nos suene a cierto condimento esencial de la comida tailandesa, la verdad es que no tiene nada que ver con ello. Para explicarlo de forma resumida, decimos que a una función de X número de argumentos le podemos aplicar currying cuando la podemos definir como X funciones anidadas que producen el mismo resultado. Lo veremos más claro con un ejemplo.

Podemos tener una función add que produzca la suma de 3 argumentos.

const add = (x, y, z) => x + y + z  
add(1, 2, 3) // 6  

Y a su vez podríamos tener funciones anidadas que nos den el mismo resultado.

const add = x => y => z => x + y + z  
add(1)(2)(3) // 6

// El ejemplo superior es una forma compacta de escribir lo siguiente
const add = (x) => {  
  return (y) => {
    return (z) => {
      return x + y + z
    }
  }
}

¿Qué ventaja tiene esto? Es el mismo resultado. Sí, pero en el segundo caso no tenemos por qué ejecutar todas las funciones de una sola vez. Veámoslo de otro modo.

const add = x => y => x + y  
const addTwo = add(2)  
const addSix = add(6)  
console.log(addTwo(3)) // 5  
console.log(addSix(3)) // 9  

Lo que hemos hecho ha sido almacenar la función y posponer el cálculo del resultado.

Si habéis seguido la serie de capítulos sobre Javascript puede que os recuerde a lo que hacía la función Function.bind, la cual nos permitía asignar el this dentro de una función y añadir parámetros por defecto.

Realmente lo que hacía bind es lo que se conoce como aplicación parcial, es decir, creamos una nueva función que almacena parte de los argumentos necesarios para la ejecución de la original. De hecho, es algo habitual confundir el currying con la aplicación parcial ya que muchas librerías usan una función curry para ambos casos pues podemos usar las funciones resultantes de ambas maneras o incluso mezclarlas.

const add = (x, y, z) => x + y + z  
const curried = curry(add)

curried(1)(2)(3) // 6  
curried(1, 2)(3) // 6  
curried(1)(2, 3) // 6  
curried(1, 2, 3) // 6

const addOneTwo = add(1, 2)  
addOneTwo(3)     // 6  

Pero volvamos al ejemplo de antes con la función original add.

const add = x => y => z => x + y + z   // (x + (y + (z))  

Si nos fijamos, los valores que le vamos pasando como argumentos van de izquierda a derecha, pero internamente la función se resuelve de derecha a izquierda.

¿Recordáis que antes dije que compose aplicaba las funciones en el orden inverso al que se las pasábamos? Por convención, en FP las funciones que definamos siempre deben resolverse de derecha a izquierda para que todas sean homogéneas, de modo que quien lea o use nuestro código no se encuentre con sorpresas.

¿Y si mezcláramos compose con curry?

const add = curry((x, y) => x + y)  
const mul = curry((x, y) => x * y)

const addTwo = add(2)  
const mulSix = mul(6)

const twoSix = compose(addTwo, mulSix)  
twoSix(3) // 20

[1, 2, 3, 5].map(twoSix) // [8, 14, 20, 32]

Con solo dos funciones se nos abren infinidad de caminos nuevos para combinar y producir nuevas funciones de utilidad que podemos luego reutilizar o combinar de nuevo.

Si os quedáis con ganas de más os animo a practicar y probar sus posibilidades. Casi todos los ejemplo que he puesto son simples operaciones con números para que se entiendan claramente, pero los conceptos son los mismo con objetos más complejos.

Aunque hay muchas librerías enfocadas a programación funcional yo casi siempre suelo recurrir a Ramda en el caso de Javascript. La documentación es sencilla y clara, os recomiendo echarle un ojo y probar otras funciones como map ó prop junto con arrays y objetos.

npm install ramda  
import { curry, compose } from 'ramda'  
// ..

"No existe lo desconocido, sólo lo temporalmente desconocido" - Star Trek (1979)