Hoy os traigo un artículo donde intento centrar un poco el concepto de programación funcional, ya que debido al uso que se le da actualmente se ha pervertido un poco la nomenclatura y me encuentro mucho la respuesta de “usamos lodash y underscore” a la pregunta de “¿hacéis programación funcional?”. Y lamentablemente, lodash y librerías similares, están lejos de ese paradigma por diversos motivos que exploraremos hoy.
Empecemos con un poco de historia.
Breve introducción a la programación funcional
Actualmente existen dos modelos de computación que fueron presentados en la década de los 30. Por un lado, tenemos el cálculo lambda, definido por Alonzo Church en 1932 y por el otro la Maquina de Turing, presentada por Alan Turing en 1937. Ambos modelos son universales ya que permiten computar cualquier cosa computable, de hecho, es posible implementar uno dentro del otro y viceversa.
La máquina de Turing, por un lado, define una serie de axiomas fácilmente trasladables al hardware:
Una cinta infinita que iremos modificando (estado).
Una lista de instrucciones que se ejecutarán y modificarán el estado (programa).
En esto fue en lo que se basó John von Neumann cuando en 1945 presentó su arquitectura de computación electrónica. Más tarde llegaría lo que todos conocemos como lenguaje ensamblador que no era otra cosa que implementar el modelo de Turing en la arquitectura de Neumann de una forma que fuese fácil de entender por humanos.
Por el otro lado, el cálculo lambda se compone de 3 elementos que nos obligan a pensar más en abstracto:
Valores.
Funciones.
Aplicación de funciones.
Solo con funciones y una forma de representar la información (valores) podremos ir haciendo uso de la composición y la recursividad para computar cualquier cosa computable.
Por tanto, decimos que la programación funcional es un paradigma de programación al igual que la programación orientada a objetos, pero esta se basa en cálculo lambda y concretamente en composición de funciones puras para modelar las soluciones de software.
Sin lugar a dudas, el modelo triunfante por excelencia ha sido aquel que se asemejaba más a como los ordenadores funcionaban. Así si el cálculo lambda nos obliga a abstraernos hasta el punto en el simplemente le decimos a la máquina que son las cosas, en el modelo de Turing en el que se basa la programación orientada a objetos, tendemos a pensar de forma más imperativa y mutable. Es decir, tenemos que decirle a la maquina como hacer todo, instrucción por instrucción mientras mantenemos el estado del programa en una cinta.
Perversión de la programación funcional
Aunque no fue el modelo “ganador”, el cálculo lambda y la programación funcional han sido muy usados desde entonces, sufriendo muchas veces un “cherry-picking” por parte de lenguajes puramente orientados a objetos que atraídos por las bondades de la programación funcional que es por filosofía más reutilizable y que clama de no tener (o aislar) los side-effects, se han ido quedando con lo que les interesaba.
De ahí que la connotación de “multiparadigma” haya hecho estragos en las propias definiciones de programación funcional y que nos hayamos pensado que por usar la librería phunctional1 (de PHP) o lodash2 (de Javascript) estemos usando dicho paradigma. Cuando la realidad es que dichas librerías son una suerte de cajones de utilidades preparados para trabajar con lenguajes orientados a objetos y no algo que existiría per se en un lenguaje puramente funcional.
Y el ejemplo está en la naturaleza de las funciones que ofrecen este tipo de librerías, como la denominada función map de lodash que solo aplica a colecciones iterables en lugar de a Functors3 o la función instance_of de phunctional que tiene una importante y explícita relación con clases, cuando un lenguaje funcional no es que no tenga ya clases, es que no tiene ni objetos.
Por esa regla de tres, cualquier librería que ofrezca funciones de diversa índole e inmutables, es programación funcional. Y no es correcto, ya que la interpretación de programación funcional NO es tener funciones que hagan cosas, sino el aislar los side-effects en una aplicación de la lógica funcional a través de usar funciones puras, composición de funciones y aplicación como únicos bloques de construcción.
En contraposición, la POO pone en el centro a los objetos y las funciones (o métodos en jerga OO) quedan subordianados a ellos y serán invocadas en secuencia para realizar (comúnmente) mutaciones en dichos objetos, reduciendo la reusabilidad, pero maximizando la encapsulación.
Hay, no obstante, librerías como Ramda que aparte de ser un cajón de funciones estás siguen una especificación de tipos algebraicos similares a los que nos encontraríamos en lenguajes puramente funcionales, como Haskell y construidos usan funciones combinadoras4.
En resumen, muchas de estas librerías de lenguajes orientados a objetos que se denominan asi mismas funcionales, quieren decir realmente que proporcionan funciones puras, curried, que trabajan de forma inmutable y usan la misma nomenclatura que usaría un lenguaje puramente funcional. Y esto no lo digo yo, sino que lo dicen ellas mismas5.
Javascript como lenguaje funcional
Quiere decir eso que una librería como phunctional ¿no es realmente programación funcional? Pues sí y no.
Me explico, estas librerías que no se basan como tal en tipos algebraicos funcionales son como un “crea tu propia aventura de la programación funcional”. Es decir, se extraen y aíslan cosas interesantes del paradigma que son fácilmente adaptables a un lenguaje fuertemente orientado a objetos (como es PHP en este caso), como la composición de funciones y la inmutabilidad, pero sin estar basado en tipos algebraicos puros.
Así si para ti la programación funcional es tener “funciones”, lodash y phunctional son por supuesto librerías funcionales. Pero si para ti la programación funcional es algo más cercano al cálculo lambda donde las funciones son ciudadanos de primera clase y no tienes nada más que eso (y quizá unas pocas estructuras) para construir software, entonces las librerías mencionadas son solo utilidades.
Esto mismo le ha pasado siempre a Javascript. Si para ti la programación orientada a objetos son Clases y Herencia, entonces Javascript no es un lenguaje de programación orientada a objetos. Pero si en cambio, para ti la OOP es lo mismo que para su inventor, Alan Kay que afirmaba que este tipo de programación solo tenía 3 características principales6:
Encapsulación.
Paso de mensajes (comunicación de unos objetos con otros, ya sea por API o por patrones de mensajes).
Late binding (hace referencia a que los objetos puedan evolucionar en runtime, permitiendo extenderlo y/o modificarlos una vez instanciados).
Entonces Javascript es un lenguaje de programación orientado a objetos como lo es Java y como lo es PHP, solo que en lugar de estar basado en clases está basado en prototypes7.
Así que la próxima vez que alguien te diga que Javascript no es un lenguaje de programación orientada a objetos “completo” por no tener “interfaces” o “clases” recuerda que, para el creador de la POO, ni las clases, ni la herencia ni las interfaces eran una característica esencial del paradigma.
Por último, y al carecer Javascript de clases y al tratar a las funciones como valores, este se acerca más al paradigma histórico de la programación funcional por lo que es más fácil implementarlo que en otros lenguajes como Java, sin embargo, y aunque tengamos librerías puramente funcionales como Ramda, mantengamos todo inmutable y solo usemos funciones, Javascript no es un lenguaje puro funcional porque no fuerza al programador a usar ese estilo, ni mantiene la pureza ni aísla los side-effects de forma implícita.
Si quieres lenguajes funcionales de verdad, te recomiendo que pruebes Elm lang o Haskell. Y lenguajes no puramente funcionales, pero al igual que en Javascript muy fáciles de implementar, te recomiendo Rust.
Funciones combinadoras
Y después de esta introducción vamos a ver como si que podemos acercarnos mucho al paradigma funcional histórico, basado en cálculo lambda8, donde solamente usando funciones podemos hacer cualquier cosa.
Las funciones combinadoras nos ayudan a descomponer y componer interfaces e implementaciones. Son meras funciones, que combinadas con otras nos dan superpoderes.
Pero antes de que entren en juego, tienes que entender bien a que nos referimos cuando hablamos de descomponer.
Primero que nada, tenemos dos formas de descomposición:
Descomponer una implementación
Descomponer una interface
Cuando hablamos de descomponer implementaciones, nos referimos a que dado el siguiente código:
fetch('/users').then(res => res.json()).then(res => console.log(res));Descomponemos el flujo en funciones puras que puedan ser componidas de nuevo en el futuro. Así podríamos hacer:
const getUsers = () => fetch('/users');
const responseToJSON = response => response.json();
const print = value => console.log(value); Y ahora, las volvemos a componer para obtener el mismo resultado que el primer código:
getUsers()
.then(responseToJSON)
.then(print);Al transformar cada proceso en una función pura, habilitamos esta composición y otras futuras, por lo que hemos ganado reutilización y legibilidad.
En segundo lugar, podemos descomponer interfaces. Veamos el siguiente código:
const map = (arr, fn) => arr.map(fn);Descomponer la interface no es otra cosa que, manteniendo el mismo output, cambiemos el cómo esa funcion es aplicada. Por ejemplo, en lugar de tener que aplicar completamente la función map pasándole todos sus argumentos, vamos a transformarla en una función curried:
const mapCurried = arr => fn => map(arr, fn);Hemos descompuesto la interface de map (sin modificar map, obviamente) y hemos creado una nueva forma de llamarla. En este caso la versión curried de la misma función.
Con estas dos técnicas podremos construir todo lo que queramos, creando por el camino funciones que nos ayuden a crear otras funciones. Y esas funciones que veremos a continuación están nombradas como si fuera pájaros.
Engañando a un Burlón
Esta es una traducción bastante abierta del libro de Raymond Smullyan9 titulado: To mock a Mockingbird10. Un libro de lógica combinatoria que, si no eres fan de palabros raros o en la carrera de informática llamabas “árabe” a la asignatura de lógica, te recomiendo no leer. Ya lo he hecho yo por ti para sacar la información valiosa para este artículo.
Smullyan fue un matemático y ornitólogo empedernido (estoy me lo he inventado) que dedicó su libro a hablar de combinadores y a ponerles el nombre de pájaros.
Así y en memoria de este buen señor, voy a ir presentándote a cada uno de ellos, y quién sabe si tiene usted algún problema y se los encuentra, quizá pueda contratarlos.
The Idiot bird
El pájaro idiota. Un nombre desafortunado, no cabe duda, pero bien empleado cuando asistimos a una de sus actuaciones en el bosque, ya que ante cualquier sonido que llega a este curioso animalito, nos devuelve de vuelta el mismo sonido. Así no tiene nada suyo, lo único que hace es un eco de lo que escucha.
Este pájaro podemos definirlo en Javascript tal que así:
const I = x => x;Date cuenta que este combinador, hace honor a su nombre y simplemente hace un eco de lo que recibe, sin modificarlo.
I('hello'); // 'hello'
I([1,2,3]); // [1,2,3]A veces en lugar de Idiota, decimos también Identity. No queremos ofender a nadie.
Para una función muy poco útil, pero nada más lejos de la realidad como ya veremos.
The Kestrel
Este amigo tiene algún tipo de curiosa obsesión. Ya que, si le damos un pájaro X y un pájaro Y, se queda embobado mirando a X.
Vamos a implementarlo:
const K = x => y => x;Toma dos valores, y devuelve el primero.
Este combinador tiene dos casos de uso concretos:
Crear funciones constantes:
K(1)(2) // 1Convertir valores a funciones:
const one = K(1)
one() // 1
De nuevo, parece una tontería, pero es muy útil cuando tenemos una función que en lugar de esperar un valor, espera una función que devuelva un valor, por ejemplo.
The Bluebird
Este es mi preferido, y enseguida entenderás porqué :).
Si un bosque contiene un Bluebird entonces por cada pájaro C y D, hay un pájaro E que estará sujetando a C con D.
La definición en código es:
const B = f => g => x => f(g(x));Efectivamente, el Bluebird ¡es un compose!. Y es que la composición de funciones en programación funcional es un combinador.
Espero que ya sepas todas las posibilidades de este azulado amigo, pero te muestro un pequeño código a continuación:
const inc = x => x + 1;
const double = x => x * 2;
const doubleNext = B(double)(inc);
console.log(doubleNext(1)) // 4 (dobla el siguiente número de 1, que es 2
The Cardinal
Este es uno de los pájaros más importantes en lógica combinatoria.
No he conseguido bucear lo suficiente en el libro de Raymond para entender su metáfora con los pájaros, así que me la voy a inventar yo siguiendo su misma línea (de sinsentido xd):
Dado un pájaro Y en una rama al lado izquierdo de un árbol y otro X en el lado derecho, el cardinal esta tan bizco que los ve a cada uno en el lado opuesto.
Y la definición sería:
const C = f => x => y => f(y)(x);Si te fijas bien este combinador es similar a la función flip que ya hemos usado en el blog11. Así, el cardinal descompone la interface de una función para invertir el orden en el que se le pasan sus argumentos.
Esto es muy usado para metamorfosear funciones, que, por el simple hecho de proporcionarles sus argumentos en orden distinto, se pueden usar de forma distinta:
const get = obj => prop => obj[prop];
const getWith = C(get);
const nameOf = getWith('name');
nameOf({ name: 'Elena' }) // Elena
The Thrush
Este pájaro es derivable de un Idiot y un Cardinal. Es decir, que para definir un T podemos hacer:
const T = C(I);Así lo de función combinadora no es un nombre casual, sino que son funciones que se combinan unas con otras para formar nuevas funciones combinadoras.
Te recuerdo la implementación de I y C:
const I = x => x;
const C = f => x => y => f(y)(x);
// Paso a paso
const CI = x => y => I(y);
const T = CI;
// o lo que es lo mismo
const T = x => y => y(x)Esto nos sirve para poder aplicar valores a funciones. De tal forma que, en un bosque con un nido, el thrush puede buscar comida (x) y llevarla al nido para alimentar a su prole (y).
Y un ejemplo más práctico:
const inc = x => x + 1;
const t42 = T(42)
t42(inc) // 43
The Vireo
Por último, esta ave es una variación del thrush. Así podíamos entender un thrush como un combinador que nos permite aplicar una función y sobre un valor x, y en el caso del vireo, aplicamos una función z a dos valores x e y.
El thrush alimentaba a su primogénito, el vireo podría alimentar a otro hijo más.
La definición por tanto sería:
const V = x => y => z => z(x)(y); Esta función combinadora es muy interesante porque nos permite representar información. Por ejemplo, dada la función add:
const add = x => y => x + y;Podría usarse para sumar dos números representados en un vireo de la siguiente forma:
const values = V(3)(2);
console.log(values(add)) // 5Hay muchos más combinadores, pero solo hemos visto los más sencillos y utilizados. Por ejemplo, la famosa aceleradora de startups de capital riesgo estadounidense Y combinator12, toma el nombre del combinador Y, que se utiliza comúnmente para hacer recursividad y cuya implementación haría explotar más de una cabeza.
Implementando tuplas
Después de introducir a las funciones combinadoras más usadas, toca pasar a ver un ejemplo práctico con ellos. Por lo que vamos a implementar la estructura Tupla usando solo funciones.
Empecemos definiendo que es una tupla: Una tupla es una colección de tamaño fijo (normalmente 2, pero podrían ser más) que pueden contener diferentes tipos de datos. Normalmente se usan para agrupar valores que tienen sentido que vayan juntos.
En nuestro caso vamos a definir eso, una tupla que llamaremos pair que representará un conjunto de dos valores. Y después tendremos una función first para obtener el primer elemento y una función second para, sorpresa, obtener el segundo.
Empecemos definiendo la estructura, para ello usaremos el vireo:
const pair = V;Recuerda que el vireo representa dos valores x e y, y nos permite aplicarlos sobre una función z. Esa función z para nosotros será el ‘selector’ que nos permitirá operar con la estructura. Así podemos crear un pair de la siguiente forma:
const myPair = pair('hello')('world');Ahora vamos a crear la función first que trabajando con esa estructura nos devuelva el primer elemento. Para ello vamos a usar el pájaro que dado dos pájaros siempre se quedaba siempre obsesionado mirando el primero, es decir, el kestrel:
const first = K;Y si la usamos como “selector” de nuestro pair:
myPair(first) // 'hello'De igual forma necesitamos ahora una función second que devuelva el segundo elemento. Para ello necesitamos modificar el kestrel para que en lugar de que se obsesione con el pájaro x lo haga con él y. Y eso lo podemos hacer uniéndolo al Idiot:
const second = K(I);Lo que hemos hecho aquí es lo siguiente:
K = x => y => x;
I = x => x
K(I) == y => I == y => x => x Hemos modificado el kestrel para que en lugar de que se obsesione con el primero, lo haga con el segundo. Así:
myPair(second) // 'world'Conclusión
Como has podido ver, el estilo más puro de programación funcional solo utiliza funciones y combinación de las mismas, para programar cualquier cosa. Además, has aprendido que decir que los lenguajes actuales sigan el paradigma funcional no los hace per se funcionales, sino que se han elegido un subconjuntode funcionalidades y características que cuadran bien o son fácilmente implementables en lenguajes orientados a objetos, de tal forma que podamos obtener lo mejor de dos mundos, aunque con limitaciones.
Igualmente, hemos visto que hay librerías “funcionales” más funcionales que otras, ya que muchas veces el marketing nos lleva a pensar que estamos usando un paradigma cuando realmente solo estamos usando una serie de utilidades componibles que no siguen ninguna especificación funcional ni implementan tipos algebraicos.
El sacar la función map de un Array y hacerla inmutable, no la vuelve funcional, sino una función de alto orden.
Sin embargo, tampoco está bien ser dogmático ni en una cosa ni en la otra, ya que todos los lenguajes actuales tienen las suficientes diferencias con los paradigmas históricos funcionales o orientados a objetos, como para ser nombrados de otra forma o al menos no regirlas bajo las mismas “reglas” que sus predecesores. De forma habitual, un lenguaje OO puede volverse multiparadigma tomando prestado features de los lenguajes funcionales, pero un lenguaje funcional es difícil que tome prestado nada de OO puesto que es un enfoque completamente diferente.
Finalmente, y en aras de reforzar un enfoque funcional tradicional donde las funciones son el centro y único bloque de construcción, hemos visto cómo podemos construir hasta estructuras de datos, solo con funciones sin ni siquiera usar objetos.
Nada más por hoy :)
https://github.com/Lambdish/phunctional
https://lodash.com/
https://www.youtube.com/watch?v=m3svKOdZijA&app=desktop
https://github.com/lodash/lodash/wiki/FP-Guide#lodashfp
https://medium.com/javascript-scene/the-forgotten-history-of-oop-88d71b9b2d9f
https://stackoverflow.com/questions/186244/what-does-it-mean-that-javascript-is-a-prototype-based-language
https://es.wikipedia.org/wiki/C%C3%A1lculo_lambda
https://es.wikipedia.org/wiki/Raymond_Smullyan
https://www.amazon.es/Mock-Mockingbird-Other-Logic-Puzzles/dp/0192801422
https://es.wikipedia.org/wiki/Y_Combinator
