Nuestra agenda para hoy serÃĄ aprender quÃĐ son applicative functors (aplicativos de ahora en adelante) usando javascript. SÃ, usaremos javascript. No me juzguen, es lo que sÃĐ. Al terminar este artÃculo sabrÃĄn cÃģmo crearlos, cÃģmo reconocerlos y les enseÃąarÃĐ un truco que puede o no resultarles Útil alguna vez.
Bien, empecemos desde el principio.
ÂŋQuÃĐ es un functor?
Desde un punto de vista "tÃĐcnico" podemos decir que son un tipo de contenedor. VerÃĄn, la manera mÃĄs fÃĄcil de implementar un functor es "envolviendo" un valor dentro de una estructura. Para interactuar con el valor dentro del functor se provee un mÃĐtodo normalmente llamado map, este nos permite transformar el valor usando una funciÃģn (un callback) y luego envuelve el nuevo valor otra vez en una estructura del mismo tipo.
Veamos quÃĐ puede hacer map. Para familiarizarnos con este tipo de estructuras voy a demostrar sus capacidades usando un tipo de dato comÚn en javascript, los arreglos.
const numbers = [1];
const plus_one = (number) => number + 1;
numbers.map(plus_one);
// [ 2 ]
ÂŋQuÃĐ pasa aquÃ?
Tenemos un nÚmero dentro de un arreglo, luego usamos map para acceder a ÃĐl y transformarlo usando una funciÃģn, y despuÃĐs el nuevo valor que obtenemos es puesto nuevamente en un arreglo. Eso es todo. Eso es bÃĄsicamente el comportamiento que debe tener un functor.
Ahora bien, los arreglos no son los Únicos que siguen este patrÃģn, en javascript tenemos otra estructura que actÚa de la misma manera, la clase Promise. Con las promesas no tenemos un mÃĐtodo map pero tenemos uno llamado then, no son exactamente iguales en cuestiÃģn de comportamiento pero se acerca lo suficiente.
const number = Promise.resolve(1);
const plus_one = (number) => number + 1;
number.then(plus_one);
// Promise { <state>: "pending" }
// 2
Aquà ocurre la misma cosa, tenemos un valor dentro de una estructura (una promesa), tenemos un mÃĐtodo que nos da acceso al valor (then) y finalmente el nuevo valor queda atrapado en una nueva instancia de la misma estructura.
Y ese es el patrÃģn. Ya hemos cubierto todo lo que necesitamos saber sobre los functors por ahora. Si desean saber mÃĄs detalles sobre ellos revisen este artÃculo: El Poder de Map.
ÂŋListos para seguir?
Applicatives
Resulta que los aplicativos son functors con caracterÃsticas extras. Ellos nos dan la habilidad de mezclar dos functors. EspecÃficamente, nos permiten aplicar una funciÃģn dentro de un functor a un valor que tambiÃĐn esta dentro de un functor.
Espera... ÂŋquÃĐ? ÂŋUna funciÃģn dentro de un functor?
SÃ. Algo asÃ.
const plus_one = (number) => number + 1;
// Y luego
[plus_one];
// Ã
Promise.resolve(plus_one);
ÂŋPor quÃĐ alguien harÃa eso?
Buena pregunta. La respuesta, nadie lo harÃa. Si hablamos de patrones comunes en javascript este no es uno de ellos. Eso no significa que los aplicativos no tenga un uso.
Volviendo a nuestra definiciÃģn. Normalmente, si tenemos un valor y una funciÃģn somos capaces de aplicar dicha funciÃģn asÃ: una_funciÃģn(un_valor). Eso no funcionarÃa si ambos estÃĄn encerrados dentro de una estructura. Para "arreglar" eso, los aplicativos tienen un mÃĐtodo llamado ap (apply abreviado) que se encarga de sacar la funciÃģn y el valor de sus respectivas estructuras y aplicar la funciÃģn.
Y es en este punto que me gustarÃa mostrarles un ejemplo de un tipo de dato que ya sigue las reglas de los aplicativos pero no se me ocurre ninguno. Pero no teman, tomemos esto como una oportunidad para hacer algo mÃĄs.
Crear un Aplicativo desde cero
Para no complicarnos mucho lo que haremos serÃĄ crear una pequeÃąa extensiÃģn de la clase Promise. Vamos a hacer que una promesa se comporte mÃĄs como un functor aplicativo.
ÂŋPor donde comenzamos?
- La meta
Lo que queremos hacer es retrasar ejecuciÃģn de una promesa. Normalmente cuando se crea una promesa esta ejecuta la "tarea" asignada inmediatamente pero no queremos eso, esta vez queremos controlar cuando se ejecuta la tarea. Para lograr nuestro objetivo crearemos un mÃĐtodo llamado fork, este se encargarÃĄ de crear la promesa y preparar las funciones en caso de ÃĐxito y error.
function Task(proc) {
return {
fork(err, success) {
const promise = new Promise(proc);
return promise.then(success).catch(err);
}
}
}
Genial. Ahora vamos comparar esto con una promesa normal.
let number = 0;
const procedure = function(resolve, reject) {
const look_ma = () => {
console.log(`IT WORKED ${++number} times`);
resolve();
};
setTimeout(look_ma, 1000);
};
new Promise(procedure); // Esta se ejecuta inmediatamente
Task(procedure); // Esta no hace nada
Task(procedure) // Esta sÃ
.fork(
() => console.error('AAHHH!'),
() => console.log('AWW')
);
Si ejecutan ese cÃģdigo deberÃan ver estos mensajes despuÃĐs de 1 segundo.
IT WORKED 1 times
IT WORKED 2 times
AWW
Ahora que tenemos lo que queremos vamos al siguiente paso.
- Haz un functor
Como ya saben los aplicativos son functors, significa que ahora necesitamos un mÃĐtodo map.
Repasemos una vez mÃĄs. ÂŋCuÃĄl es el comportamiento que esperamos de map?
- DeberÃa darnos acceso al valor almacenado internamente a travÃĐs de una funciÃģn.
- DeberÃa retornar un nuevo contenedor del mismo tipo. En nuestro caso una nueva instancia de
Task.
function Task(proc) {
return {
+ map(fn) {
+ return Task(function(resolve, reject) {
+ const promise = new Promise(proc);
+ promise.then(fn).then(resolve).catch(reject);
+ });
+ },
fork(err, success) {
const promise = new Promise(proc);
return promise.then(success).catch(err);
}
}
}
ÂŋQuÃĐ pasa en map? Bueno, primero recibimos el argumento fn esa serÃĄ una funciÃģn. Luego, retornamos una instancia de Task. Dentro de esa nueva instancia construimos la promesa justo como hacemos en fork pero esta vez es mÃĄs "seguro" porque no se ejecutarÃĄ inmediatamente. El siguiente paso es colocar las funciones que requiere promise en su respectivo orden, primero fn la cual transformarÃĄ el valor, luego resolve que marca el "fin" de la tarea actual y finalmente catch que recibirÃĄ la funciÃģn reject de la tarea actual.
Podemos probar lo que tenemos hasta ahora.
const exclaim = (str) => str + '!!';
const ohh = (value) => (console.log('OOHH'), value);
Task((resolve) => resolve('hello'))
.map(exclaim)
.map(ohh)
.fork(console.error, console.log);
Si ejecutan eso tal cual como esta deberÃan ver esto.
OOHH
hello!!
Pero si eliminan fork deberÃan tener esto.
SÃ, asà es, deberÃan tener absolutamente nada. Ya hemos terminado con el patrÃģn functor de nuestro Task.
- Vamos por Apply
Ya estamos a medio camino. Lo que haremos ahora serÃĄ que crear ap.
Como yo lo veo ap es map pero con un giro en la trama: la funciÃģn que queremos aplicar estÃĄ dentro de una instancia de Task [mÚsica dramÃĄtica suena en el fondo].
Con esa idea en la mente podemos implementar ap.
function Task(proc) {
return {
map(fn) {
return Task(function(resolve, reject) {
const promise = new Promise(proc);
promise.then(fn).then(resolve).catch(reject);
});
},
+ ap(Fn) {
+ return Task(function(resolve, reject) {
+ const promise = new Promise(proc);
+ const success = fn => promise.then(fn);
+ Fn.fork(reject, success).then(resolve);
+ });
+ },
fork(err, success) {
const promise = new Promise(proc);
return promise.then(success).catch(err);
}
}
}
ÂŋNotan la diferencia con map? No se preocupen igual les dirÃĐ, la diferencia es que para aplicar la funciÃģn en Fn usamos fork en lugar de interactuar con una promesa normal. Eso es todo. Veamos si funciona.
const to_uppercase = (str) => str.toUpperCase();
const exclaim = (str) => str + '!!';
const Uppercase = Task((resolve) => resolve(to_uppercase));
const Exclaim = Task((resolve) => resolve(exclaim));
const Hello = Task((resolve) => resolve('hello'));
Hello.ap(Uppercase).ap(Exclaim)
.fork(console.error, console.log);
ÂĄLo logramos! Ahora podemos mezclar funciones que se encuentren dentro de aplicativos. Pero Task aÚn no puede entrar en el club de los aplicativos. Tenemos que ocuparnos de otra cosa primero.
- El ingrediente olvidado
Los aplicativos deben ser capaces de colocar cualquier valor dentro de la unidad mÃĄs simple de su estructura.
La clase Promise tiene algo asÃ. En lugar de hacer esto.
new Promise((resolve) => resolve('hello'));
Usualmente hacemos esto.
Promise.resolve('hello');
Luego de usar Promise.resolve podemos empezar a usar mÃĐtodos como then y catch. Eso es lo que le hace falta a nuestro Task.
Para esta implementar esto necesitaremos un mÃĐtodo estÃĄtico. Para esto existen varios nombres, algunos lo llaman "pure" otros lo llaman "unit" y tambiÃĐn estÃĄn quienes lo llaman "of".
Task.of = function(value) {
return Task((resolve) => resolve(value));
};
Y ahora sÃ, finalmente podemos decir que tenemos un aplicativo.
Algo que puedes usar en tu desarrollo diario
Poder crear tu propio tipo de dato es genial, Âŋpero no serÃa mejor si pudiÃĐramos aplicar estos patrones con las estructuras que ya existen?
Tengo buenas y malas noticias. La buena es que definitivamente podemos. La mala es que a veces puede ser incÃģmodo.
Sigamos con el ejemplo de Task que hemos usado hasta ahora. Pero ahora digamos que queremos usar map y ap pero no queremos crear una nueva estructura. ÂŋQuÃĐ hacemos? Un par de funciones serÃĄn suficientes.
Si ya estÃĄn familiarizados con los patrones que buscan, escribirlos en unas funciones estÃĄticas bastarÃĄ. Asà lucirÃa nuestro Task como funciones simples.
const Task = {
of(value) {
return Promise.resolve(value);
},
map(fn, data) {
return data.then(fn);
},
ap(Fn, data) {
return Fn.then(fn => data.then(value => fn(value)));
}
};
Para usar map serÃa asÃ.
const to_uppercase = (str) => str.toUpperCase();
Task.map(to_uppercase, Task.of('hello'))
.then(console.log);
Y ap funciona de la misma manera.
const exclaim = (str) => str + '!!';
Task.ap(Task.of(exclaim), Task.of('hello'))
.then(console.log);
Puedo percibir su escepticismo desde aquÃ. Sean pacientes. Ahora, map parece medio Útil pero ap no tanto. No se preocupen, aÚn podemos usar ap para un bien mayor. ÂŋY si les digo que podemos tener una versiÃģn "mejorada" de map? Nuestro map sÃģlo trabaja con funciones que reciben un argumento y eso es bueno pero puede haber ocasiones en que necesitemos mÃĄs que eso.
Digamos que tenemos una funciÃģn que recibe dos argumentos pero en su mayorÃa los argumentos casi siempre vienen de dos promesas diferentes. Asà que, en nuestra situaciÃģn imaginaria tenemos estas funciones.
function get_username() {
return new Promise((resolve) => {
const fetch_data = () => resolve('john doe');
setTimeout(fetch_data, 1000);
});
}
function get_location() {
return new Promise((resolve) => {
const fetch_data = () => resolve('some place');
setTimeout(fetch_data, 500);
});
}
function format_message(name, place) {
return `name: ${name} | place: ${place}`;
}
Cuando usamos format_message sus argumentos vienen de las otras dos funciones get_username y get_location. Esas Últimas dos son asÃncronas, entonces tal vez se vean tentados a usar las palabras clave Async/Await pero esa no serÃa una buena idea. VerÃĄn, esas funciones no depende la una de la otra, estarÃamos desperdiciando tiempo si hacemos que se ejecuten en secuencia cuando deberÃan ejecutarse de manera concurrente. Una soluciÃģn puede encontrarse en la forma de Promise.all y se ve asÃ.
Promise.all([get_username(), get_location()])
.then(([name, place]) => format_message(name, place))
.then(console.log);
Ahà lo tienen. Eso funciona. Pero nosotros podemos hacer algo mejor, ya que tenemos los aplicativos de nuestro lado. AdemÃĄs, ya tenemos ese objeto Task. Ahora sÃģlo vamos a agregar una funciÃģn mÃĄs, esta harÃĄ lo mismo que estÃĄ haciendo Promise.all.
Task.liftA2 = function(fn, A1, A2) {
const curried = a => b => fn(a, b);
return Task.ap(Task.map(curried, A1), A2);
};
Ya les explico el nombre despuÃĐs. Ahora veamos cÃģmo se usa.
Task.liftA2(format_message, get_username(), get_location())
.then(console.log);
ÂŋNo les parece que esto es un poco mejor?
Y sÃ, es cierto que pueden presentar argumentos contra la implementaciÃģn de liftA2 e incluso todo el objeto Task, pero todos los patrones que he mostrado aquà deberÃan funcionar para los aplicativos que puedan encontrarse por ahÃ.
Como un ejercicio pueden intentar implementar map y ap para la clase Set. Vean que clase de cosas graciosas descubren en el proceso.
En fin, el nombre liftA2. En el paradigma funcional cuando logramos que una funciÃģn trabaje con un tipo de contenedor como lo son los functors se dice que "levantamos" (lift en inglÃĐs) esa funciÃģn al contexto de ese contenedor. ÂŋQuÃĐ significa eso de contexto? Bueno, en el mundo de los arreglos la funciÃģn que le proveen a map puede ejecutarse muchas veces (o ninguna), en el contexto de una promesa la funciÃģn que suministran a then sÃģlo se ejecuta cuando la promesa culmina su tarea de manera exitosa. ÂŋYa ven lo que digo? Bien. ÂŋY el A2? Ya saben, es porque recibe sÃģlo dos argumentos.
Hay otro truco que se puede hacer con los aplicativos pero aÚn no entiendo completamente cÃģmo funciona asà que serÃĄ en otra ocasiÃģn.
ConclusiÃģn
ÂŋQuÃĐ aprendimos hoy, clase?
- Aprendimos de los functors:
- QuÃĐ hacen.
- QuÃĐ patrones deben seguir.
- Aprendimos de los aplicativos
- QuÃĐ son.
- QuÃĐ hacen.
- CÃģmo crear uno desde cero.
- CÃģmo hacer un mÃĐtodo
apaÚn si la estructura con la trabajamos no tiene soporte para el patrÃģn de los aplicativos. - Y esa cosa
liftA2que se ve genial.
ÂŋAprendieron todo eso? Dios santo. Ustedes son los mejores.
Bueno, mi trabajo aquà ha terminado.
Fuentes
- Fantasy Land
- Static Land
- Fantas, Eel, and Specification 8: Apply
- Fantas, Eel, and Specification 9: Applicative
- Professor Frisby's Mostly Adecuate Guide to Functional Programming. Chapter 10: Applicative Functors
- Learn you a Haskell: Functors, Applicative Functors and Monoids
Gracias por su tiempo. Si este artÃculo les pareciÃģ Útil y quieren apoyar mis esfuerzos para crear mÃĄs contenido pueden dejar una propina en ko-fi.com/vonheikemen.

Top comments (4)
Un tema super avanzado e interesante el que acabo de leer. Ahora, este tipo de patrÃģn se vuelve un tanto esotÃĐrico de usar, Âŋno crees? Para jugar y aprender estÃĄ bien pero en el dÃa a dÃa, toca usar lo normalito ð
Muy buen artÃculo!
Concuerdo.
Lo que pasa es que los aplicativos resuelven un problema que sÃģlo te encuentras si utilizas los functors con mucha frecuencia. En nuestro dÃa a dÃa, en el mejor de los casos, los utilizamos de manera trivial. Si la comunidad de javascript comenzara a adoptar patrones como los que yo describo aquÃ: tÃĐcnicas de composiciÃģn, tarde o temprano tendrÃan que acudir a cosas como los aplicativos.
Cierto. Me hace recordar a los bloques en Ruby.
Los bloques son una forma de ejecutar cÃģdigo en medio de otra ejecuciÃģn. Un ejemplo muy pobre serÃa algo como:
Y hay un montÃģn de artÃculos explicando bloques, procs y lambdas (cosas similares) pero en realidad el caso de uso es tan especÃfico que muchos devs Ruby no usan bloques cuando escriben cÃģdigo.
Terminamos usando los bloques en DSLs de gemas o funciones de la librerÃa estÃĄndar del lenguaje ð
Puede que este tema que explicas de functors viva algo similar. En el dÃa a dÃa no tiene tanto uso pero en librerÃas de terceros donde hay muchas abstracciones, seguro tendrÃĄn mÃĄs cabida.
No del todo. Este patrÃģn de los functors puede ser Útil en muchas situaciones. Algunos lenguajes compilados de tipado fuerte los han adoptado para resolver problemas como los valores ausentes o el manejo de errores. Nosotros los que nos ganamos la vida con lenguajes dinÃĄmicos (javascript, ruby, python, etc...) simplemente aprendimos a vivir sin ellos.