Table of Contents
JavaScript es un lenguaje que, junto a su ecosistema, está evolucionando muy deprisa. Nuevas librerías y frameworks aparecen a un ritmo frenético, ofreciendo nuevas arquitecturas y funcionalidades. En artículos anteriores ya hemos visto qué nos ofrece TypeScript y cómo migrar una aplicación antigua hacía este Superset de JS. En cambio, en este artículo me gustaría poner un par de sencillos ejemplos de cómo podemos usar conceptos de Programación funcional en JavaScript. Para ello, usaremos una librería de poderosas abstracciones que ofrece un amplio set de Monads: Ramda-fantasy, y otra librería llamada Ramda, que nos ayudará a construir pipelines funcionales y conseguir inmutabilidad. Cabe destacar, que no son las únicas librerías que nos ofrecen esto. Podéis fácilmente encontrar alternativas a las mismas.
Programación funcional en javascript
La programación funcional puede mejorar notablemente el código de una aplicación, sin embargo, sus conceptos pueden ser algo difíciles de entender al principio. Y dado que para explicarlos en detalle, ya podríamos estar varios artículos, hemos preferido poner dos ejemplos prácticos de código, que sirvan para presentar los conceptos. Si le resultan de interés al lector, invitamos a que los sigan explorando.
1- Maybe Monad
En el primer ejemplo veremos cómo podemos evitar verificar si una variables es Null. Para ello, imaginemos que en nuestra aplicación existen Users que tienen la siguiente forma:
const someUser = {
name: 'some_name',
email: '[email protected]',
settings: {
language: 'sp'
}
};
Y tenemos una función que devuelve un mensaje de saludo en el idioma que tiene seteado el usuario.
const allGreetings = {
'en': 'Hello!',
'sp': 'Hola!',
'fr': 'Bonjour!'
};
const getGreetingForUser = (user) => {
//to be implemented
}
Veamos una implementación de la función ‘getGreetingForUser’, siguiendo un modelo imperativo:
const getGreetingForUser = (user) => {
if (!user) {
return allGreetings.en;
}
if (user.settings && user.settings.language) {
if (allGreetings[user.settings.language]) {
return allGreetings[user.settings.language]
} else {
return allGreetings.en;
}
} else {
return allGreetings.en;
}
};
console.log(getGreetingForUser(someUser));
Como podéis observar, hemos tenido que verificar si el user existía, si tenía un idioma seteado, y si ese idioma está entre aquellos para los cuales tenemos un saludo preparado. Y en caso de que algo vaya mal, devolvemos el idioma por defecto, que en nuestro caso es el inglés.
Ahora veamos la misma función, pero esta vez, usaremos conceptos de Programación Funcional en su implementación.
const getGreetingForUser = (user) => {
return RamdaFantasy.Maybe(user)
.map(Ramda.path(['settings', 'language']))
.chain(maybeGreeting);
};
const maybeGreeting = Ramda.curry((greetingsList, userLanguage) => {
return RamdaFantasy.Maybe(greetingsList[userLanguage]);
})(allGreetings);
console.log(getGreetingForUser(someUser).getOrElse(allGreetings.en));
Esta solución puede parecer confusa al principio, pero una vez entendidos los conceptos que usamos, no lo es tanto.
Para tratar con la situación de un posible caso de null o undefined, usamos el monad Maybe. Nos permite crear un “wrapper” alrededor del objeto, y asignar un comportamiento por defecto, para el caso de que dicho objeto sea null.
Comparemos las dos soluciones:
//En lugar de verificar si user es null
if (!user) {
return allGreetings.en;
}
//usamos:
RamdaFantasy.Maybe(user) //metemos user en el wrapper
———————-
//En lugar de
if (user.settings && user.settings.language) {
if (allGreetings[user.settings.language]) {
//usamos:
<userMaybe>.map(Ramda.path(['settings', 'language'])) //map trabaja con los datos en caso de existir
——————–
//En lugar de devolver el valor por defecto en el else
return indexURLs['en'];
//usamos:
.getOrElse(allGreetings.en) //getOrElse devolverá o bien devolverá el valor de Some, o bien el valor por defecto que le especificamos.
2- Either Monad
El monad Maybe es muy útil cuando conocemos el comportamiento por defecto en caso de que haya un error por Null. En cambio, podemos usar el monad Either en caso de tener una función que queremos que lance un error, o si encadenamos varias funciones que lanzan errores y queramos saber cual es la que falla.
Ahora, imaginemos que queremos calcular el precio de un producto, teniendo en cuenta el IVA y los posibles descuentos aplicables. Para ello ya contamos con este código:
const withTaxes = (tax, price) => {
if (!_.isNumber(price)) {
return new Error("Price is not numeric");
}
return price + (tax * price);
};
const withDiscount = (dis, price) => {
if (!_.isNumber(price)) {
return new Error("Price is not numeric");
}
if (price < 5)
return new Error("Discounts not available for low-priced items");
}
return price - (price * dis);
};
const isError = (e) => e && e.name === 'Error';
const calculatePrice(price, tax, discount) => {
//to be implemented
}
Veamos una implementación de la función ‘calculatePrice’, siguiendo un modelo imperativo:
const calculatePrice = (price, tax, discount) => {
const priceWithTaxes = withTaxes(tax, price);
if (isError(priceWithTaxes)) {
return console.log('Error: ' + priceWithTaxes.message);
}
const priceWithTaxesAndDiscount = withDiscount(discount, priceWithTaxes);
if (isError(priceWithTaxesAndDiscount)) {
return console.log('Error: ' + priceWithTaxesAndDiscount.message);
}
console.log('Total Price: ' + priceWithTaxesAndDiscount);
}
//calculamos el precio final de un producto que vale 25, siendo el IVA 21%, y el descuento 10%.
calculatePrice(25, 0.21, 0.10)
Ahora veamos como podemos reescribir esta función usando el monad Either.
Either tiene dos constructores, Left y Right. Lo que se quiere conseguir, es almacenar las excepciones en Left, y el resultado normal (happy path) en Right.
Primero, cambiemos las funciones ya existentes withTaxes y withDiscount, para que devuelvan Left en caso de error, y Right en caso de que todo vaya bien:
const withTaxes = Ramda.curry((tax, price) => {
if (!_.isNumber(price)) {
return RamdaFantasy.Either.Left(new Error("Price is not numeric")); //ponemos el error en Left
}
return RamdaFantasy.Either.Right(price + (tax * price)); //ponemos el resultado en Right
});
const withDiscount = Ramda.curry((dis, price) => {
if (!_.isNumber(price)) {
return RamdaFantasy.Either.Left(new Error("Price is not numeric")); //ponemos el error en Left
}
if (price < 5) {
return RamdaFantasy.Either.Left(new Error("Discounts not available for low-priced items")); //ponemos otro error en Left
}
return RamdaFantasy.Either.Right(price - (price * dis)); //ponemos el resultado en Right
});
Luego, creamos una función para el caso Right (mostrar el precio), y otra para el caso Left (mostrar el error), y las usamos para crear el monad Either:
const showPrice = (total) => { console.log('Price: ' + total) };
const showError = (error) => { console.log('Error: ' + error.message); };
const eitherErrorOrPrice = RamdaFantasy.Either.either(showError, showPrice);
Por último, solo nos queda ejecutar el monad para calcular el precio final:
//calculamos el precio final de un producto que vale 25, siendo el IVA 21%, y el descuento 10%.
eitherErrorOrPrice(
RamdaFantasy.Either.Right(25)
.chain(withTaxes(0.21))
.chain(withDiscount(0.1))
)
Conclusiones: programación funcional en javascript
Como hemos podido observar, una vez desgranado el código tanto con el monad Maybe como con Either, su complejidad no es tan grande. Y, si usados correctamente, pueden ayudar a que nuestro código sea más legible fácil de mantener.
El único inconveniente podría sea la barrera de entrada que hemos de superar al principio; pero esto se arregla fácilmente viendo algunos ejemplos y haciendo pruebas.
Esperamos que con este artículo sobre programación funcional en javascript, podáis empezar a explorar los conceptos de Programación Funcional, tanto en Javascript como en otros lenguajes con los que trabajáis.