Aproximadamente un mes desde el último artículo, volvemos a la carga un año más con más contenido para el blog de el rincón del front.
Espero que hayáis pasado unas buenas vacaciones y descansado, porque vamos a empezar didácticos hablando de un tema que mencionamos de pasada el año pasado pero que es esencial para entender el cómo se maneja el código impuro al estilo funcional.
De los efectos no nos libramos
Cuando en programación funcional se dice que no tiene side-effects, no es en un sentido literal. Lo que viene a decir es que en este paradigma los efectos se separan del código funcional, por lo que en la parte donde tenemos nuestra reglas de negocio (lo que verdaderamente importa en el software) no hay efectos.
Esto suele llevar a error al hacer que pensemos en que si no podemos tener side-effects entonces no podemos hacer nada interesante, porque obviamente todo lo “divertido” es considerado un efecto. Me estoy refiriendo claro está, a llamadas a servicios, acceder a APIs del navegador o incluso generar un número aleatorio.
Por eso es importantísimo que os quedéis con el matiz anterior y que repito aquí por si había alguno todavía añorando el olor del turrón:
En la programación funcional aislamos los efectos del código funcional; como al final es el código que importa, pues no es erróneo decir que el paradigma funcional no tiene side-effects. Y además, de cara al marketing, suena mejor :)
Recordando la gestión de efectos
Como las navidades son traicioneras y el alcohol hace más que un estrago en nuestra memoria, voy a refrescaros un poco sobre como gestionábamos los efectos con un enfoque funcional. Así, les damos un poco de contexto a los que hayan aterrizado de nuevas en esta newsletter.
Cuando hablábamos de los thunks en el artículo dedicado al patrón flux, dijimos que toda la magia de la gestión de efectos giraba en torno a la idea de que un efecto no es realmente un efecto hasta que se ejecuta. Así, podemos representarlos para poder separarlos de ese código funcional que mencionábamos antes y poder trabajar con ellos; pero no ejecutarlos hasta que de verdad lo necesitemos.
De hecho, algunos lenguajes como Elm nos abstraen completamente de la creación y gestión de efectos. Obligandonos además a controlar cada caso, por lo que además nos libran de las temidas excepciones en tiempo de ejecución.
Veamos por ejemplo la siguiente función:
function greet (name) {
document.querySelector('#greeting').innerText =
`Hello, ${name.toUpperCase()}`;
}
Este código que haría llorar al mismísimo Robert C. Martin, es lo menos funcional del universo. Pero la realidad, es que es el día a día probablemente de muchos de los que estáis leyendo aquí.
Esa función es impura porque genera efectos y además los mezcla con el código funcional, por lo que, si mañana queremos rehusar parte de ese código, pues no podremos hacerlo sin efectuar un refactor.
Vamos a separar pues, los efectos del código importante (el que es potencialmente reutilizable):
function concat(text) {
return function (anotherText) {
return text.concat(anotherText);
}
}
function toUpperCase(text) {
return text.toUpperCase();
}
const greetName = compose(concat('Hello, '), toUpperCase);
function setName (name) {
document.querySelector('#greeting').innerText = name;
}
Más código sí, pero reutilizable, probable (que se puede probar) y mantenible. Si os fijáis, antes el código funcional de greet (la concatenación y el paso a mayúsculas) no se podía probar de forma unitaria sin mocks.
Si no sabes lo que es compose, te recomiendo que te leas este artículo primero. Yo te espero aquí.
Vale, hemos separado el código importante de los efectos. Hasta aquí alguno se podrá estar preguntando: pero, ¿por qué decimos que el código que hay en setName, son efectos?
Pues muy sencillo, primero estamos accediendo a una variable global, document, que puede o no estar definida (si ejecutamos este código en Node.js, ya os digo yo que no lo estará). Además, cualquiera puede acceder a esa variable y cambiar su interior (poco recomendable, pero posible) y finalmente estamos ejecutando una función que puede producir nulos (si el nodo no existe).
Todas estas cosas que no podemos prever y que dependen de sistemas que no controlamos, es lo que denominamos side-effects.
Bien, aclarado esto, vemos que, si bien ahora hemos separado los efectos, no están aislados ya que aún podemos ejecutarlos en cuanto llamemos a las funciones:
const greetAndSet = compose(setName, greetName);
Al realizar la composición para tener la misma funcionalidad que había inicialmente, vuelve a estar unido lo puro con lo impuro, por lo que greetAndSet sería una función impura.
Cuando aislamos side-effects no lo hacemos para una función o para un momento determinado, tenemos que hacerlo con todo el código a la vez.
Representando efectos
Ya hemos mencionado que en aras de poder aislar los efectos tenemos que tener una forma de trabajar con ellos, pero sin llegar a ejecutarlos.
Cuando veíamos los thunks en la arquitectura flux, deciamos que estas funciones realmente eran una versión lazy de las funciones con efectos; o, dicho de otra manera, si tenemos la función setName que sabemos que en cuanto la ejecutemos se van a liberar sus efectos, podemos encapsularla en otra para así poder ejecutar la función mas externa, sin lanzar los efectos:
function LazySetName () {
return function setName (name) {
document.querySelector('#greeting').innerText = name;
}
}
Con esta operación tan en apariencia sencilla, hemos convertido un efecto en “puro”. Ahora podremos trabajar con LazySetName e incluso ejecutarlo sin que los efectos se liberen al exterior.
Para hacerlo, necesitamos, sin embargo, modificar un poco las funciones funcionales para que trabajen con funciones (en lugar de con primitivas) y mantengan siempre a estas encapsuladas en una función. Se que parecen los tres tristes tigres, pero vamos a verlo con un ejemplo:
function GreetAndSet (f) {
return function (name) {
return f(greetName(name));
}
}
GreetAndSet(LazySetName());
La diferencia de la función GreetAndSet en contraposición a la función greetAndSet de más arriba, es que la primera es pura porque no genera ningún efecto mientras que la segunda los disparará todos en el momento en el que lo ejecutemos:
GreetAndSet(LazySetName()); // Function
greetAndSet() // ejecuta todos los efectos que hay en setName
De la primera forma solo hemos dejado indicado los efectos y las operaciones, en el segundo estamos literalmente ejecutándolo todo, código funcional y código impuro.
El functor Effect
Aunque el ejemplo anterior nos ha servido para probar el concepto del que hemos estado hablando, no es demasiado útil. Primero porque nos tenemos que preocupar siempre de no romper la encapsulación del efecto y segundo porque tenemos que crear versiones de nuestras funciones puras que trabajen en la categoría de funciones en lugar de primitivas normales, y eso rompe un poco esta idea de reutilización de la que hablabamos al principio.
Para poder maximizar la reutilización necesitamos crear una categoría para los efectos y así poder enseñar a nuestras funciones a trabajar en esa nueva categoría, sin necesidad de implementarlas de nuevo.
Ya que necesitamos abstraer el cómo se aplican las funciones con efectos (para poder encapsularlas) y el cómo aplicamos una función transformadora en la categoría de efectos, vamos pues a crear un nuevo tipo algebraico llamado Effect, que implemente las leyes de los functors:
function Effect(fn) {
return {
}
};
Cuya anotación sería la siguiente:
Function -> Effect
Ahora, vamos a agregar a nuestro nuevo tipo un mecanismo para poder ejecutar los efectos que tenga en su interior:
function Effect(fn) {
return {
run(x) {
return fn(x);
}
}
}
Con el método run, simplemente cogemos la función que lanza efectos y la ejecutamos. La idea es no tener que llamar a este método hasta el final, como ya veremos.
De esta forma, ahora podemos meter nuestra función setName de arriba, dentro del efecto:
function setName(name) {
document.querySelector("#app").innerText = name;
}
const e = Effect(setName);
e.run('Irene'); // se ejecutan los efectos
Así, hemos trasladado toda la impureza de nuestro código al método run.
Ahora tenemos una estructura que guarda en su interior una función, pero de momento es poco útil y tampoco es un Functor, porque para serlo se tienen que cumplir una serie de leyes. Para ello, vamos a añadir un método map que nos permita aplicar funciones de la categoría de primitivas a la categoría de Effect.
function Effect(fn) {
return {
run(x) {
return fn(x);
},
map(f) {
}
};
}
Este método map será el encargado de aplicar transformaciones de funciones que no estén en la categoría de Effect y poder así transformarlo. Además, al habernos abstraído de la aplicación de funciones sobre el Effect, podemos dejar abstraído todo ese comportamiento dentro del map, de tal forma que para el usuario final sea igual el hacer un map a un array por ejemplo, que a un efecto.
Bien, para poder implementar el método map, necesitamos que se cumplan unas leyes que además convertirán al tipo algebraico Effect en un Functor:
La función map debe de respetar la identidad:
functor.map(x => x) === functor
La función map debe de soportar composición de funciones:
functor.map(f).map(g) === functor.map(x => f(g(x)))
.
La primera ley hace alusión al hecho de que la función map de un Functor siempre ha de devolver el mismo tipo que el Functor. Está es fácil: si tenemos un array y hacemos map, ¿qué obtenemos? un array. Aquí igual: si tenemos un effect y hacemos map, obtenemos un effect.
La segunda ley es muy importante, porque un map no es solo un puente entre categorías, sino que además es una composición de funciones de una categoría a la otra, por lo que la composición de dos funciones en la categoría de las primitivas debe de ser igual que aplicar esas funciones por separado sobre el effect.
Los tipos algebraicos tienen leyes porque les hacen comportarse de forma consistente y predecible. Si cada vez que hiciésemos map sobre un array, nos devolviese un tipo distinto, sería muy difícil trabajar con esos tipos.
Puedes encontrar más información sobre Functors aquí o aquí.
Vamos por tanto a implementar el map de nuestro functor Effect siguiendo las leyes que acabamos de aprender:
function Effect(fn) {
return {
run(x) {
return fn(x);
},
map(f) {
return Effect(x => f(fn(x)));
}
};
}
Recuerda que el efecto recibe una función que tiene un efecto dentro y que el objetivo de map es aplicar una función sobre esa función.
Dejo un gráfico a continuación que espero que ejemplifique lo hemos logrado:
Transformando efectos
Ahora que tenemos una función map, vamos a poder transformar efectos sin ejecutarlos, lo cual es poderosísimo.
Veamos por ejemplo las siguientes funciones:
function log(name) {
console.log(name);
}
function getName() {
return document.querySelector('#name').innerText;
}
function main () {
const name = getName();
const nameWithHello = `Hello ${name}`;
log(nameWithHello.toUpperCase());
}
De nuevo, todo el código funcional está mezclado con el código impuro. Podemos como antes sacar la parte funcional a funciones para poder reusarlas:
Pero main sigue mezclando parte funcional con parte impura y eso no es lo que queremos, por lo que vamos a empezar a meter las funciones impuras dentro de efectos, empezando por getName:
const getNameEffect = Effect(getName); // Effect HtmlElement
Y ahora podemos aplicar transformaciones puras sin ejecutar realmente los efectos:
getNameEffect.map(concatHello).map(toUpperCase) // Effect String
Y mejor aún, ya que tenemos una forma estándar de tratar los efectos, podemos definir las partes impuras como funciones que devuelven efectos directamente:
function log(name) {
return Effect(() => console.log(name));
}
function getName() {
return Effect(() => document.querySelector('#name').innerText);
}
Además, podemos crear una factoría que nos permita de forma más sencilla el colocar valores (no solo funciones) dentro de los efectos:
Effect.of = value => Effect(() => value);
function log(name) {
return Effect.of(console.log(name));
}
function getName() {
return Effect.of(document.querySelector('#name').innerText);
}
Efectos dentro de efectos
Hemos visto que con map podemos aplicar funciones de la categoría de tipos primitivos a functors de la categoría de Effect. Pero, ¿qué ocurre si queremos aplicar una función que trabaja en la categoría de Effect (es decir, devuelve un efecto) sobre una función que también pertenece a esa categoría?
Es decir, queremos poder hacer esto:
function main () {
const getNameEffect = Effect(getName); // Effect HtmlElement
const logEffect = Effect(log);
return getNameEffect
.map(concatHello)
.map(toUpperCase)
.map(logEffect); <--- estamos mapeando una función que devuelve un efecto
}
El problema con este código es que en el último map hemos aplicado una función que ya devuelve un efecto, por lo que hemos acabado con un Effect(Effect(String))
y eso se vuelve muy tedioso de gestionar.
Para solucionar este problema, podemos hacer que el Functor Effect se comporte también como una mónada mediante la implementación de la operación bind (de haskell) o chain.
Al igual que ocurría con los functors, las mónadas tienen que cumplir unas leyes:
Identidad por la izquierda: Colocar un valor en la mónada y después encadenarle una función tiene que producir el mismo resultado que pasar que aplicar la función al valor:
Monad.of(value).chain(f) === f(value)
Identidad por la derecha: Aplicar la función identidad sobre una mónada a través del método chain, debe de ser lo mismo que la mónada original:
m.chain(x => x) === m
Composición: Dadas dos funciones f y g, aplicarlas por separado con chain debe de ser igual a aplicar una vez la composición de f y g:
m.chain(f).chain(g) === m.chain(x => f(x).bind(g))
Lo de las leyes parece un poco restrictivo, pero la verdad es que cuando todos tus tipos algebraicos se comportan de la misma forma, es mucho más sencillo construir código genérico que funcione con cualquier cosa.
Ahora lo que tenemos que hacer es implementar un método chain que cumpla esas leyes. Entonces, necesitamos romper un nivel de anidación una vez que hayamos hecho el map y para ello solo hay que ejecutar la función del efecto más externo, que en lugar de ejecutar los efectos, devuelve otro efecto:
Entonces en este caso, hacer “run” sobre el efecto más hacía afuera no ejecuta los side-effects, sino que simplemente rompe un nivel de anidación. Esto lo dejaremos abstraído en el método chain, de tal forma que para los clientes de nuestra mónada Effect sea transparente:
function Effect(fn) {
return {
//...map,run
chain(f) {
return this.map(f).run(); // run aquí rompe un nivel de nesting
}
};
}
Así, chain es igual que map solo que como espera recibir una función transformadora que también devuelve un efecto, rompemos esa anidación extra que añade el map mediante la llamada al método run.
Algunas implementaciones suelen crear un método idéntico llamado join que hace lo mismo que run pero que no tiene la connotación negativa de pensar que está ejecutando los side-effects. Es una cuestión de gustos y claridad.
Convirtiendo a la función main en pura
Ahora ya tenemos todos los ingredientes para poder tener la misma lógica que el ejemplo inicial, pero haciendo que main sea puro en lugar de impura:
const getNameEffect = Effect(getName); // Monada
const logEffect = Effect(log); // Monada
function main () { // función pura que no genera side-effects
return getNameEffect
.map(concatHello)
.map(toUpperCase)
.chain(logEffect);
}
La clave radica en que hemos realizado todas las operaciones, pero sin ejecutar nada, ya que la función main solo sabe de efectos (que son monadas) y de que para transformar efectos se pueden aplicar otras funciones puras a través del método map.
Y map es una función pura porque si la ejecutamos no genera side-effects:
main();
Simplemente devuelve una mónada Effect, que en el caso que queramos ejecutar y entonces sí, lanzar todos los efectos, lo podremos hacer a continuación:
main().run(); // ejecuta toda las transformaciones y efectos
Y así, a menos que llamemos al run de nuestro main, todo el programa es puro porque, aunque representa los side-effects, no los ejecuta nunca.
Y aquí es donde las afirmaciones de “la programación funcional no tiene side-effects” cobran sentido, puesto que hemos desarrollado todo el programa de forma funcional aislando en mónadas todo lo impuro e impidiendo que se ejecuten entre el código funcional.
Al final los lenguajes funcionales solo tienen un único punto de entrada (main) donde al ejecutarlo se obtienen las mónadas con los side-effects y (habitualmente) un proceso interno se encarga de ejecutarlos.
Conclusión
Si has llegado hasta aquí sin una embolia, enhorabuena. Yo no lo entendí la primera vez que lo leí. Ni la primera ni la décima, pero cuando se practica lo suficiente se interiorizan todos estos conceptos.
También de decir, que habitualmente un programador funcional solo entiende las leyes que rigen los tipos algebraicos, pero no los implementa. Nosotros hemos implementado una mónada Effect para ejemplificarlo todo, pero en la práctica los lenguajes proveen estás APIs y en caso de no hacerlo, con implementarlo una vez o usar alguna librería es suficiente.
Hoy hemos hablado de mónadas que son a su vez Functors y pretenden solucionar los problemas de anidamiento de estos últimos. Dependiendo de los tipos algebraicos que se estén implementando, las funciones map y chain variaran, pero las leyes que se les aplican siempre se han de respetar. Es por eso que trabajar con tipos algebraicos vuelve al código muy determinista, aunque a priori parezca un poco ofuscado.
En futuras entregas veremos otras mónadas, pero al final la idea es siempre la misma; contexto de ejecución que nos permiten dar control sobre el cómo y el cuándo se aplican las funciones. Así, por ejemplo, un Maybe (como vimos en un artículo pasado) se aprovecha del control de aplicación para decidir si aplicar o no una transformación en base a si el valor que está dentro de la mónada es un nulo o no. En este caso hemos vuelto a una función perezosa, impidiendo que pueda ser ejecutada directamente.
Nada más por hoy :)