Programación Asíncrona en Dart: Funciones y Streams

by , under Technical practices

FacebooktwitterlinkedinmailFacebooktwitterlinkedinmail

Me he decidido a escribir este articulo tan técnico en este momento porque creo que, aunque no sigue un orden lógico dentro de la serie de artículos sobre Dart que está por venir, es muy importante para los que empiecen con este lenguaje.

Desde mi anterior articulo me he dedicado a sondear posibles pilas tecnológicas para implementar una aplicación web en Dart. Lo que he descubierto es que, aunque la capa servidora está aún muy verde, es fácil montar una aplicación web que genere contenido basado en plantillas y siguiendo el modelo MVC (al estilo de Struts).

De momento estoy usando MongoDB como base de datos con el driver a pelo (bastante sencillo puesto que Mongo usa JSON, que se mapea fácilmente a/desde objetos Dart) y voy a empezar a mirar un ORM (Objectory sobre MongoDB).

Las librerías en las que me estoy basando son:

  • Rikulo Stream Server (http://rikulo.org/projects/stream)
  • mongo_dart (https://github.com/vadimtsushko/mongo_dart)

La parte cliente la estoy postergando de momento porque está mucho más estable el lenguaje en la parte cliente y, por tanto, ofrece menos dudas en cuanto a su viabilidad. De hecho ya hay un framework para el cliente (que viene con Dart) llamado web_ui. Por supuesto no es obligatorio usarlo y hay más frameworks cliente, pero el hecho de que venga con el lenguaje supongo que favorecerá bastante su implantación.

De todo el desarrollo que llevo hecho y de mis incursiones en la lista de discusión de Google Dart -por cierto la recomiendo: es muy interesante ver como se va moldeando el lenguaje entre toda la comunidad- he podido sacar varias conclusiones. Una de ellas es que lo que más le puede costar a alguien que venga del mundo Java (y especialmente si ha programado sólo en servidor) es la programación asíncrona.

Programación asíncrona

En Dart todas las APIs de entrada/salida son asíncronas puesto que el lenguaje Dart es mono-thread. Esto quiere decir que cada programa Dart sólo dispone de un thread con lo que, si lo bloqueamos para realizar una lectura o una escritura de, por ejemplo, un socket paramos todo el programa. Evidentemente esto en un servidor web querría decir que dejaríamos de atender más clientes, con lo que la liaríamos parda. Para resolver este “problema” las APIs de Dart son asíncronas (al estilo de Node.js). De esta forma, cuando leemos de un socket, la llamada vuelve inmediatamente y Dart nos vuelve a llamar cuando hay datos disponibles.

El modelo asíncrono tiene seguidores y detractores. Para los que están acostumbrados a programar de forma síncrona y mono-thread es un infierno. Pero cuando programas de forma síncrona y tienes varios threads que cooperan entre si entonces no está tan claro que es más complejo.

La sincronización multi-thread es difícil de entender y, una vez la has entendido, siempre te deja dudando de si no te habrás dejado algún bloqueo mutuo o un cuello de botella. En programación asíncrona no hace falta sincronizar pero la ejecución se bifurca infinidad de veces. Conclusión: la programación síncrona es más fácil de escribir, pero mas difícil de implementar correctamente. Por el contrario, escribir código asíncrono es más tedioso pero también es más difícil meter la pata (al menos en temas de concurrencia).

Esto, que queda muy bonito dicho así, se empieza a complicar cuando tienes que leer de un socket, pero a la vez de un fichero y, ademas, acceder a la base de datos. En este escenario ya tienes tres fuentes asíncronas y el quebradero de cabeza puede ser considerable, porque el flujo de programación se puede convertir en un grafo muy complejo.

Afortunadamente, tantos años de programación asíncrona (por ejemplo en Javascript) han dado su fruto y disponemos de un patrón bastante potente: las promesas. Una promesa representa una computación aún no realizada que nos dará un determinado resultado (o un error) en el futuro.

Como era de esperar, Dart ha interiorizado este patrón en el lenguaje por medio de dos clases: Future y Stream. Un Future es lo mismo que una promesa y un Stream es como un Future recurrente. Por ejemplo, un Future<String> es un objeto que representa que, en algún momento, una computación asíncrona devolverá un String, mientras que un Stream<String> es un objeto que representa que una computación asíncrona generara uno o más Strings de forma recurrente en el futuro.

Hasta aquí todo bien. Se entiende la teoría. Pero luego usarlo en la practica no es tan fácil porque los Futures se pueden encadenar entre si de muchas maneras y no es nada evidente como hacerlo. Este es el punto en el que creo que bastante gente que venga de la programación síncrona en servidor puede tirar la toalla.

Así que, para vuestro solaz y disfrute, os voy a poner un ejemplo de como se pueden encadenar llamadas asíncronas para que, cuando hagáis vuestros pinitos en Dart, no tengáis que jurar en arameo (bastante tenemos con Dart como para tener que aprender también lenguas muertas).

El ejemplo está directamente sacado del código que estoy escribiendo:

  var db = new Db("mongodb://127.0.0.1/test");
  var entry = new Entry.empty();
  parsePostBody(connect.request).then( (params) {
    return ObjectUtil.inject( entry, params );
  }).then( (entry) {
    return db.open();
  }).then( (_) {
    return db.collection("Entries").insert( entry.toMap() );
  }).then( (_) {
    db.close();
    sendRedirect(connect, "/");
  });

Este código abre una conexión a la base datos, crea un objeto del modelo de negocio llamado Entry y después llama a un método asíncrono que descodifica los parámetros enviados en una petición POST de HTTP. Este método es asíncrono porque tiene que leer del socket que conecta nuestro servidor con el navegador (cliente) así que devuelve un Future<String>. Sobre ese Future<String> llamamos al método then(), que recibe una función creada al vuelo (llamadas también funciones lambda o clausuras, o “closures” en inglés) con un solo parámetro (un mapa que contiene los parámetros del POST). Esta “closure” es invocada cuando los parámetros están listos, porque se han leído en su totalidad del socket, e inmediatamente se llama a ObjectUtil.inject(), que también es asíncrona y devuelve un Future<Entry>.

Sin embargo, observad que, en lugar de llamar a then() otra vez sobre el resultado de ObjectUtil.inject(), lo que hacemos es devolver el Future<Entry> como valor de retorno de la “closure” inicial. Esto es lo que se llama encadenamiento de futuros y sirve para poder poner el siguiente then() a continuación de la closure y, de esta forma, obtener un código que, si bien es asíncrono, se asemeja mucho a -o se lee como- código síncrono.

De esta forma podemos encadenar tantas acciones asíncronas como queramos sin volvernos locos. Imaginemos que pesadilla sería este código sin encadenamiento de futuros. Nos quedaría algo así:

var db = new Db("mongodb://127.0.0.1/test");
var entry = new Entry.empty();
parsePostBody(connect.request).then( (params) {
  ObjectUtil.inject( entry, params ).then( (entry) {
    db.open().then( (_) {
      db.collection("Entries").insert( entry.toMap() ).then( (_) {
        db.close();
        sendRedirect(connect, "/");
      });
    });
  });
});

Creo que es bastante evidente cual de las dos formas da más “asquete” y espero que todos sabremos captar las bondades del encadenamiento de futuros.

Quiero advertir que en este código he omitido deliberadamente el control de errores por no liarlo más, pero baste decir que hay otro método paralelo al then() llamado catchError() que nos permite escribir cosas del estilo:

parsePostBody(connect.request).then( (params) {
  print( "Habemus params: ${params}" );
}).catchError( (err) {
  print( "Mal rollo (AKA error): ${err}" );
});

Fijémonos en lo gracioso del asunto: el catchError() se está aplicando no sobre el Future que devuelve parsePostBody(), sino sobre el Future que devuelve el then() (no se podía esperar que devolviese otra cosa el then() 😉 . Lo bueno de esto es que el catchError() captura los errores tanto del parsePostBody() como del código de la closure que procesa su resultado. Un lío, ¿a que si?.

Pero en realidad, lo bueno de todo esto es que, a pesar de que es complicado entender todo el flujo y el manejo que hacen los Futures por dentro, se hace muy intuitivo programarlo y funciona todo como esperamos (al menos con algoritmos mundanos yo no he tenido ningún problema).

Veamos este ejemplo de la implementación del método parsePostBody:

Future<Map<String,String>> parsePostBody( HttpRequest request ) {
  var contentType = request.headers["content-type"][0];
  switch( contentType ) {
    case "application/x-www-form-urlencoded": 
      return IOUtil.readAsString( request ).then( (body) {
        return _parseUrlEncodedBody(body);
      });
    default: 
      return new Future.immediateError("Unsupported content: ${contentType}");
  }
}

Map<String,String> _parseUrlEncodedBody(String body) {
  . . .
}

Si nos fijamos, el método parsePostBody() devuelve el Future que devuelve el método then() de otro Future que devuelve IOUtil.readAsString(). Visto así no hay quien lo entienda, pero… si ponemos return _parseUrlEncodedBody(body); en la closure del then, ¿qué acaba devolviendo el método parsePostBody()? En efecto, el mapa que retorna el método síncrono _parseUrlEncodedBody(). ¿A que mola?

Conclusión

Personalmente es la primera vez que uso promesas. Sabía que existían en Javascript pero yo me había quedado en los callbacks (sobre todo porque, habitualmente, uso GWT en vez de Javascript a pelo y en GWT no hay closures). Por eso me gustaría obtener feedback de los que las hayáis usado en Javascript y me gustaría que los que habéis pasado toda la vida programando en el servidor de forma síncrona no os amilanaseis a la primera de cambio 😉 : la programación asíncrona no es agradable, pero tampoco es el fin del mundo. Es sólo un modelo más de concurrencia.

Os animos a dejar vuestros comentarios sobre el tema más abajo. Gracias.

Futuros artículos

En el próximo artículo trataremos los problemas que se puede encontrar alguien que venga de Javascript al empezar con Dart. Ya voy anticipando que ese problema será el uso de tipos de datos, por lo tanto el artículo tratará de cómo funciona el sistema de tipos opcionales de Dart. También echaremos un vistazo a las partes de la guía de estilo de programación en Dart relacionadas con este tema.

Leave a Reply