Decisión de sintaxis de nullability en Dart: a?[b] o a?.[b]

Dart está en el proceso de rediseñar su sistema de tipos para que los tipos individuales sean nullable (las expresiones de ese tipo pueden tener…

Actualización (3/2021): Gracias a tus comentarios, el equipo de Dart decidió usar la sintaxis a?[b].

Dart está en proceso de rediseñar su sistema de tipos para que los tipos individuales sean nullable (las expresiones de ese tipo pueden tener el valor null) o non-nullable. Más adelante este año te contaremos más sobre el cronograma y el proceso de despliegue, pero eventualmente el código Dart será non-nullable por defecto (NNBD), y tendrás que usar una sintaxis especial para indicar que un tipo permite valores null.

Por ejemplo, para declarar que un entero puede ser null, necesitarás añadir un ? después del tipo:

int? someInt; // someInt can be null.

Esa sintaxis de signo de interrogación puede resultarte familiar si has visto código de Kotlin, Swift o C#. Pero algunas cosas son diferentes — notablemente el operador de subíndice ([]), que sueles ver usado para acceso a listas o arrays. C# y Swift usan ?[]. El plan actual para Dart (y, por cierto, para ECMAScript) es usar ?.[] en su lugar:

e1?.[e2] // null if e1 is null; otherwise it’s e1[e2]

Este artículo te lleva detrás de escena para explicar el razonamiento detrás de esta decisión, y para animarte a compartir tus opiniones y sugerencias. La mayor parte de este contenido está basado en un comentario de Bob Nystrom (munificent) en el issue del lenguaje #376, que resume una discusión entre Bob y el dueño de la especificación NNBD, Leaf Petersen (leafpetersen).

¿Por qué usar un punto?

#

Ambas opciones para [] tienen ventajas.

e1?[e2]:

  • Sigue a C# y Swift

  • Es conciso

  • Es similar a la sintaxis del operador ! (que podrás añadir después de una expresión para indicar que su valor no es null, aunque tenga un tipo nullable): e1![e2]

e1?.[e2]:

  • Es similar a la sintaxis de cascada: e1..[e2] // sintaxis de cascada e1?..[e2] // sintaxis de cascada null-aware

  • Es similar a otra sintaxis de métodos null-aware: e1?.e2()

  • Se extiende naturalmente a otros operadores

  • Evita la ambigüedad en el siguiente código: { e1 ? [e2] : e3 }

Pasamos un rato intentando encontrar formas de evitar la ambigüedad de ?[. El problema es que no se puede saber si un código como { e1 ? [e2] : e3 } es un set literal que contiene el resultado de una expresión condicional, o un map literal que contiene el resultado de un subíndice null-aware.

Si añadimos paréntesis para que ese código sea inequívoco, podríamos elegir añadirlos alrededor de toda la expresión — { (e1 ? [e2] : e3) } — haciéndolo inequívocamente un set literal. O podríamos añadirlos alrededor de la primera parte — { (e1 ? [e2]) : e3 } — haciéndolo inequívocamente un map literal. Pero en ausencia de paréntesis, el parser no sabe qué hacer.

Hay varias soluciones para esta ambigüedad, pero ninguna parece muy satisfactoria. Un enfoque es confiar en los espacios en blanco para distinguir entre las opciones. En este enfoque, siempre se trata e1 ? [e2] como el inicio de una expresión condicional, porque hay un espacio entre el ? y el [. Y siempre se trata e1?[e2] como un subíndice null-aware porque no hay espacio entre esos dos tokens. Pero confiar en los espacios en blanco puede perjudicar la experiencia del usuario.

En teoría, confiar en los espacios en blanco no es un problema en código formateado. Pero muchos usuarios escriben código Dart sin formatear como entrada para el formateador. Y ese formato de entrada se volvería más sensible a los espacios en blanco y frágil en este rincón del lenguaje. Hasta ahora, ese tipo de rincones es muy raro en Dart, lo cual es una buena característica. (Uno de esos rincones es que — — a y --a son ambos válidos pero significan cosas diferentes.)

Ignorando la ambigüedad, hay otra razón para usar el punto: si añadimos formas null-aware para otros operadores — e1?.+(e2), etc. — probablemente querremos requerir el punto, en cuyo caso requerirlo para el subíndice es consistente con ese futuro.

Otra adición que hemos discutido para NNBD es una sintaxis de llamada null-aware. Si no requerimos un punto ahí, tiene exactamente el mismo problema de ambigüedad:

var wat = { e1?(e2):e3 }; // Map or set?

Sea cual sea la solución que se nos ocurra para ?[, también tendremos que aplicarla a ?(.

Finalmente, considera este ejemplo de encadenamiento del subíndice:

someJson?[recipes]?[2]?[ingredients]?[pepper]

A nuestros ojos, eso no se ve muy bien. Se lee menos como una cadena de métodos y más como una combinación de operadores infijos — un poco como ??. Compáralo con este código:

someJson?.[recipes]?.[2]?.[ingredients]?.[pepper]

Aquí, es más claramente una cadena de métodos. Comunicarlo visualmente también es importante, porque los usuarios necesitan entender rápidamente cuánto de una expresión será null-short-circuited.

Poniendo todo eso junto, parece que deberíamos usar la forma ?.[ por las siguientes razones:

  • Evita problemas de ambigüedad. (El lexer ya trata ?. como un único token "null-aware".)

  • Se extiende naturalmente a una llamada null-aware.

  • Se extiende a otros operadores null-aware.

  • Deja a Dart como un lenguaje de entrada más robusto para el formateador.

  • Se ve bastante bien en una cadena de métodos.

  • Hace visualmente más claro cuánto de una cadena de métodos puede ser short-circuited.

¿Qué opinas?

#

Siempre estamos interesados en feedback y sugerencias. La mejor forma de dar feedback sobre esta sintaxis es comentar (o darle thumbs-up a un comentario) en el issue del lenguaje #376. Mientras estás en eso, considera revisar todas las otras características interesantes del lenguaje en las que estamos trabajando.

Aquí es donde puedes encontrar más información:

NNBD:

Otros cambios y características del lenguaje Dart:

Una declaración de función que demuestra un tipo de retorno nullable y un argumento nullable.
Una función que acepta un argumento null y puede retornar null.

Más de Dart