Skip to content

dart

dartis (y 2)

En este artículo se continúa la revisión de las características ofrecidas por dartis, un cliente para Redis escrito en Dart que publiqué hace unos días.

Pub/Sub

Redis permite que los clientes entren en un modo de funcionamiento especial en el que se implementa el patrón Publish/Subscribe. En este modo los clientes pueden suscribirse a canales y recibir mensajes a través de ellos. Es decir, que Redis se comporta como un sistema de mensajería. En este modo los clientes sólo pueden ejecutar los comandos SUBSCRIBE, UNSUBSCRIBE, PSUBSCRIBE, PUNSUBSCRIBE, PING y QUIT.

El servidor envía, tanto las respuestas a las suscripciones a canales, como los propios mensajes publicados en dichos canales, en un stream de eventos con una estructura de mensajes distinta a la que se recibe cuando el cliente se encuentra operando en el modo normal, por lo que se debe conocer en todo momento en que modo se encuentra trabajando el cliente para poder procesar correctamente las respuestas recibidas del servidor.

La conexión a este modo de funcionamiento se realiza de forma explícita en dartis a través de una factoría estática. Y los eventos recibidos se publican a través de un Stream.

final pubsub = await PubSub.connect<String, String>('redis://localhost:6379');

pubsub.subscribe(channel: 'dev.dart');

pubsub.stream.listen(print, onError: print);

Tener una clase especializada para implementar este modo permite reducir la complejidad del código. Máxime cuando Redis sólo permite a los clientes en este modo ejecutar un conjunto de comandos muy concretos y salir terminando la conexión con el servidor. Aunque impide que el cliente ejecute otros comandos antes de entrar en este modo, como por ejemplo validarse contra el servidor utilizando una clave.

Monitor

Redis permite también que los clientes trabajen en modo monitor. En este modo no pueden ejecutar ningún comando, se limitan a recibir mensajes del servidor. Los mensajes recibidos contienen un texto con detalles acerca de todos los comandos ejecutados por todos los clientes contra el servidor. Es decir, que cada vez que un cliente ejecuta un comando, el servidor envía un mensaje informando de tal acción a todos los clientes que se encuentran en modo monitor. Esto resulta de utilidad para depurar aplicaciones de forma no intrusiva.

La conexión a este modo de funcionamiento se realiza de forma explícita en dartis a través de una factoría estática. Y los eventos recibidos se publican a través de un Stream.

final monitor = await Monitor.connect('redis://localhost:6379');

monitor.start();

monitor.stream.listen(print);

De igual forma que en el modo del apartado anterior, disponer de una clase especializada reduce la complejidad de la implementación, pero no permite ejecutar ningún comando previo a la entrada en este modo.

Inline Commands

Redis permite la ejecución de comandos sin tener que implementar el protocolo RESP. En este modo los comandos se envían como cadenas de texto plano al servidor. Es similar a como se haría a través de una sesión Telnet, escribiendo directamente en una consola de texto.

final terminal = await Terminal.connect('redis://localhost:6379');

terminal.run('PING\r\n'.codeUnits);

terminal.stream.listen(print);

Las respuestas recibidas desde el servidor en este modo están formateadas siguiendo el protocolo RESP, por lo que este modo es adecuado para comprobar la respuesta exacta del servidor ante un determinado comando, evitando cualquier tipo de agente intermedio que la procese o altere de alguna forma.

Transacciones

Redis permite ejecutar varios comandos dentro del contexto de una transacción. Aunque no es una acción de “todo o nada” como habitualmente se sobreentiende que funciona una transacción con la mayoría de software existente.

Una transacción empieza en Redis ejecutando el comando MULTI y termina ejecutando el comando EXEC. Todos los comandos que se ejecutan después de MULTI se encolan y se ejecutan cuando se llama a EXEC. Si se produce un error encolando un comando la transacción se aborta en el momento que se llama a EXEC, no en el momento que se produce el error encolando el comando. Y si se produce un error durante la ejecución de un comando encolado se continúa con el siguiente comando encolado, no se produce un rollback de lo ejecutado hasta el momento.

EXEC retorna el resultado de la ejecución de todos los comandos ejecutados en el contexto de la transacción, tanto los terminados con éxito como los terminados con error, para dar la oportunidad al cliente de actuar en función del resultado individual de cada comando.

await commands.multi();

commands.set(key, 1).then(print);
commands.incr(key).then(print);

await commands.exec();

Con este patrón no se puede utilizar await  sobre cada comando, ya que el Future de cada comando no se completa hasta que se obtiene la respuesta del servidor, algo que no ocurre hasta ejecutar EXEC.

Una transacción iniciada con MULTI puede abortarse ejecutando el command DISCARD antes de llamar a EXEC. El comando DISCARD termina la transacción en curso descartando todos los comandos encolados hasta el momento.

Por último, comentar que en el contexto de una transacción no se debe utilizar el comando CLIENT REPLY, ya que en determinados casos el cliente puede perder la sincronía con el servidor. Es por ello que las transacciones en Redis están documentadas como una funcionalidad a ser deprecada, prefiriéndose el uso de scripts en Lua en el servidor.

Scripts Lua

Redis permite ejecutar scripts en Lua en el servidor. Esta característica es ofrecida a través de comandos, por lo que no requiere ninguna implementación especial por parte del cliente.

await commands.eval<void>(
  'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}',
  keys: [key1, key2],
  args: ['first', 'second']);

La única característica particular a tener en cuenta con esta funcionalidad es que la salida de la ejecución de un script puede ser de cualquier tipo. Lo mismo puede resultar en una primitiva, como una cadena de texto o un entero, o un array de valores heterogéneos. Para lidiar con esta situación, dartis admite un parámetro que permite mapear la respuesta del servidor en cualquier tipo que se quiera.

class CustomMapper implements Mapper<List<String>> {
  @override
  List<String> map(Reply reply, RedisCodec codec) =>
    codec.decode<List<String>>(reply);
  }
...
final results = await commands.eval<List<String>>(
  'return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}',
  keys: [key1, key2],
  args: ['first', 'second'],
  mapper: CustomMapper());

print(results); // ['key1', 'key2', 'first', 'second']

En el ejemplo del código anterior, el mapper convierte la respuesta del servidor en una lista de cadenas de texto utilizando las facilidades que ofrece dartis para crear serializadores y deserializadores personalizados.

Serializadores/Deserializadores

dartis tiene un sistema de conversores dinámico que permite añadir conversores nuevos o sobreescribir los existentes. Un conversor es una clase que convierte una primitiva en una lista de bytes de cara a ser enviado al servidor, y convierte una lista de respuestas del servidor o bytes en una primitiva de cara a que un cliente pueda utilizarla en una aplicación.

Por defecto hay registrados conversores para los tipos de uso más habitual como enteros, decimales, cadenas de texto y listas. Utilizando UTF-8 por defecto para las cadenas de texto.

Los conversores que transforman primitivas en listas de bytes se denominan codificadores, y son clases que se crean extendiendo de Converter o Encoder. Por ejemplo, el siguiente código muestra un conversor que transforma instancias del tipo DateTime de Dart en listas de bytes.

class DateTimeEncoder extends Encoder<DateTime> {
  @override
    List<int> convert(DateTime value, [RedisCodec codec]) =>
    utf8.encode(value.toString());
  }
}

Por su parte, los conversores que transforman listas de respuestas del servidor o bytes en una primitiva se denominan decodificadores, y son clases que se crean extendiendo de Converter o Decoder. Por ejemplo, el siguiente código muestra un conversor que transforma una respuesta del servidor en una instancia del tipo DateTime de Dart.

class DateTimeDecoder extends Decoder<SingleReply, DateTime> {
@override
DateTime convert(SingleReply value, RedisCodec codec) =>
value.bytes == null ? null : DateTime.parse(utf8.decode(value.bytes));
}

Todos los codificadores se registran utilizando el atributo codec del cliente.

client.codec.register(
  encoder: DateTimeEncoder(),
  decoder: DateTimeDecoder());

Permitir definir tipos personalizados facilita trabajar de forma sencilla con los comandos propios que pueda definir cualquier módulo para Redis. Siendo un módulo una extensión que se puede añadir a un servidor Redis, a modo de plugin, y que dartis soporta de manera natural.

Y estas son básicamente las principales características de la librería. En el propio repositorio del proyecto puede encontrarse más documentación y ejemplos de uso.

Para terminar, comentar que el proyecto está desarrollado utilizando Visual Studio Code como IDE con tan sólo un par de extensiones. EditorConfig para garantizar la uniformidad del formato de los ficheros, y Dart Code para la integración de las herramientas del SDK, en particular el formateador de código y el analizador de código estático. Travis como servidor de integración continua y Coveralls para el análisis de la cobertura del código. El proyecto tiene implementadas unos trescientos casos de prueba, incluyendo tanto pruebas unitarias como de integración, con un noventa por ciento de cobertura.

dartis (1)

Hace un par de días liberé dartis, un cliente para Redis escrito en Dart. Empecé a desarrollarlo para probar algunas de las novedades de la versión 2 de Dart y al final he acabado con un cliente sencillo pero bastante completo.

RESP

REdis Serialization Protocol (RESP) es el protocolo que implementa Redis para comunicar los clientes con el servidor. Es un protocolo muy básico sobre TCP, con tan sólo cinco tipos de paquetes distintos. Los mensajes son todos de tipo texto, con un primer carácter identificador del tipo de paquete, seguido de una longitud en algunos casos, a continuación el contenido del mensaje en sí mismo, y un par de caracteres de terminación.

El hecho de que sea un protocolo de tipo texto tiene la ventaja de que resulta fácil de implementar y depurar, pero tiene el inconveniente de que hay que convertir entre binario y texto continuamente, en particular las longitudes de los mensajes. En principio he optado por utilizar las facilidades del propio lenguaje para las conversiones, sin realizar ninguna medición del rendimiento, pero podría estudiarse la posibilidad de hacer una implementación más específica.

Por otra parte, debido a que los paquetes no contienen ningún tipo de identificador, es necesario guardar la secuencia de cada comando enviado de forma ordenada y casarla contra las respuestas recibidas en el mismo orden. Con el hándicap de que es posible deshabilitar las respuestas del servidor, por lo que en algunos casos los paquetes han de enviarse sin esperar respuesta. En determinadas circunstancias muy concretas el protocolo no impide que se produzcan pérdidas de sincronía entre cliente y servidor.

Conexión

La conexión al servidor se realiza siguiendo el típico patrón consistente en utilizar un método estático en la clase del cliente a modo de factoría.

final client = await Client.connect('redis://localhost:6379');
…
await client.disconnect();

Como parámetro se admite una cadena de conexión siguiendo el formato clásico de protocolo, host y puerto. Lo que posibilita una configuración muy sencilla y ampliable.
Redis permite trabajar con varias bases de datos dentro de una misma instancia, pero esta forma de trabajo no está totalmente soportada cuando se utiliza una instalación de Redis en cluster, por lo que es mejor no utilizar esta característica si no es absolutamente necesaria. Resulta más sencillo levantar más instancias.

Redis permite proteger de forma opcional con una clave los accesos al servidor, pero normalmente las instancias de Redis no se encuentran abiertas al mundo exterior, sino como dentro de una DMZ, por lo que no suele utilizarse, aunque ello depende en gran medida de las políticas de seguridad de cada cual.

Algunos puntos abiertos en la implementación de las conexiones con el servidor son la posibilidad de indicar un timeout, realizar reconexiones automáticas en caso de pérdida de conexión, y un pool de conexiones.

Comandos

Un cliente le comunica a un servidor Redis lo que quiere hacer a través de comandos. Y existen más de 200 comandos distintos. dartis implementa una interface genérica que permite ejecutar cualquier tipo de comando, a la vez que ofrece una interface más específica que expone todos los comandos ofrecidos por Redis de forma fuertemente tipada.

dartis funciona de forma totalmente asíncrona, por lo que todos los comandos retornan un Future que se completa cuando el servidor retorna el resultado.

Además, el conjunto de comandos está expuesto utilizando Generics, lo que permite que cada aplicación que utilice la librería pueda decidir el tipo concreto de datos que quiere utilizar, en vez de limitarse a utilizar String como hacen otras librerías.

final commands = client.asCommands<String, String>();

await commands.set(‘key, ‘value’);
final value = await commands.get(‘key’);

print(value);

Redis almacena secuencias de bytes, no cadenas de textos, por lo que puede utilizarse para gestionar cualquier tipo de información.

final commands = client.asCommands<String, List<int>();

await commands.set(‘key’, <int>[1, 2, 3]);

Los comandos están expuestos en forma de vista sobre el cliente, es decir, es la misma instancia pero exponiendo sólo una interface sobre la misma, por lo que pueden obtenerse distintas vistas del mismo cliente para trabajar con distintos tipos de datos.

final strings = client.asCommands<String, String>();
final bytes = client.asCommands<String, List<int>>();

String title = await strings.get('book:24902:title');
List<int> cover = await bytes.get('book:24902:cover');

Es seguro trabajar con varias vistas a un mismo tiempo, ya que todas utilizan el mismo cliente.

Pipelining

Redis permite enviar una al servidor una serie de comandos a un mismo tiempo, en vez de uno a uno. Aunque en la práctica esta es más una característica propia de las comunicaciones que de Redis. El cliente puede enviar más de un comando dentro de un mismo paquete TCP. Y el servidor puede procesar paquetes TCP que contengan más de un comando.
Por defecto dartis sólo envía un comando en cada paquete TCP, deshabilitando de forma explícita el algoritmo de Nagle sobre el socket, para garantizar que cada comando se envía de forma inmediata al servidor, pero permite activar el pipelining llamando al método pipeline:

client.pipeline();

commands.incr('product:9238:views').then(print);
commands.incr('product:1725:views').then(print);
commands.incr('product:4560:views').then(print);

client.flush();

En buena lógica, con este patrón no se puede utilizar await sobre cada comando, ya que el Future de cada comando no se completa hasta que se obtiene respuesta del servidor, y los comandos no se envían al servidor hasta que se vacía el pipeline con la llamada al método flush.

Otra opción es utilizar la lista de Futures retornada por el método flush para esperar hasta que se completen todos los comandos.

client.pipeline();

commands
..incr('product:9238:views')
..incr('product:1725:views')
..incr('product:4560:views');

final futures = client.flush();

await Future.wait<Object>(futures).then(print);

Esta forma de trabajo se utiliza sobre todo para realizar cargas masivas de información o clientes con casos de uso concretos que requieren un mayor rendimiento.

Fire And Forget

Redis permite que los clientes indiquen al servidor que no quieren recibir respuestas del servidor. Es decir, se pueden enviar comandos para su ejecución y desentenderse del resultado (fire and forget).

Esta es una característica que Redis ofrece a través del comando CLIENT REPLY. El comando necesita un parámetro que admite el valor OFF para deshabilitar todas las respuestas, SKIP para deshabilitar sólo la respuesta del comando siguiente, y ON para habilitar todas las respuestas.

En este modo los comandos se completan de forma inmediata con el valor null cuando las respuestas del servidor están deshabilitadas.

await commands.clientReply(ReplyMode.off);

await commands.ping().then(print); // null
await commands.ping().then(print); // null
await commands.ping().then(print); // null

La implementación de esta característica no es compleja, pero requiere que el cliente detecte de manera explícita que se está ejecutando el comando CLIENT REPLY, y llevar la cuenta de para que comandos se debe esperar respuesta y para cuáles no. Si el propio comando CLIENT REPLY es el que falla entonces puede llegar a perderse la sincronía entre servidor y cliente, aunque se presupone una casuística con una probabilidad muy baja de que suceda.

Dart 2.0.0 (y 2)

Después de comentar en el artículo anterior algunos de los hitos importantes en la historia de Dart, junto con las características y herramientas básicas del lenguaje, ahora toca repasar algunas de las novedades específicas más relevantes de la recién liberada versión 2.0.0.

  • El sistema de tipos ahora es más estricto que en la versión anterior. Utilizando una combinación de comprobaciones realizadas tanto en tiempo de compilación como en tiempo de ejecución se garantiza que el valor asignado a una variable o parámetro es del tipo declarado para los mismos, elevándose un error en caso contrario. Con este cambio Dart se aleja de su sistema de tipos inicial, más parecido al de JavaScript, que realiza conversiones de los valores de un tipo a otro de manera automática y sin indicación explícita por parte de los desarrolladores. El nuevo sistema de tipos permite desarrollar sistemas con comportamientos más predecibles y facilita la detección temprana de errores.
  • El tipo entero int tiene ahora un tamaño fijo predefinido de 64 bits. En la versión anterior el tipo no tenía un tamaño definido y las variables enteras podían tener cualquier valor, Dart reservaba más memoria para ellas a medida que su valor crecía. Ahora los enteros son de tamaño fijo y la funcionalidad anterior está en la clase BigInt. Tipo este último que se ha añadido de forma nativa a JavaScript hace relativamente poco tiempo para representar enteros con una precisión de tamaño arbitrario.
  • Se ha eliminado la necesidad de utilizar new y const para instanciar objetos. Su uso está marcado como deprecado, pero se sigue soportando. Este cambio equipara los constructores con las factorías, haciéndolos patrones intercambiables e indistinguibles a la hora de invocarlos. Además tiene la ventaja de que no es necesario utilizar const de forma repetitiva cuando en una sentencia se construye un objeto constante por composición de otros objetos constantes.
  • Las funciones marcadas con async se ejecutan ahora de forma síncrona hasta alcanzar la primera llamada a await dentro del cuerpo de dichas funciones. En la versión anterior la ejecución de las funciones asíncronas se realizaba en el siguiente ciclo de ejecución del event loop de Dart empezando desde el principio del cuerpo de dichas funciones. Con este cambio el comportamiento es más acorde a lo que la intuición sugiere y no se presupone la necesidad de conocer el detalle de funcionamiento interno de la máquina virtual para programar correctamente una función de manera asíncrona.
  • Las directivas part of ahora pueden utilizarse con una cadena de texto con la ruta de un fichero en vez del nombre de una librería. Esto permite crear librerías anónimas y facilita organizar los proyectos de forma interna con una mayor granularidad. La sintaxis original ha sido marcada como deprecada.
  • El tipo void se ha promocionado y ahora puede utilizarse como tipo parametrizado dentro de expresiones genéricas. Es decir, ahora por ejemplo puede escribirse Future<void>. Sin embargo, no puede utilizarse dentro de todas las expresiones, por lo que T is void, T == void o identical(T, void) no son expresiones válidas en tiempo de compilación. Mi impresión general es que este cambio se ha publicado sin estar completamente definido, y en consecuencia implementado y probado. Y si ese es el comportamiento definido por diseño entonces el cambio se me antoja un tanto extraño.
  • Cuando se invoca un método que no se encuentra implementado, como puede llegar a ocurrir en el caso de métodos de clases abstractas, Dart invoca a un método con un nombre prefijado llamado noSuchMethod. En la versión anterior de Dart el método no recibía ningún parámetro opcional de la función original no implementada. En la nueva versión recibe los parámetros opcionales con sus valores por defecto.
  • Se han dejado de soportar los paquetes dart:isolate  y dart:mirrors para la parte cliente web, aunque aún se siguen soportando para la parte servidora ejecutándose sobre la máquina virtual en línea de comandos. Con respecto a dart:isolate, se recomienda el uso de webworkers para ejecutar tareas en segundo plano en el navegador. Y con respecto a dart:mirrors, el mecanismo de reflexión siempre ha sido una patata caliente en Dart, y parece que con esta versión va a seguir siéndolo. Habrá que esperar a la siguiente.
  • Los nombres de constantes de todas las librerías se han renombrado para utilizar minúsculas en vez de mayúsculas. Escribir las constantes utilizando mayúsculas es una vieja práctica que se sigue utilizando dentro las guías de estilo de la mayoría de lenguajes de programación. El cambio es una propuesta de hace un tiempo y que se ha materializado en esta nueva versión de Dart. Su intención es que si una variable deja de ser una constante no debería implicar reescribir todo el código que hace referencia a ella.
  • Las librerías internas del core del SDK han sufrido muchos cambios. Se han añadido y modificado clases y métodos. Enumero a continuación algunos de estos cambios más representativos. En el paquete dart:async, a la clase Stream se le han añadido los métodos cast y castFrom para realizar comprobaciones en tiempo de ejecución de conversiones de objetos de un tipo a otro. En el paquete dart:collection, a la mayoría de las clases se les ha añadido el constructor of para facilitar la instanciación de colecciones a partir de iterables. En el paquete dart:convert, la mayoría de conversores retorna ahora Uint8List en vez de List<int>. En el paquete dart:core, a los iterables se les ha añadido el método whereType para facilitar el filtrado de secuencias heterogéneas de objetos. Y en el paquete dart:typed_data, se ha añadido una clase de vista inmutable para todos los tipos de listas existentes.
  • El cambio más relevante del formateador de código es que ahora formatea las expresiones interpoladas dentro de las cadenas de texto. Y que se le ha incorporado el parámetro –fix en línea de comandos para ayudar con la migración del código existente a la nueva versión de Dart. El nuevo parámetro hace que el formateador elimine automáticamente new y const al instanciar objetos, y sustituye : por = al asignar valores por defecto a los parámetros opcionales.
  • El analizador de código ahora sólo funciona en modo estricto y trata el directorio packages como cualquier otro directorio ordinario. Estas dos modificaciones reflejan dos cambios importantes realizados en la nueva versión de Dart. El cambio referido al modo estricto se refiere al nuevo sistema de tipos, y el cambio referido al directorio packages se refiere a que en la versión anterior el directorio era en realidad un enlace simbólico que se utilizaba internamente para la resolución de paquetes y requería un tratamiento especial.
  • El gestor de dependencias se ha reescrito y ahora dispone de un sistema de construcción y publicación más flexible que los antiguos transformers, que han dejado de ser soportados. Además, ahora al publicar un paquete se obliga a declarar la versión máxima de SDK soportada. A los paquetes existentes que no tienen declarada ninguna versión máxima se les aplica el valor <2.0.0 por defecto.
  • El compilador cruzado de Dart a JavaScript ahora genera código para la versión 2 de Dart, lo que implica que el código generado tiene un tamaño mayor que en la versión anterior debido al mayor número de comprobaciones que se realizan en el sistema de tipos. Para tratar de aliviar este efecto se le ha añadido el parámetro –omit-implicit-checks al compilador con objeto de eliminar dichas comprobaciones del código generado para los casos en que no consideren realmente necesarias en tiempo de ejecución. Otro cambio significativo es que la clase Promise de JavaScript ahora se expone con un mecanismo compatible con la clase Future de Dart.
  • Dartium, la versión de Chrome que incluía la máquina virtual de Dart embebida, ha dejado de desarrollarse y dársele soporte.

Además de todos estos puntos comentados, se han corregido muchos casos de uso poco habituales del sistema de tipos y de la ejecución asíncrona de métodos. La release notes correspondiente contiene un listado muy detallado y su lectura es obligada para obtener una visión más completa de todas las modificaciones introducidas.

Dart 2.0.0 (1)

Esta semana se publicó la versión 2.0.0 de Dart, un lenguaje de programación desarrollado por Google que a mí personalmente siempre me ha parecido muy productivo. Es un lenguaje sencillo pero potente que viene con “baterías incluidas”, a la manera de Java, es decir, con un conjunto de librerías fundamentales con las que empezar a trabajar de forma inmediata. ¿Necesitas una cola? Pues una línea para importar el paquete de colecciones y otra línea para instanciarla. ¿Necesitas un socket? Pues una línea para importar el paquete de entrada/salida y otra línea para instanciarlo. Y aunque lo mismo se puede decir de la mayoría de lenguajes de programación hoy en día, los cuales posiblemente dispongan de un ecosistema de librerías más rico, Dart tiene una facilidad de uso y rendimiento que hacen muy grata la experiencia de desarrollo.

La mayoría de características que se encuentran en versiones modernas de JavaScript, como async y await para la programación asíncrona, o yield para la construcción de generadores, por citar sólo un par de ejemplos, se incorporaron hace años de forma nativa a Dart. Para mí siempre ha sido un buen laboratorio donde probar los futuros añadidos que se harán a JavaScript en el futuro. Empecé a utilizarlo para mis proyectos personales hace ya siete años, desde las primeras versiones beta, tanto para hacer aplicaciones en la parte cliente como en la parte servidora, y nunca me he arrepentido del tiempo invertido.

Dart es un lenguaje de script que se ejecuta sobre una máquina virtual propia creada por Lars Bak y Kasper Lund, en base a su exitosa experiencia de desarrollo de la máquina virtual v8 de JavaScript, y que por cierto, hace poco dejaron Google para montar su propia startup. Los planes iniciales eran incorporar la máquina virtual de Dart a Chrome, pero finalmente esa idea se desestimó. Durante todo este tiempo Google ha mantenido el desarrollo de Dartium, una versión de Chrome que incorpora Dart de forma nativa, pero con la versión 2 han decidido abandonar totalmente dicho proyecto. Y esto es una cuestión importante, ya que el anuncio oficial en su día de que la máquina virtual de Dart no se incorporaría a Chrome fue un punto de inflexión fundamental para el lenguaje, muchos desarrolladores dejaron de ver Dart como un lenguaje con futuro y prefirieron seguir desarrollando en JavaScript o TypeScript. Dart entró entonces en una especie de limbo, pero Google decidió no dejar morir el lenguaje y utilizarlo en productos internos como AdSense y AdWords por ejemplo.

Dart ha vuelto a aparecer en los medios últimamente gracias a dos noticias que han supuesto un revulsivo para el lenguaje. Por una parte el anuncio de que Fuchsia, el nuevo sistema operativo que está desarrollando Google, utilizará Dart para el desarrollo de aplicaciones. Y por otra parte la popularidad de Flutter, un framework para desarrollar aplicaciones nativas en Android e iOS que utiliza Dart. La primera noticia todavía está por concretar, pero la segunda es un hecho. Aún recuerdo el vídeo de hace unos años donde se hizo la primera presentación del proyecto, donde un excitado Eric Seidel hablaba de un proyecto que entonces se llamaba Sky y más tarde se renombró como Flutter. Algunas de las características que más han llamado la atención de Flutter son que un mismo código base sirve tanto para Android como iOS, que las interfaces de usuario se desarrollan escribiéndolas mediante código en vez de plantillas, a la manera de Ext JS, que incorpora toda una galería de componentes utilizando los estilos de Material Design por defecto, y que sus herramientas junto con su filosofía de trabajo con Dart acortan significativamente el ciclo de desarrollo.

Dart se puede utilizar como lenguaje de script en el servidor o compilado a JavaScript en el cliente. Google ha anunciado que con la versión 2 del lenguaje se van a centrar en la parte cliente, objetivo que en realidad lleva intentando desde el principio, pero que no ha acabado de alcanzar. Incluso llegó a intentar cambiar el desarrollo de su popular framework Angular de JavaScript a Dart, pero al final lo dejó estar por la fría acogida que tuvo el anuncio y creó una línea de desarrollo en paralelo con AngularDart. Producto este último al que han vuelto a intentar dar más protagonismo de cara a conseguir que más equipos de desarrollo lo adopten y deje de ser sólo una herramienta de uso interno. Su demo de construcción de una PWA con un 100% de puntuación en los tests de Lighthouse es una buena declaración de intenciones.

Google creó junto con el lenguaje un IDE llamado Dart Editor basado en Eclipse y pensado específicamente para trabajar con Dart. Con los años dejaron de mantenerlo y hoy en día el IDE que recomiendan es WebStorm, aunque es posible trabajar con otros editores. Yo en particular utilizo Visual Studio Code con el plugin Dart Code.

En cuanto al lenguaje en sí mismo, Dart sigue el estándar ECMA-408. Similar en su sintaxis a los lenguajes imperativos clásicos como Java, pero incluyendo elementos de sus versiones más modernas, tomados en su mayoría del ámbito de la programación funcional, como por ejemplo las expresiones lambda, e incorporando algunos elementos de su propia cosecha, como por ejemplo los constructores con nombre o la invocación de métodos en cascada. Inicialmente débilmente tipado, Dart ha ido evolucionando hacia un sistema de tipado más fuerte, aunque conservando aspectos tales como la inferencia automática de tipos cuando estos se excluyen de las declaraciones. En Dart todos los objetos son instancias de clases, incluso los tipos primitivos ofrecidos de forma nativa, como los números enteros por ejemplo. Como todo lenguaje en continua evolución, aún tiene características que se encuentran en otros lenguajes que serían deseables incorporar al mismo, y algunas lagunas, como un sistema de reflexión, pero en general el balance es bastante positivo.

Dart incorpora de forma nativa las herramientas básicas necesarias para un desarrollo moderno. En particular un formateador de código, un analizador de código estático, y un gestor de dependencias.

El formateador de código se llama dartfmt y es algo particular, porque no se puede configurar, por lo que a veces el resultado no es exactamente el deseado, pero es algo con lo que se puede vivir. La justificación de esta decisión de diseño es que se pretende que todo el código escrito en Dart tenga el mismo aspecto independientemente de su origen. A escala mundial. Una visión que sólo puede tenerse en una organización global como Google. Hace algún tiempo leí un artículo del autor, Bob Nystrom, sobre la vasta cantidad de permutaciones que pueden generarse a partir de un mismo código, atendiendo sólo a razones de formateado del mismo, y el reto que supone elegir una de dichas permutaciones de forma programática.

El analizador de código estático se llama dartanalyzer y es la base sobre la que se ha construido un linter. Esta herramienta si es parametrizable y existen decenas de reglas que pueden activarse según las necesidades particulares de cada proyecto. No obstante, es importante tener en cuenta que algunas reglas se han definido en base a la guía de estilo específica de Flutter en vez de a la guía de estilo más general de Dart. Esto quiere decir que cuando salta una regla hay que comprobar si se trata de una violación del estilo general recomendado por Dart o del estilo más específico de Flutter. Por ejemplo, Flutter recomienda que se utilice siempre código fuertemente tipado, mientras que Dart recomienda que se utilice la inferencia de tipos para las variables locales. La regla del linter por defecto sigue las recomendaciones de Flutter, por lo que si se define una variable local sin indicar de manera explícita su tipo se produce una violación de la regla. Y si se desactiva la regla entonces el linter deja de realizar la validación para todo el código, incluso en las interfaces públicas, donde si es conveniente utilizar declaraciones fuertemente tipadas. Es de suponer que estas reglas se irán refinando con el tiempo.

El gestor de dependencias es una herramienta llamada pub que existe desde las primeras versiones de Dart. Su funcionamiento es similar a herramientas similares basadas en publicar y descargar paquetes en base a su versión, como Maven en Java o npm en node.js. El diseño de la web del gestor de paquetes ha ido evolucionando a lo largo de los años. En la actualidad no es sólo un mero listado de paquetes disponibles, sino que ofrece resúmenes acerca de cada paquete con indicadores acerca de su popularidad o volumen de actualizaciones. Para forzar la migración de los paquetes existentes a versiones más recientes de Dart la propia web realiza de forma automática comprobaciones compilando el código y verificando si cumplen con las reglas de estilo. El resultado de dichas comprobaciones se publica en la propia web, por lo que es sencillo hacerse una idea del estado de un paquete y la dificultad que puede entrañar realizar los cambios necesarios para actualizarlo a una nueva versión de Dart.

Para Dart 2.0.0 se ha creado además un sistema de herramientas para facilitar el desarrollo, construcción y empaquetado, a la manera de gulp y similares en JavaScript. Lo que ofrece este sistema son las facilidades habituales para la compilación, monitorización de cambios y ejecución de pruebas. Incluyendo el uso de servicios de integración continua, como Travis CI por ejemplo, y permitiendo la creación de plugins que pueden ejecutarse durante las distintas fases del proceso de construcción de una aplicación.

Resumiendo, Dart es un lenguaje por el que Google sigue apostando, a pesar de no ser muy utilizado o conocido por el gran público, y al que se le está dotando de las herramientas necesarias para que resulte más atractivo a los desarrolladores. Aún así, su supervivencia depende de la aceptación de Flutter, un framework que curiosamente está cobrando más relevancia que el propio lenguaje en sí mismo, y la incógnita en el horizonte del futuro Fuchsia. Con Kevin Moore como Product Manager, en vez de Seth Ladd, que siempre me ha parecido tener un perfil más político, y que de hecho se ha subido a la estela del exitoso Flutter, junto con la salida de Lars Bak y Kasper Lund, queda por ver el rumbo que seguirá tomando el lenguaje.

dart-challenges (2)

Sigo trabajando en completar todos los retos de programación de www.codingame.com.

Hace unos días terminé todos los de nivel 3 y dejé todo el código subido a dart-challenges, aprovechando además para añadir un montón de comentarios en todas las soluciones de nivel 1 y 2, que ya había subido anteriormente, además de pasarlas por el formateador de código de Dart Editor.

En total llevo casi 40 problemas resueltos. Y de todo tipo, que es lo más interesante. Bastante de ellos recursivos, pero siempre con algún detalle que se aparta del algoritmo clásico y que te obliga a pensar la estrategia adecuada en cada caso particular.

Mi siguiente objetivo son los retos de nivel 4. ¡Ya sólo me quedan cinco!