Nuestra industria está plagada de modas. Todavía recuerdo la programación orientada a aspectos, la inyección de dependencias, la sobreutilización de XML, la arquitectura orientada a servicios, programación funcional,… Algunas de estas modas quedan en nada, y de otras quedan algunos posos que es la esencia útil después de quitarles polvo y paja.
Pues bien, de un tiempo a esta parte se oye un murmullo en forma de… promesas sobre todo en el mundo de JavaScript. Han surgido al menos uno, dos y tres frameworks, una especificación y otra especificación y tropecientos artículos de considerable extensión a favor y en contra. Incluso algunos básicamente pregonan que la gente no se está enterando de qué va esto. Qué locura. Con tanta agitación me he querido enterar bien y después de leer y leer y de informarme todo lo posible llego a la conclusión de que las promesas no me aportan nada nuevo y me hacen cambiar mis APIs y ¡hasta me hacen escribir más código!
¿Pero de qué va esto de las promesas?
Para explicarlo me voy a saltar a otro lenguaje donde creo que sí son útiles. Me voy a Java un momento pero casi cualquier otro lenguaje valdría. Supongamos que queremos descargar contenido de dos páginas web que devuelven texto plano y lo queremos concatenar. Lo común en un lenguaje como Java donde las APIs suelen ser síncronas sería hacer algo así (voy a obviar todo el tiempo la gestión de errores por brevedad):
String a = descargarContenidoWeb("...");
String b = descargarContenidoWeb("...");
// hacer algo con a y b
String c = a + b;
Esto está bien, pero es mejorable porque como las llamadas son bloqueantes (esperan) la segunda descarga sólo empieza cuando la primera ha terminado. Veamos cómo sería usando Future (la forma estándar de usar promesas en Java). Reimplementaríamos el método descargarConteniedoWeb internamente para que devolviese una promesa y luego lo usaríamos así:
Future<String> a = descargarContenidoWeb("...");
Future<String> b = descargarContenidoWeb("...");
// hacer algo con a y b
String c = a.get() + b.get();
Nada más llamar a descargarContenidoWeb el contenido se empieza a descargar, pero la ejecución no se bloquea y la segunda descarga también se empieza a ejecutar en segundo plano a la vez que la primera. Como el contenido se está descargando y todavía no podemos viajar al futuro, no se devuelve un String con el contenido, sino una promesa. Pero tarde o temprano queremos obtener finalmente el contenido. Para eso está el método get() de las promesas. Este método chequea si la descarga ha terminado, y si no ha terminado bloquea hasta que termine. Con el get() estamos bloqueando, pero las descargas ya se han lanzado a la vez en segundo plano. Sencillo y útil, no?
Ahora volvamos a JavaScript. En JavaScript no hay APIs bloqueantes (salvo muy puntuales excepciones). Las funciones que pudieran ser bloqueantes en vez de devolver una variable reciben un callback como parámetro que será llamado cuando la operación termine. Esto lo habremos visto muchas veces si hemos usado JavaScript en el frontend (con jQuery por ejemplo) para hacer AJAX o para escuchar eventos HTML, etc. Así pues una llamada a la función descargarContenidoWeb tendría que ser una cosa así
descargarContenidoWeb('...', function(err, contenido) {
// ...
})
console.log('Esto ese ejecuta mientras se descarga el contenido')
Como la función no bloquea podemos llamar a la función dos veces consecutivas sin anidación, para descargar los contenidos en paralelo.
descargarContenidoWeb('...', function(err, a) { ... })
descargarContenidoWeb('...', function(err, b) { ... })
Listo, ya estamos descargando los contenidos en paralelo. Pero… ahora tenemos dos funciones de callback y necesitamos hacer algo cuando terminen ambas descargas. ¿Cuándo sabemos que las dos han terminado? ¿Dónde ponemos ese código? ¿Cómo nos las arreglamos? Bien, veamos una forma rápida de gestionarlo
var _a = null, _b = null
function finalizar() {
if (_a && _b) {
var c = _a + _b
}
}
descargarContenidoWeb(function(err, a) {
_a = a; finalizar()
})
descargarContenidoWeb(function(err, b) {
_b = b; finalizar()
})
Mmmm… Solucionado, pero poco elegante y poco legible. ¿Se puede mejorar? Sí, con un callback handler como async (se puede usar tanto en nodejs como en el navegador; en nodejs es el segundo módulo más popular). Con async nuestro problema podría solucionarse así:
var urls = ['...', '...']
async.map(urls, descargarContenidoWeb, function(err, results) {
var c = results[0] + results[1]
})
La función map() recibe un array de elementos y una función que los procesará. Finalmente devuelve un array de resultados. Así de sencillo.
¿Y qué hay de las promesas? Bien, pues con promesas en vez de usar los callbacks de toda la vida tendríamos que reimplementar la función descargarContenidoWeb para que devolviese una promesa. Después usarla con Q sería una cosa así:
var a = descargarContenidoWeb('...')
var b = descargarContenidoWeb('...')
Q.allResolved([a, b]).then(function (promises) {
var c = a.valueOf() + b.valueOf()
})
¿Queda bien, no? Sí, pero no veo ninguna mejora, incluso ocupa más código, y hemos tenido que reimplementar la función y añadir un framework externo que tendrá que usar tanto quien implementa la función como quien la usa, y el nivel de anidamiento es el mismo, y…
…Y además librerías como async hacen muchísimas más cosas sin tener que reimplementar nada. En nuestro ejemplo imaginemos que tenemos mil URLs en vez de dos. Nos interesaría no descargar las mil de vez, sino ir descargando como mucho un número máximo de items a la vez. Pues bien con async es tan sencillo como usar mapLimit() en vez de map(). Simplemente tiene un argumento más en el que le decimos el máximo de items a procesar a la vez. ¡Voilá! Yo estoy encantado, de verdad que no he encontrado ninguna otra combinación de lenguaje o framework donde fuera tan sencillo hacer cosas en segundo plano de forma tan simple y flexible. Os invito a echar un vistazo a todas las funciones y ejemplos del módulo async.
¡Es más! Si tenemos un nivel medio de JavaScript en apenas 25 líneas de código podemos hacer nuestro micro-callback-handler (lo he llamado collector) sin necesidad de usar una librería externa como async. El código queda así:
var c = collector()
descargarContenidoWeb('...', c.bind('a'))
descargarContenidoWeb('...', c.bind('b'))
c.collect(function(errors, results) {
var c = results.a + results.b
})
Lo único que hace bind() es crear una función de callback que guardará el resultado con el nombre que le damos y finalmente hay un callback para recolectar todos los resultados. Super sencillo y de nuevo una forma simple y legible de hacer lo mismo y sin necesidad de modificar la implementación interna de la función.
Mis conclusiones
- Las promesas son útiles en un lenguaje con APIs síncronas (ej: Java) porque ayudan a hacer cosas en paralelo, pero en un lenguaje con APIs no bloqueantes pierden utilidad porque ejecutar cosas en paralelo es el comportamiento por defecto. Con la ayuda de un callback handler además podemos hacer virguerías.
- Un callback handler permite hacer todo lo que podemos hacer con las promesas y más. Y además sin tener que cambiar las implementaciones de nuestras funciones para usar promesas.
- Las promesas no resuelven el callback-hell o pirámide de la muerte. La pirámide de la muerte se resuelve muy fácilmente: dividiendo el código en funciones.
Finalmente he dejado unos ejemplos comparando el uso de una librería de promesas versus la librería async. Se compara el uso de una función aislada y la ejecución en paralelo de dos funciones.
Y después de esta chapa… ¿Creéis que las promesas en JavaScript son una moda más? ¿Creéis que son útiles? ¿Merecen la pena? ¿Me estoy dejando algo?