Haciendo de Dart un mejor lenguaje para UI

En el equipo de Dart, estamos ocupados implementando un puñado de cambios del lenguaje que me emocionan mucho. Todos se relacionan con colecciones…

Primer plano de código de UI de Dart mostrando un método Widget build.

En el equipo de Dart, estamos ocupados implementando un puñado de cambios en el lenguaje que me emocionan mucho. Todos están relacionados con los literales de colección, la sintaxis integrada para crear listas, mapas y conjuntos:

dart
var someList = [1, 2, 3, 4];
var someMap = {key: 1, another: 2};
var someSet = {1, 2, 3, 4};

Si no escribes código Dart hoy, esto puede no ser muy relevante para ti y tus metas vitales, pero espero que sigas leyendo de todos modos. Creo que las funcionalidades son interesantes por sí mismas, y el modelo de ejecución que las sustenta podría estirar tu cerebro de formas útiles y/o interesantes. Siempre me resulta divertido aprender sobre cosas nuevas de lenguajes, incluso en lenguajes que no uso actualmente.

Cómo los usuarios de Flutter construyen su UI

#

Si escuchaste algo sobre Dart en el último año, probablemente fue en el contexto de Flutter. Si ese nombre no te suena, Flutter es un framework de UI para construir aplicaciones móviles multiplataforma. No puedo hacerle justicia aquí, pero haz clic en el enlace y responderá cada pregunta en tu corazón. (Bueno, al menos cada pregunta sobre Flutter. No te dirá por qué ese amor del instituto nunca te devolvió la llamada.)

Una decisión clave que toma cualquier framework de UI es cómo se definen los elementos visuales básicos de la UI — botones, colores, texto, layout, etc. ¿Los creas en algún tipo de formato separado de “plantilla” o “markup” o directamente en el código ejecutable donde se define el comportamiento de la UI? Cada quince años más o menos, la industria cambia sobre cuál es la respuesta correcta.

Angular y la mayoría de los frameworks web siguen los pasos de HTML y usan plantillas. React pone la UI dentro de tu JavaScript, pero también añade un DSL embebido llamado JSX para que se parezca a HTML. Intentando tener la tortilla y comérsela, supongo, aunque no todos describirían HTML como particularmente parecido a un postre.

Flutter pone la UI directamente en tu código Dart, usando la sintaxis normal de expresiones de Dart. Contempla:

dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      home: Scaffold(
        appBar: AppBar(title: Text('Welcome to Flutter')),
        body: Column(
          children: [
            Text('Hello World'),
            Text('This is Flutter'),
            Text('Written in Dart')
          ]
        )
      )
    );
  }
}

Todo después de esa palabra clave return es una gran expresión anidada que produce un fragmento de interfaz de usuario. Usar Dart para esto tiene un puñado de beneficios materiales:

  • Solo hay un lenguaje que aprender: Dart. Como Dart fue diseñado para ser familiar para personas que vienen de otros lenguajes, esperemos que no sea demasiado difícil.

  • Puedes usar todas las funciones de abstracción de un lenguaje de programación de propósito general al construir tu UI. Extrae piezas hacia funciones reutilizables. Dale a esas funciones parámetros para variar la UI generada. Guarda cosas en variables locales. Haz lo que quieras.

  • Nunca chocas contra un muro de expresividad y tienes que portar a un lenguaje diferente. Si alguna vez usaste un lenguaje declarativo, probablemente te encontraste con la situación en la que llegas al límite de lo que realmente puede expresar. En ese punto, o abandonas lo que intentabas hacer, o reescribes laboriosamente todo en un lenguaje de nivel más bajo, generalmente imperativo. Como ya estás en un lenguaje con todas las funciones con Dart, nunca chocas contra ese muro y tu código de UI crece suavemente en sofisticación.

El desafío principal, por supuesto, y la razón por la que la gente crea lenguajes declarativos en primer lugar, es que definir cosas en un lenguaje imperativo puede ser realmente tedioso y difícil de leer.

Imagina que en lugar de este pequeño fragmento de HTML:

html
<p>I am an <strong>exciting</strong> paragraph!</p>

Tuvieras que escribir algo como:

java
doc.beginTag("p");
doc.appendText("I am an ");
doc.beginTag("strong");
doc.appendText("exciting");
doc.endTag("strong");
doc.appendText("paragraph!");
doc.endTag("p");

Afortunadamente, los lenguajes modernos y las APIs no son tan de bajo nivel. Aunque las sentencias son imperativas, las expresiones son bastante declarativas. Mientras que el código anterior es enredado, este está más o menos al nivel de HTML:

java
p("I am an ", strong("exciting"), " paragraph!")

El paradigma reactivo moderno donde “construyes” tu UI construyéndola desde cero como una sola expresión te lleva bastante lejos. La parte relevante del ejemplo de Flutter de allá arriba es solo:

dart
MaterialApp(
  title: 'Welcome to Flutter',
  home: Scaffold(
    appBar: AppBar(title: Text('Welcome to Flutter')),
    body: Column(
      children: [
        Text('Hello World'),
        Text('This is Flutter'),
        Text('Written in Dart')
      ]
    )
  )
)

Tiene paréntesis y corchetes en lugar de corchetes angulares, pero por lo demás no está demasiado lejos de un lenguaje de “markup”. Es sorprendente lo bien que funciona esto. La sintaxis de Dart está basada en JavaScript, que la tomó de Java, que la tomó de C. En el camino, añadimos la sintaxis de literal de lista con corchetes y los parámetros nombrados, pero esos son cambios bastante menores.

C fue diseñado para implementar sistemas operativos de línea de comandos en el PDP-11. El hecho de que su notación escale no tan mal para construir UIs gráficas en dispositivos móviles es sea un testimonio del gusto de diseño de Ritchie, o de nuestro Síndrome de Estocolmo colectivo alrededor de la sintaxis de C. De cualquier manera, funciona… más o menos.

En el ejemplo aquí, no se requiere lógica interesante en tiempo de ejecución para construir la UI. Todo encaja bien en una sola expresión anidada. Pero digamos que, por la razón que sea, no quieres mostrar la parte “This is Flutter” del texto los martes. (Quizás necesitas hacer espacio en la pantalla para el banner ¡Taco Tuesday!)

Hay algunas formas de expresar eso, pero ninguna se siente tan agradable y declarativa como el ejemplo anterior. Aquí hay una:

dart
Widget build(BuildContext context) {
  var texts = [Text('Hello World')];
  if (!isTuesday) {
    texts.add(Text('This is Flutter'));
  }
  texts.add(Text('Written in Dart'));

  return MaterialApp(
    title: 'Welcome to Flutter',
    home: Scaffold(
      appBar: AppBar(title: Text('Welcome to Flutter')),
      body: Column(children: texts)
    )
  );
}

Estamos mucho más cerca del desagradable código imperativo de bajo nivel que lleva a la gente a usar plantillas. Cuando miramos código real de Flutter, nos entristeció ver mucho que se parecía a esto. Así que, hace aproximadamente un año, el equipo de Flutter nos pidió en Dart que creáramos cambios en el lenguaje para hacer el código de UI escrito en Dart más fácil de escribir, leer y mantener.

“UI as Code”

#

Llamamos a esta iniciativa “UI as code”, ya que se trata de construir tu UI usando código. Pero el objetivo final son funcionalidades del lenguaje que sean generalmente aplicables a tantos programas de Dart como sea posible, con Flutter o sin él. (Si quieres mucho más contexto, aquí está un largo documento de motivación que escribí.)

Después de explorar un montón de opciones, decidimos centrarnos en algunas mejoras específicas alrededor de los literales de colección. Esto puede no parecer tan atractivo como meter algo como JSX en Dart (no es que descarte eso por completo), pero tiene la ventaja de ser mucho más fácil para los usuarios aprovecharlo incrementalmente en su código.

Solo hacer los literales de lista más atractivos puede parecer tener, eh, un impacto limitado. Pero, si miras código de UI de Flutter como el anterior, es básicamente un gran árbol de llamadas a constructores y literales de lista. Los literales de lista son una gran fracción del territorio. (Vaya, lenguajes enteros se han diseñado alrededor de ellos.) Si indagas en ejemplos donde parece que deberías poder escribir algo declarativamente pero en su lugar tienes que hacer un montón de mutación imperativa desagradable, muy a menudo es alrededor de listas.

Si podemos hacer las colecciones mejores, podemos hacer mucho código de Dart mejor. Con ese fin, estamos añadiendo tres nuevas funcionalidades:

Spreads

#

A menudo, cuando construyes una lista de widgets, algunos de esos widgets ya están en alguna otra lista. Aquí hay un poco de código de Flutter:

dart
Widget build(BuildContext context) {
  var conversation = buildTab2Conversation();
  var listItems = [Tab2Header()];
  listItems.addAll(conversation);
  listItems.add(buildFooter());

  return CupertinoPageScaffold(
    child: ListView(children: conversation),
  );
}

El método buildTab2Conversation() devuelve una lista de widgets que queremos rodear con la cabecera y el pie. Tener que construir la lista resultante imperativamente es un verdadero fastidio. Fuerza al código a leerse “al revés” donde ves un montón de cosas revolviendo los hijos antes de llegar al código para ver de qué son hijos de.

Dart tiene una funcionalidad llamada cascadas de métodos que ayuda hasta cierto punto. Estas te permiten meter una llamada a un método mutante en medio de una expresión mientras devuelves el objeto original. Con eso, obtienes:

dart
Widget build(BuildContext context) {
  return CupertinoPageScaffold(
    child: ListView(children: [
      Tab2Header()
    ]..addAll(buildTab2Conversation())
      ..add(buildFooter())),
  );
}

Eso es algo mejor, pero todavía es bastante torpe. Ese ..add() al final para añadir un solo elemento es particularmente escandaloso. Probablemente puedes adivinar cómo lo arreglamos ya que varios otros lenguajes ya tienen la misma solución. (>90% del diseño de lenguajes es averiguar qué funcionalidades tomar prestadas de otros lenguajes.) Estamos añadiendo una nueva sintaxis llamada spreads.

Dentro de un literal de colección, un spread desempaqueta otra colección e inserta sus contenidos directamente en su lugar. Por ejemplo:

dart
var a = [2, 3];
var b = [5, 6];
var c = [1, ...a, 4, ...b]; // [1, 2, 3, 4, 5, 6].

El ... antes del elemento de la lista hace que sus elementos se interpolen en la lista circundante. Esta es la misma sintaxis que usa JavaScript. Python, Ruby, y algunos otros usan un prefijo * para lo mismo, pero nos pareció que no destacaba lo suficiente visualmente. Con esta funcionalidad, el ejemplo de Flutter se convierte en:

dart
Widget build(BuildContext context) {
  return CupertinoPageScaffold(
    child: ListView(children: [
      Tab2Header(),
      ...buildTab2Conversation(),
      buildFooter()
    ]),
  );
}

Creo que esto es una mejora real. Todos los hijos de la list view están anidados confortablemente en el único literal de lista. Se ve mejor, y también funciona mejor con la inferencia de tipos. Con todos los elementos dentro de la lista, podemos usarlos todos al inferir el tipo de la lista.

Estoy mostrando un ejemplo de Flutter aquí, pero pasé mucho tiempo revisando un enorme corpus de código Dart para ver dónde sería útil esta sintaxis y aparece por todas partes. En particular, el código que construye listas de argumentos de línea de comandos para invocar otros programas realmente se beneficia de los spreads.

Elementos

#

Antes de entrar en las dos últimas funcionalidades, quiero profundizar en qué es realmente un spread is. Parecerá que estoy insistiendo demasiado en el punto, pero prometo que ser claro sobre esto será útil más adelante. Aquí hay una pregunta provocadora: ¿es un spread una expresión?

Parece uno, porque aparece en un literal de lista en un lugar donde se espera una expresión:

dart
var stuff = [1, ...things, 4];

Como una expresión, la evalúas y produce algunos datos. ¿Quizás es una expresión que evalúa a un objeto Iterable? Pero, espera, eso no tiene sentido. Eso es lo que hace la expresión dentro del spread. Si solo quieres una expresión que evalúe a un Iterable, no hay necesidad de poner el ... antes.

Un spread no evalúa a un solo objeto Iterable, desempaqueta ese objeto y evalúa a la serie de objetos producidos por el Iterable. No sería útil entonces reempaquetar eso en algún objeto nuevo. Pero una expresión siempre evalúa a un único objeto.

Si un spread fuera una expresión, ¿qué significaría usar uno en otros lugares donde se permite una expresión?

dart
var wat = ...things;

¿Qué haría esto? No tiene sentido almacenar el Iterable completo como un objeto en wat. Si quisieras eso, podrías simplemente omitir el ... completamente. La respuesta es que los spreads no son expresiones. Son una categoría sintáctica diferente. Dart, como muchos lenguajes, ya tiene dos grandes grupos de sintaxis: sentencias y expresiones.

Las sentencias se ejecutan pero no producen ningún valor de resultado. En su lugar, se espera que tengan algún efecto secundario útil. No pueden usarse en ningún contexto donde se necesita un valor porque no te dará uno. Por eso, digamos, esto está prohibido:

dart
var wat = for (var i = 0; i < 10; i++) print(i);

Una sentencia for no produce un valor, así que no tiene sentido meter una en un inicializador de variable. Sí hay lenguajes que unifican expresiones y sentencias y permiten código como este. Definen cada sentencia para que se ejecute de alguna manera y también produzca un valor. Pero Dart no es uno de esos lenguajes.

Las expresiones evalúan a un único valor de resultado. Puedes usarlas en lugares donde un valor es útil. También hay “sentencias de expresión” — una expresión seguida de un punto y coma — que son sentencias que contienen una sola expresión. Práctico ya que muchas expresiones también tienen efectos secundarios y son útiles incluso cuando su resultado no se necesita.

Un spread no es ninguna de esas. Un spread puede evaluar a cero valores (si esparces una colección vacía), un valor, o muchos. Es su propio tipo de cosa. Un buen nombre para esta categoría sería “generador”. Mi modelo para esto viene de Icon donde cada expresión puede ser un generador. Pero Dart ya tiene funciones generadoras así que no quería sobrecargar el término.

Un spread solo puede aparecer en un lugar que pueda manejar elegantemente recibir cero o más valores. Sin reconstruir completamente el modelo de ejecución del lenguaje y convertirlo en Icon (lo cual me resulta extrañamente atractivo, pero probablemente no práctico…), no hay muchos lugares que cumplan esa restricción. Básicamente literales de colección y quizás listas de argumentos posicionales. (Escribí una propuesta para esto último, pero es bastante complejo así que no lo estamos haciendo, al menos no ahora mismo.)

Eso deja dentro del cuerpo de los literales de colección. Basándome en eso, los llamo “elementos”. Un elemento es un fragmento de código que al evaluarse produce cero o más valores. Esos valores luego se interpolan en su lugar en el contexto circundante donde aparecen. Así, en una lista, se convierten en una serie de elementos en la nueva lista. En un mapa, se convierten en una serie de pares clave/valor. Ya te haces una idea.

El cuerpo de un literal de colección puede así contener expresiones o elementos. Permitir dos categorías es un poco confuso, pero afortunadamente podemos simplificar esto diciendo que una colección solo contiene elementos. Entonces definimos un “elemento de expresión” como un elemento que contiene una sola expresión. El elemento siempre produce un resultado — el valor de la expresión. Algo así como el equivalente de elemento de una sentencia de expresión.

Bien, entonces ahí es donde estamos. Hemos cambiado las colecciones para que contengan elementos en lugar de expresiones y hemos definido dos tipos de elementos, spreads y elementos de expresión. Para evaluar un literal de colección, recorres los elementos, evalús cada uno, y concatenas (o unes en el caso de un conjunto) todos los objetos resultantes.

Con ese modelo en mente, podemos recorrer las otras dos nuevas funcionalidades:

Collection If

#

Escribe cualquier código de Flutter y bastante rápido te encuentras con casos donde el árbol de widgets que quieres construir cambia según alguna condición. Digamos que tenemos:

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      IconButton(icon: Icon(Icons.menu)),
      Expanded(child: title),
      IconButton(icon: Icon(Icons.search)),
    ],
  );
}

Más tarde, decidimos que queremos usar un botón de búsqueda diferente en Android. Ya puedes hacer eso usando una expresión condicional:

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      IconButton(icon: Icon(Icons.menu)),
      Expanded(child: title),
      isAndroid
          ? TextButton("Search")
          : IconButton(icon: Icon(Icons.search)),
    ],
  );
}

Esto funciona bien, aunque nunca he encontrado que el operador condicional de C sea muy fácil de leer. A menudo, sin embargo, no quieres intercambiar un widget según una condición, quieres simplemente omitir uno. Digamos que no quieres mostrar un cuadro de búsqueda en Android en absoluto. Hoy en día, los usuarios de Flutter tienden a usar uno de dos patrones. Este es uno:

dart
Widget build(BuildContext context) {
  var children = [
    IconButton(icon: Icon(Icons.menu)),
    Expanded(child: title)
  ];

  if (!isAndroid) {
    children.add(IconButton(icon: Icon(Icons.search)));
  }

  return Row(children: children);
}

Te fuerza a reorganizar toda la función extrayendo la lista de hijos y construyéndola imperativamente antes de usarla. El otro patrón se ve así:

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      IconButton(icon: Icon(Icons.menu)),
      Expanded(child: title),
      !isAndroid ? IconButton(icon: Icon(Icons.search)) : null,
    ].where((widget) => widget != null).toList(),
  );
}

Esto usa una expresión condicional que a veces produce un null y luego filtra ese null de la lista resultante. Mérito para quien se le ocurrió esto, pero no es algo que ningún usuario deba tener que escribir para lograr una tarea tan simple. Los problemas simples deberían tener soluciones simples, y los pequeños cambios conceptuales en tu programa no deberían requerir grandes cambios textuales.

Aquí está lo nuevo. Para el primer ejemplo donde queremos un botón diferente en Android, se ve así:

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      IconButton(icon: Icon(Icons.menu)),
      Expanded(child: title),
      if (isAndroid)
        TextButton("Search")
      else
        IconButton(icon: Icon(Icons.search)),
    ],
  );
}

En lugar de ?:, usa la familiar sintaxis de if y else. Esto es literalmente solo unos cuantos tokens diferente de la expresión condicional existente, así que no parece justificar su peso. El caso más interesante es cuando queremos omitir el botón en Android:

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      IconButton(icon: Icon(Icons.menu)),
      Expanded(child: title),
      if (!isAndroid)
        IconButton(icon: Icon(Icons.search)),
    ],
  );
}

Nota que no hay cláusula else. Ambos ejemplos se parecen bastante a lenguajes como Ruby donde if es una expresión. Pero una expresión siempre debe evaluar a un valor, incluso cuando la condición es falsa. En Ruby, en ese caso, evalúa implícitamente a nil.

Pero eso no es lo que quieres aquí. No quieres terminar con un elemento null en tu lista de widgets hijos. Por eso el ejemplo de expresión condicional de allá arriba tenía que usar el molesto where() para filtrarlo. Afortunadamente, eso no es un problema aquí. Porque el if dentro de una colección no es una expresión. Es un elemento.

Ahora ves por qué te arrastré por todo ese asunto de los spreads. Los elementos nos dan la base para tener una sintaxis de if que te permite omitir completamente un elemento de una colección. El elemento if produce un solo valor si la condición es verdadera, o si hay un caso else. Si la condición es falsa y no hay cláusula else, simplemente no produce ningún valor.

Creo que este comportamiento es realmente útil, pero también es confuso si miras el código y esperas que if actíe como una simple expresión.

Collection For

#

La funcionalidad anterior toma una sintaxis de sentencia de Dart existente y la repurpose para hacer algo útil en el contexto de una colección. ¿Hay alguna otra forma de sentencia que valga la pena tomar?

La mayoría no tienen sentido. Poner una sentencia return ahí no haría nada útil ya que simplemente saldría de la función circundante. while tampoco es muy útil. Para salir de un bucle while, el cuerpo típicamente contiene un break, return, o algún tipo de efecto secundario como una asignación. Pero eso implica que el cuerpo contiene sentencias, que no es lo que queremos. El objetivo es hacer las colecciones más expresivas, no más imperativas.

Revisé un montón de literales de colección en código existente buscando patrones que pensé que podrían mejorarse con nueva sintaxis. El principal, con diferencia, fue if. Pero vi varios lugares que pensé que podrían mejorarse con for. Aquí hay un ejemplo ligeramente limpiado de algo de código que encontré:

dart
var command = [
  engineDartPath,
  frontendServer,
];
for (var root in fileSystemRoots) {
  command.add("--filesystem-root=$root");
}
for (var entryPoint in entryPoints) {
  command.add("$entryPoint.json");
}
command.add(mainPath);

Todo lo del command.add() se siente innecesariamente imperativo. Si permitimos bucles for dentro de un literal de colección, esto se convierte en:

dart
var command = [
  engineDartPath,
  frontendServer,
  for (var root in fileSystemRoots) "--filesystem-root=$root",
  for (var entryPoint in entryPoints) "lib/$entryPoint",
  mainPath
];

Componiendo elementos

#

Dado que ya estamos añadiendo spreads, la sintaxis de for no parece muy convincente. ¿No podrías lograr lo mismo usando spread con alguna combinación de métodos de orden superior en iterables? Sí, puedes. Obtendrías algo como:

dart
var command = [
  engineDartPath,
  frontendServer,
  ...fileSystemRoots.map((root) => "--filesystem-root=$root"),
  ...entryPoints.map((entryPoint) => "lib/$entryPoint"),
  mainPath
];

Esto funciona y está bien para algunos casos de uso. Consideremos un ejemplo ligeramente diferente. Digamos que solo queremos incluir un punto de entrada si existe un archivo JSON correspondiente. Eso significa que no estamos haciendo un mapeo simple 1–1. Usando solo spreads, obtenemos algo como:

dart
var command = [
  engineDartPath,
  frontendServer,
  ...fileSystemRoots.map((root) => "--filesystem-root=$root"),
  ...entryPoints
      .where((entryPoint) => fileExists("lib/$entryPoint.json"))
      .map((entryPoint) => "lib/$entryPoint"),
  mainPath
];

Esto también funciona. Pero empieza a ser cada vez más difícil traducir la simple lógica de “si el archivo existe, haz esto” a un estilo funcional de orden superior basado en streams. Siempre hay alguna combinación de map(), where() y quizás transform() que hace el trabajo, pero puede sentirse como traducir un haiku a Notación Polaca Inversa.

Hay una solución más limpia, e implica una pregunta clave: ¿Cuál es el cuerpo de estos nuevos elementos if y for? En los ejemplos que te he mostrado hasta ahora, siempre es una expresión. Pero no hay necesidad de restringirlo solo a eso. En su lugar, permitimos que cualquier elemento vaya ahí. En otras palabras, las tres nuevas funcionalidades se pueden componer libremente. El código anterior se puede expresar así:

dart
var command = [
  engineDartPath,
  frontendServer,
  for (var root in fileSystemRoots) "--filesystem-root=$root",
  for (var entryPoint in entryPoints)
    if (fileExists("lib/$entryPoint.json")) "lib/$entryPoint",
  mainPath
];

Un simple if dentro de un for, tal como lo harías si estuvieras escribiendo sentencias imperativas. La semántica para componer elementos resulta ser bastante obvia:

  • Un elemento if produce todos los valores producidos por su cláusula then si la condición es verdadera, de lo contrario produce todos los elementos de la cláusula “else”. Si no hay “else”, no produce ningún elemento.

  • Un elemento for produce la concatenación de todos los valores producidos por su elemento cuerpo cada vez que el cuerpo se ejecuta.

Esto habilita algunos patrones que creo que son geniales. Un problema obvio que encuentras es querer incluir u omitir múltiples valores según una sola condición. Así que, digamos en nuestro ejemplo anterior queríamos saltar tanto el título como el cuadro de búsqueda en Android. Puedes hacer eso envolviendo un spread en un if:

dart
Widget build(BuildContext context) {
  return Row(
    children: [
      IconButton(icon: Icon(Icons.menu)),
      if (!isAndroid) ...[
        Expanded(child: title),
        IconButton(icon: Icon(Icons.search)),
      ]
    ],
  );
}

El spread es necesario aquí para desempaquetar la lista interna. De lo contrario, cuando no estés en Android, incluirías la lista interna completa como un solo valor. (Consideramos aplanar implícitamente en casos como este basándonos en tipos estáticos, pero eso se vuelve realmente dudoso cuando piensas en cómo deberían comportarse cosas como un List<Object>.)

Puedes pensar en esparcir un literal de lista como el equivalente de elemento de un bloque con llaves para sentencias — te permite poner múltiples elementos en un lugar donde solo se espera uno. (Si estás familiarizado con el operador coma, esa es esencialmente la forma análoga para expresiones. Paralelismos por todas partes.)

Usar for e if en un literal de colección vacío te da una sintaxis no muy lejana de la sintaxis especial de “list comprehension” que soportan otros lenguajes como Python:

dart
[for (var i = 1; i <= 5; i++) if (i.isOdd) i * i] // [1, 9, 25].

Incluso puedes anidar for:

dart
[for (var x in hor) for (var y in vert) Point(x, y)]

Esto construye una lista que contiene el producto cartesiano de todos los puntos en el rectángulo dado por hor y vert.

Además, estas nuevas funcionalidades se componen a través de tipos de colección. He estado usando listas como ejemplo porque aparecen más a menudo, pero todas estas funcionalidades también funcionan dentro de mapas y conjuntos. La única diferencia es que con los conjuntos, los duplicados se descartan implícitamente. Y en los mapas, en lugar de tener elementos de expresión primitivos, el elemento básico es un par clave/valor. Por ejemplo, esto:

dart
Map<String, WidgetBuilder>.fromIterable(
  allGalleryDemos.where((demo) => demo.exists),
  key: (demo) => '${demo.routeName}',
  value: (demo) => demo.buildRoute,
);

Se puede reescribir como:

dart
{
  for (var demo in allGalleryDemos)
    if (demo.exists) '${demo.routeName}': demo.buildRoute,
};

Una preocupación real que tenemos con todas estas funcionalidades es que esencialmente te estamos dando nuevas formas de expresar cosas que ya puedes expresar hoy. Esto tiene un costo porque significa que los usuarios necesitan gastar energía mental decidiendo qué funcionalidad usar, y al leer código de otros, pueden pasar tiempo cuestionando por qué se eligió una opción sobre la otra. Simplemente hay más funcionalidades que aprender y el lenguaje es más grande.

Pasamos mucho tiempo agonizando sobre esto. A veces, simplemente no hacer nada es el mejor diseño. La simplicidad tiene mucho valor, y es raro que tengas la oportunidad de hacer un lenguaje más simple con el tiempo. Sin embargo, después de revisar mucho código y trabajar con un encantador investigador de UX en un estudio, estamos bastante seguros de que estas funcionalidades son lo suficientemente ligeras y útiles como para justificar su peso.

Como con cualquier cambio de lenguaje, nunca sabes realmente cómo funcionará hasta que llega a las manos de los usuarios. Estas funcionalidades deberían llegar a tus manos en la próxima versión de Dart 2.3, y estoy muy emocionado de ver qué haces con ellas.

Más de Dart