Back to HomeBack to Home

¿Pero qué es una promesa?

Toda nuestra vida ha estado envuelta en promesas, como el amor adolescente, y como no es suficiente, hemos decidido traer las promesas a la programación también.

Utilizo el término promesa porque estoy más acostumbrado a este por mi lenguaje de programación principal, pero en otros existen bajo otro nombre: Futures en Rust, y de una manera un poco alejada, están las goroutines en Go.

Empecemos.

 

¿Qué es una promesa?

Dependiendo de dónde lo leas o quién te lo cuente, la película es diferente. La definición más popular, es que es una abstracción que te permite modelar un flujo de trabajo asíncrono/concurrente.

En JavaScript hemos pasado por muchas, muchas maneras de trabajar a la par con el navegador/sistema operativo, que juegan un papel clave en el mundo de las promesas (más sobre esto adelante).

Antes de las promesas, teníamos los callbacks, que eran una manera de decirle al navegador que cuando terminara de hacer algo, ejecutara una función que le pasábamos. El problema con los callbacks es que si tenías que hacer muchas cosas, terminabas con un código que parecía un árbol de navidad:

function setTimer(time, cb) {
	setTimeout(() => {
		cb();
	}, time);
}

setTimer(1000, () => {
	setTimer(1000, () => {
		setTimer(1000, () => {
			setTimer(1000, () => {
				console.log('4 segundos');
			});
		});
	});
});

Esto es lo que conocemos como callback hell, basado en el infame callback-based approach.

Las contras de trabajar de esta manera son, por ejemplo, que el uso de memoria crece de manera lineal con el número de callbacks que hay, o que debbugear uno de estos callbacks también puede ser complejo.

Las promesas son la manera que tenemos ahora de acabar con esta complejidad:

function setTimer(time) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve();
		}, time);
	});
}

setTimer(1000)
	.then(() => setTimer(1000))
	.then(() => setTimer(1000))
	.then(() => setTimer(1000))
	.then(() => console.log('4 segundos'));

A este estilo de programación se le llama the continuation-passing style.

La diferencia no se queda únicamente en la sintaxis, sino que las promesas nos devuelven una máquina de estados, donde cada una puede estar pendiente, resuelta o rechazada (pending, fullfilled or rejected). En el ejemplo anterior si llamamos a setTimer(200), nos devuelve una promesa con estado pending.

Trabajando de esta manera solucionamos una gran cantidad de problemas que nos traía el uso de callbacks, pero aún seguimos con el problema de tener diferentes formas de escribir apps.

 

Async/Await al rescate

Desde aquí un pequeño saludo a mi novia, que nos gusta cantar la canción de El Rey León pero diciendo "async await" en vez de (pido perdón) awimbawé

Para poder hablar sobre async-await primero tenemos que saber lo que es una corrutina (coroutine). La explicación más sencilla es que una corrutina es una función que depende de otras piezas, como el sistema operativo o el navegador, y que puede ser pausada y reanudada en cualquier momento.

Además, vienen en dos sabores: simétricas y asimétricas, que son en las que nos vamos a fijar. La diferencia es el lugar donde producen un valor: en el caso de las simétricas, puede ser en varios sitios muy específicos, como por ejemplo otra corrutina, mientras que las asimétricas solo producen (lo que conocemos como yield) en el scheduler, que de una manera u otra podríamos llamarlo Event Loop (el famoso).

Cuando vemos la palabra async, lo que estamos haciendo es pedirle al compilador que transforme lo que parece una función en nuestro código en una promesa. await, por otro lado, le cede el control al scheduler, y la función se suspende hasta que la promesa se resuelve.

La gran ventaja de esto es que podemos escribir programas con concurrencia de una manera muy muy similar a la que escribimos programas con operaciones secuenciales:

function setTimer(time) {
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve();
		}, time);
	});
}

async function main() {
	await setTimer(1000);
	await setTimer(1000);
	await setTimer(1000);
	await setTimer(1000);
	console.log('4 segundos');
}

main();

Además, son bastante fáciles de implementar.

 

¿Qué sacamos de todo esto?

La próxima vez que alguien os diga que JavaScript no tiene concurrencia, podéis decirle que se equivoca. La gente suele confundir concurrencia con paralelismo, de ahí la negativa.

Si os ayuda a diferenciar entre uno y otro, a mi me viene bien pensar que concurrencia es trabajar de manera eficiente: progresar en una tarea cuando no puedes progresar en otra. Paralelismo, si bien se desbloquea mediante la concurrencia, necesita más recursos que esta, pero lo que nos permite es progresar en varias tareas al mismo tiempo.

En otros lenguajes de programación esto lo podemos conseguir a través de tasks o green threads, pero esto lo podemos dejar para otro día.

¡Hasta la próxima!