Programación funcional con Javascript. (Parte V)

 

Continuamos con nuestra colección de posts sobre Programación funcional con Javascript (parte I, parte II, parte III y parte IV). En esta quinta entrada volveremos a ver algo más de teoría antes de ver en un último post la práctica, en el que veremos cómo solucionar problemas de tareas asíncronas, o control de errores, que nos quedaba pendiente.

Pero antes de nada os invitamos a tener la mente abierta, ya que nos encontraremos con vocabulario nuevo al que, quizás, no nos hemos enfrentado antes. Pero tranquilos, iremos poco a poco avanzando sobre algunos de los conceptos básicos más teóricos, y luego veremos ejemplos prácticos del día a día.

Siéntate y disfruta de la lectura. No será hasta casi el final del post cuando veremos realmente aplicados algunos de los conceptos.

Containers

Éste no es en sí uno de esos conceptos con nombres extraños que veremos, pero si estará presente en todos ellos.

Definiremos un contenedor para almacenar un valor e interactuar con él; tendremos que vivir con que, en algunos casos, introduciremos el valor en el contenedor y no lo volveremos a extraer de ahí. Recordad que hay que tener la mente abierta.

De alguna manera, ésto ya pasa cuando realizamos alguna aplicación con programación orientada a objetos: introducíamos valores a través de setters en una clase y durante la ejecución de ese proceso no salían de ahí. Sí, teníamos getters a través de los cuales obtener esos valores, pero seguramente pasaban a otra clase tras algún cálculo. Dejemos ésto aparte y veamos cómo sería un contenedor.

const Container = function(x) {
  this.value = x
}

Bastante sencillo. Como se ve arriba, el contenedor recibirá un valor y lo almacenará. Ahora vamos a crear un método en nuestro contenedor que nos ayude a almacenar el valor.

Container.of = (x) => new Container(x)

Y, a continuación, vamos a probarlo y ver cómo lo almacena.

Container.of(10) // { "value": 10 }
Container.of('Hello World') // { "value": "Hello World" }
Container.of([ 1, 2, 3 ]) // { "value": [1, 2, 3] }
Container.of({ a: 1, b: 2, c: 3 }) // { "value": { "a": 1, "b": 2, "c": 3 }

En los contenedores podemos almacenar cualquier tipo, incluso y, aunque no está mencionado arriba, podríamos almacenar funciones (first class functions: las funciones son valores). Incluso ir un paso más allá y almacenar otro contenedor.

Container.of(Container.of('two deep level'))
// { "value": { "value": "two deep level" } }

 

Functors

Empezamos a ver algo de vocabulario nuevo en este caso, a no ser que los hayas conocido a través del algebra. Pero, y para simplificar las cosas, os diré que no solo ya hemos visto un Functor en esta serie de FP, sino que además si ya has trabajado antes con JS, llevarás usándolos todo ese tiempo. Los arrays son Functors. Lo que convierte a un Array en un Functor es que implementan map. Además para considerarse Functor, la implementación de map debe seguir unas leyes.

Veamos esas leyes y cómo implementamos map en nuestro contenedor para casos sencillos:

Para hablar de la primera ley necesitaremos la función identity, ¿recordáis esa función que veíamos en Ramda llamada identity? Devolvía el mismo valor que se le pasaba. Nos vendrá bien para esta explicación.

// identity = x => x
identity(10) // 10

Esta ley dice que aplicar la función identity a través de map sobre un contenedor equivale a hacer identity directamente sobre el contenedor. Veámoslo tal cual funciona con array antes de verlo con nuestro propio contenedor.

const mapArray = [ 1, 2, 3, 4, 5 ]
mapArray.map(identity) // [ 1, 2, 3, 4, 5 ]
identity(mapArray) // [ 1, 2, 3, 4, 5 ]

Ambas expresiones devuelven un resultado equivalente. Veamos ahora su implementación y el resultado con nuestro contenedor.

Container.prototype.map = function(fn) {
  return Container.of(fn(this.value))
} 

Map aplica la función que se le pasa sobre el valor actual y devuelve su resultado en un nuevo contenedor; al igual que hace map en array, que nos devuelve un nuevo array en cada uso. Bastante sencillo hasta aquí. Veamos si la ley de identidad se aplica en nuestro contenedor de la misma forma que en array.

const c = Container.of(10)
c.map(identity) // { "value": 10 }
// equivale a
identity(c) // { "value": 10 }

Parece que sí, que cumplimos esta ley.

La segunda ley dice que al ejecutar compose con dos argumentos, cada uno de ellos una función, dentro del map del contenedor equivale a ejecutar map sobre el contenedor con una de las funciones y a su resultado aplicarle de nuevo map con la otra función … mejor veámoslo con algo mucho más legible:


// myFunctor.map(compose(f, g))
// equivale a
// myFunctor.map(g).map(f)

Probémoslo como hicimos antes con Array.

const mapArray = [ 1, 2, 3 ]
mapArray.map(compose(concat('a', add(10)) // [ "a11", "a12", "a13" ]
mapArray.map(add(10)).map(concat('a')) // [ "a11", "a12", "a13" ]

Y ahora con nuestro contenedor.

const c = Container.of(1)
c.map(compose(concat('a'), add(10))) // { "value": "a11" }
c.map(add(10)).map(concat('a')) // { "value": "a11" }

Pues también se cumple. Nuestro contenedor es un Functor.

Además de Array, las promesas de ES6 (que comentábamos en un post anterior) también se consideran Functors, no implementan un método con el nombre map pero el método then se comporta como tal. No será la única vez que mismo concepto es nombrado de formas diferentes.

Monads

Antes de explicar nada sobre los Monads, expliquemos un problema con el que nos encontraremos al trabajar con los contenedores. Ya veíamos antes que un contenedor podría contener otro como valor.

Container.of(Container.of(1)) // { "value": { "value": 2 } }

Esto podría darse en los que tengamos una función que devuelva a su vez un contenedor y queramos usarla sobre nuestro contenedor. Estos casos se darán a medida que el código vaya siendo más complejo. No crearemos un contenedor de esa manera, pero ocurrirá cuando queramos usar map sobre el contendor.

 

Veamos el siguiente ejemplo.

const concatContainers = x => Container.of(concat('Hello ', x))
Container.of('World').map(concatContainers)

Tenemos una función que retornará un contenedor con la concatenación de ‘Hello’ con el valor que le pasemos por argumentos. Así que al contenedor que se crea en map, le establecemos como valor el contenedor que se devuelve en nuestra función concatContainers. Al final el resultado es:

{ "value": { "value": "Hello World" } }

Solo necesitaríamos un contenedor, pero ahora tiene 2 niveles, en la siguiente 3, etc. Es aquí donde necesitaremos los Monads.

Relacionado con los Monads veréis por todos lados la frase de James Iry: “A monad is just a monoid in the category of endoFunctors, what's the problem?”. Entiendo que sería en tono irónico pero, lejos de ayudar, asusta. Por ello y para simplificarlo, vamos a dejarlo en que un Monad es un contenedor que implementa chain, quizás más conocido por flatMap aunque también lo llaman bind (no confundir con el método bind de las funciones que hay en JS) en algunos lenguajes como Haskell.

Vamos a ver cómo sería la implementación de chain.

Container.prototype.chain = function(fn) {
  return fn(this.value)
}

Si lo comparamos con la implementación de map, veremos que la diferencia es que map hace uso del método of para devolver un contenedor nuevo y chain devuelve el valor tras aplicar la función que se pasa por parámetros.

Veamos a continuación las leyes que debe seguir un Monad:

// Monad.of(aValue).chain(Monad.of)
//equivale a
// Monad.of(aValue)

Un Monad al que hacemos chain con una función equivale a esa función aplicada directamente sobre el valor del Monad. Tiene sentido, pero veámoslo con nuestro Container.

Container.of('some value').chain(toUpper) // "SOME VALUE"
// equivale a
toUpper('some value') // "SOME VALUE"

Vemos que se cumple, así que probamos la ley de nuevo con nuestro contenedor.

Container.of('some value').chain(Container.of) // { "value": "some value" }
// equivale a
Container.of('some value') // { "value": "some value" }

Y lo probamos sobre nuestro contenedor.

También se cumple. Nuestro contenedor ahora también es un Monad.

Así que vamos a probar el ejemplo que veíamos antes.

const concatContainers = x => Container.of(concat('Hello ', x))
Container.of('World').chain(concatContainers)
// { "value": "Hello World" }

Ahí está, el contendor solo contiene un valor y no otro contenedor.

Antes de pasar a ejemplos prácticos en el siguiente post, veamos un último tipo.

Aplicatives

En comparación con los anteriores, su nombre no solo es más sencillo sino que además nos da algo más de información.

Ahora digamos que queremos ejecutar una función sobre dos contenedores.

concat(Container.of('Hello '), Container.of('World'))
// { "value": "Hello " } does not have a method named "concat"
add(Container.of(2), Container.of(3))
// NaN

Los contenedores no son del tipo String ni Number. Usando una mezcla de chain con una función que a su vez tenga un map funcionaría.

Container.of('Hello ').chain(hello => Container.of('world').map(concat(hello)))
// { "value": "Hello World" }
Container.of(2).chain(two => Container.of(3).map(add(two)))
// { "value": 5 }

La contra de realizar la operación de esta manera, es que el contenedor interno no se evaluará hasta que lo haga el contendor externo.

Para ello los Aplicatives implementan el método ap. Veamos cómo implementarlo:

Container.prototype.ap = function(other_container) {
  return other_container.map(this.value)
}

Como vemos ap espera como argumento otro contenedor sobre el que hará map con, ojo aquí, la función que le hayamos pasado al contenedor para que establezca como su valor. Repito, el contenedor dónde llamemos al método ap, deberá contener una función y no un valor.

Vamos a probar el ejemplo anterior con nuestro nuevo método sin pararnos más con los Applicatives.

Container.of(concat).ap(Container.of('Hello ')).ap(Container.of('World'))
// { "value": "Hello World" }
Container.of(add).ap(Container.of(2)).ap(Container.of(3))
// { "value": 5 }

 

Category theory

Llegado este punto, comentar que en todo lo que hemos visto anteriormente hoy no hemos hecho referencia a la Teoría de la Categoría de la que vienen estos conceptos, por lo que  esta ha sido una introducción NO matemática a alguna de sus estructuras algebraicas y de la que, según vayáis investigando, iréis viendo referencias. Además nos hemos saltado otras condiciones,  así como otras estructuras que se destacan en el entorno de FP. La especificación para JavaScript, que en parte he intentado seguir, la podéis encontrar en el link de abajo que hace referencia a Fantasy-Land.

* Teoría de las categorías: https://es.wikipedia.org/wiki/Teor%C3%ADa_de_categor%C3%ADas

* Fantasy-Land spec: https://github.com/fantasyland/fantasy-land

 

Resumen

Estos son los puntos sobre los que, para simplificar en lo visto en esta entrada, deberíamos quedarnos:

  • Los Functors aplican, a través de map, una función a un valor que está en un contenedor.
  • Los Monads aplican,  a través de chain, una función que devuelve un valor que está otro contenedor al valor de otro contenedor.
  • Los Applicatives aplican, a través de ap, funciones que se encuentran dentro de contenedores sobre valores que a su vez están contenidos en otros contenedores.

En la siguiente, y última entrada, veremos aplicaciones a estas estructuras y cómo simplifican ciertos problemas que vemos en todas las aplicaciones.