ES6: Las promesas

 Ay las promesas. Esas grandes incomprendidas. Todo el mundo las usa, pero nadie las entiende. Ese uso mixto con los callbacks… esa manera de querer eliminarlas del mapa para dar paso a las  ‘async function‘ y así de paso seguir convirtiendo a JavaScript en un lenguaje típico de POO, esos antipatrones que llevo viendo durante tantos años…

Tengo que reconocer que soy un gran defensor de las promesas. Siempre me han parecido un gran mecanismos de gestión. Como veremos hoy, las promesas pasan a formar parte del estándar a partir de EcmaScript 6. La solución que han pensado es fácil y no tendrá mucho misterios para los senior. Sin embargo, aprovecharé el post para enlazar con mi serie funcional ya que, como veremos, las promesas tienen mucho que agradecer a este estilo de programación.

No me demoro más. Espero que os guste:

¿Qué son las promesas?

Las promesas son un patrón de diseño que nos permiten controlar la ejecución de un determinado cómputo del cual no sabemos cómo ni cuándo se nos va a devolver un determinado valor. Una promesa es un objeto que envuelve este comportamiento para que por medio de una máquina de estados podamos controlar cuándo un valor está disponible o no.

Las promesas son muy socorridas en ejecuciones de código asíncrono pues, si no tenemos claro cuando nuestro cómputo va a devolver un valor, podemos dejar una promesa en espera que se encargue de gestionar esta devolución.

Las promesas no solo nos son útiles en marcos asíncronos. Es cierto que la mayoría de veces es usado en en este contexto, pero como veremos en apartados siguientes, puede ser un buen patrón para encadenar ejecuciones sobre un valor determinado.

Una promesa se encarga de envolver una función de nuestro código. Esta promesa puede encontrarse en 3 estados principales: un estado pendiente donde la promesa se queda pendiente del valor que se necesita, un estado de completado que viene dado si nuestro envoltorio es capaz de obtener el valor deseado o un estado de rechazado si la promesa no ha obtenido el valor esperado.

La máquina de estados es sencilla y se parece a esta:

promise-states.png

¿Cómo las hemos utilizado hasta ahora?

Las promesas no son nada nuevo en los entornos front. Como decíamos, es una de las mejores formas de tratar la asincronía. Sabiendo que JavaScript es uno de los lenguajes con tareas asíncronas por antonomasia, no es de extrañar que muchos frameworks y librerías se hayan encargado de intentar hacer implementaciones sobre dicho patrón.

En los último años hemos usado las implementaciones de jQuery, hemos usado promesas en AngularJS con su servicio ad hoc ‘$q’ y hemos usado librerías específicas para gestionar promesas en NodeJS con implementaciones como las de Q.

Para hacer un pequeño ejemplo, vamos a crear un función que haga uso de promesas. Usaremos la implementación de jQuery para comparar más tarde con la implementación nativa.

Nuestra función se va encargar de esperar un tiempo de 5 segundos para saber si ya es la hora de cenar. Podemos hacer esto con promesas de la siguiente manera.

function isDinnerTime() {
    const q = $.Deferred();

    setTimeout(function () {
        const now = new Date();

        if (now.getHours() >= 22) {
             q.resolve('yes');
        } else {
             q.reject('no');
        }
    }, 5000);

    return q.promise();
}

isDinnerTime()
    .then(data => console.log('success', data))
    .fail(data => console.log('error', data));

La función ‘isDinnerTime’ lo que hace es esperar 5 segundos para tomar la decisión de si es hora de cenar o no. Es un ejemplo tonto, no sirve para mucho, pero es didáctico.

Como el ‘setTimeout’ es un proceso asíncrono, lo que hacemos es devolver un objeto promesa y delegar el control del flujo en este objeto para que por medio de la condición, decidir si la promesa pasa de estado pendiente a completado o a rechazado.

Las promesas devuelven un objeto que suele contener los métodos ‘then’ y ‘fail’ o formas similares. El primero ejecutará la función si todo ha ido como se esperaba, y el segundo se ejecuta si ha habido algo inesperado en la ejecución, como pudiera ser una excepción.

¿Cómo es la nueva especificación?

En la última especificación de EcmaScript 6, por fín contamos con unas directrices para incluir este patrón dentro del lenguaje. Lo que parece una tontería es de suma importancia:

Si estamos diciendo que las promesas son un mecanismo muy socorrido a la hora de controlar flujos asíncronos en JavaScript, quiere decir que en casi, por no decir todos los desarrollos que hagamos, vamos a depender de una librería de terceros que nos permita la funcionalidad.

Las promesas nativas acaban por fin con una dependencia, quitándonos código que mantener y consiguiendo que, por fín, se homogenice la interfaz del objeto promesa.

Además, encontrándose de forma nativa, cada navegador podrá implementar la funcionalidad de la forma más óptima posible. Salimos todos ganando.

La forma en la que se implementado las promesas me gusta. Quitamos mucho código indeseado y nos permite centrarnos en el código que provoca el fenómeno no lineal del que queremos gestionar su flujo.

Han implementado las promesas por medio de un objeto denominado ‘Promise’ – obvio. ‘Promise’ cuenta con la siguiente API:

Promise.all(iterable)

‘Promise.all’ devuelve una promesa que se resuelve cuando todas las promesas del argumento iterable han sido resueltas. Un ejemplo de esto es lo siguiente:

const pr1 = fetch('flowers.jpg');
const pr2 = fetch('clouds.jpg');
const pr3 = fetch('animals.jpg');

Promise.all([pr1, pr2, pr3])
    .then(doSomething)
    .catch(doError);

Promise.race(iterable)

Recibe un array de promesa como el anterior método, sin embargo, se resuelve o se rechaza tan pronto una de las promesas se resuelva o rechace.

Promise.reject(reason)

Devuelve un objeto ‘Promise’ que es rechazado con la razón dada.

Promise.resolve(value)

Devuelve un objeto ‘Promise’ que es resuelto con el valor dado.

El prototipo de ‘Promise’ contiene dos métodos más:

Promise.prototype.catch(onReject)

Que permite indicar una función a ejecutarse si la promesa pasa al estado rechazado.

Promise.prototype.then(onFufilled, onRejected)

que permite indicar una función de completado y rechazado para según el estado en que pase la promesa.

Convirtamos el ejemplo del apartado anterior a ES6 Style:

function isDinnerTime() {
    return new Promise(function(resolve, reject) {
        setTimeout(function () {
            const now = new Date();

            if (now.getHours() >= 22) {
                resolve('yes');
            } else {
                reject('no');
            }
        }, 5000);
    });
}

isDinnerTime()
    .then(data => console.log('success', data))
    .catch(data => console.log('error', data));

A mi personalmente, me gusta más.

Ejemplo: Envolviendo ‘XMLHttpRequest’ con promesas

Veamos un ejemplo nuevo. En esta ocasión, estoy envolviendo el comportamiento del objeto ‘XMLHttpRequest’. De esta forma, podamos usarlo por medio de una API sencilla que sea capaz de devolver promesas. Nos permite, de este modo, gestionar la devolución de valores de un backend.

Para ello, vamos a hacer un pequeño closure que se encargue de llevar a cabo el patrón Adapter.

const $http = (function () {
    // API Library
    return {
        get: function (url, payload) {
            return _ajax('GET', url, payload);
        },
        post: function (url, payload) {
            return _ajax('POST', url, payload);
        },
        put: function (url, payload) {
            return _ajax('PUT', url, payload);
        },
        delete: function (url, payload) {
            return _ajax('DELETE', url, payload);
        }
     };
      
     // Call AJAX
     function _ajax(method, url, payload) {
         return new Promise(function (resolve, reject) {
             const xhr = new XMLHttpRequest(url);
             const uri = _getUri(url, method, payload);

             xhr.open(method, uri);
             xhr.send();
             xhr.onload = onload;
             xhr.onerror = onerror

             function onload() {
                 if (this.status == 200) {
                     resolve(this.response);
                 } else {
                     reject(this.statusText);
                 }
             };
 
             function onerror() {
                 reject(this.statusText);
             }
         });
    }

    // Convert Payload Object in a Uri String
    // _getUri :: (String, String, Object) -> String
    function _getUri(url, method, payload) {
        let uri = url;

        if (payload && (method === 'POST' || method === 'PUT')) {
            uri += '?';
            let argcount = 0;

            for (var key in payload) {
                if (payload.hasOwnProperty(key)) {
                    if (argcount++) {
                        uri += '&';
                    }
                    uri += encodeURIComponent(key) + '=' + encodeURIComponent(payload[key]);
                }
            }
        }

        return uri;
     }
})();

De esta forma, hemos recreado el servicio que tiene internamente AngularJS para realizar llamadas AJAX, denominado $http. Su uso es el siguiente:

const url = 'https://developer.mozilla.org/en-US/search.json';
const query = { topic : 'js', q: 'Promise'};

$http.get(url, query)
     .then(data => console.log(1, 'success', data))
     .catch(error => console.log(2, 'error', error));

Las promesas y la programación funcional

Si por algo me gustan las promesas, es por la posibilidad de concatenar funciones dentro de un pipeline para manipular el valor retornado por un servidor.

¡Vaya! ‘pipeline’, ‘concatenar’, ‘funciones’. Parece que las promesas tienen algo que ver con la programación funcional.

Puede que a lo largo del articulo, te estuvieras dando cuenta que una promesa, este patrón tan común en desarrollos front, no es otra cosa que una mónada. Una promesa es un wrapper que protege funciones o valores y que permite concatenar cómputos.

¿Como que concatenar? Si, concatenar. La función ‘then’ es una función que devuelve otra promesa. Si nos lo llevamos a nuestro entorno monádico, parece que ‘then’ tiene un comportamiento bastante parecido al de nuestro querido amigo ‘map’.

Tanto ‘map’ como ‘then’ permiten ejecutar funciones sobre unos valores encerrados en un contexto. Y el nuevo valor es envuelto en otro envoltorio del mismo tipo, en este caso en una promesa. Es por ello que yo puedo hacer algo parecido a esto con el caso anterior:

$http.get(url, query)
     .then(JSON.parse)
     .then(R.prop('documents'))
     .then(R.sortBy(R.prop('id')))
     .then(R.head)
     .then(R.prop('url'))
     .then(R.toUpper)
     .then(console.log)
     .catch(error => console.log(2, 'error', error));

Hasta si repasamos las lecciones anteriores, la forma en que concatenamos los ‘then’ y el último ‘catch’ recuerdan a la mónada ‘Either’.

Si recordamos, ‘Either’ nos permite concatenar cómputos, como aquí, y si alguno de ellos devolvía un elemento no funcionaba como se esperaba, el tren de ‘then’ iba delegando su ejecución hasta que llevábamos a ‘catch’, de esta forma teníamos un control de excepciones en un entorno funcional sin romper la cadena.

Sin saberlo nunca yo ya estaba usando programación funcional. Ahora entiendo porque  me han gustado siempre tanto las promesas: Me permitían tener un flujo lineal y una gestión controlada de mis excepciones. ‘catch’ no es ni más ni menos que nuestro método ‘orElse’. Y además, me permitía concatenar funciones puras.

Si volvemos un poco al principio, la primera función, la de ‘isDinnerTime’, estaba envolviendo una función que tenía efectos laterales. Ese ‘setTimeout’ provocaba que la transparencia referencial se rompiese.

Con ‘promise’ estamos encerrando el comportamiento de la función en un envoltorio específico. Si lo pensamos, es parecido a lo que ocurría en la mónada IO. Al final una llamada asíncrona no deja de ser una entrada no controlada hacia mi lógica.

Parece que todo va encajando 🙂

Soporte

Aunque la funcionalidad está muy bien y nos va a ahorrar unos cuantos Kb. Es bueno que siempre tengamos en cuenta como se encuentra el soporte de los navegadores y NodeJS en lo relativo con el estándar.

El navegador se encuentra en unas buenas cuotas, pero necesitaremos de un ‘wrapper’ o un ‘polyfill’ para hacer uso de ello. Existen muchos: como este y este.

En NodeJS parece que la cosa está bastante estable en casi todas las versiones más modernas:

Si estamos haciendo uso de BabelJS, no tenemos que preocuparnos porque tiene soporte.

Conclusiones

Aunque hoy el tema parecía más destinado a enseñar cosas nuevas sobre EcmaScript 6, hemos comprobado que las promesas son una solución pensada para controlar la asincronía en estilos funcionales.

Las promesas son una de las primeras mónadas que vemos en nuestro día a día cuando desarrollamos con JavaScript. Es por ello que es tan importante conocer los principios y conceptos de la programación funcional, nos ayuda a conocer mejor nuestro entorno y las herramientas que tanto tiempo llevamos usando.

Nos leemos 🙂

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s