Decisão sobre a sintaxe de nulabilidade do Dart: a?[b] ou a?.[b]

O Dart está redesenhando seu sistema de tipos para que cada tipo seja anulável (expressões desse tipo podem ter o valor null) ou não anulável…

Atualização (3/2021): Graças ao seu feedback, a equipe do Dart decidiu usar a sintaxe a?[b].

O Dart está em processo de redesenho do seu sistema de tipos para que cada tipo seja anulável (expressões desse tipo podem ter o valor null) ou não anulável. Mais adiante este ano, contaremos mais sobre o cronograma e o processo de lançamento, mas, com o tempo, o código Dart será não anulável por padrão (NNBD), e você precisará usar uma sintaxe especial para dizer que um tipo aceita valores null.

Por exemplo, para declarar que um inteiro pode ser null, você precisará adicionar um ? após o tipo:

int? someInt; // someInt can be null.

Essa sintaxe com ponto de interrogação pode ser familiar se você já viu código em Kotlin, Swift ou C#. Mas algumas coisas são diferentes, principalmente o operador de subscrito ([]), que você mais comumente vê usado para acessar listas ou arrays. C# e Swift usam ?[]. O plano atual para o Dart (e, aliás, para o ECMAScript) é usar ?.[]:

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

Este artigo mostra os bastidores para explicar o raciocínio por trás dessa decisão e para incentivar você a dar sua opinião e suas sugestões. A maior parte deste conteúdo é baseada em um comentário de Bob Nystrom (munificent) na issue de linguagem #376, que resume uma discussão entre Bob e o responsável pela especificação do NNBD, Leaf Petersen (leafpetersen).

Por que usar um ponto?

#

As duas opções para [] têm vantagens.

e1?[e2]:

  • Segue C# e Swift

  • É concisa

  • É parecida com a sintaxe do operador ! (que você poderá adicionar após uma expressão para dizer que o valor dela não é null, mesmo que ela tenha um tipo anulável): e1![e2]

e1?.[e2]:

  • É parecida com a sintaxe de cascata: e1..[e2] // cascade syntax e1?..[e2] // null-aware cascade syntax

  • É parecida com a outra sintaxe de método null-aware: e1?.e2()

  • Se estende naturalmente a outros operadores

  • Evita a ambiguidade no código a seguir: { e1 ? [e2] : e3 }

Passamos um bom tempo tentando encontrar formas de evitar a ambiguidade de ?[. O problema é que não dá para saber se um código como { e1 ? [e2] : e3 } é um literal de conjunto que contém o resultado de uma expressão condicional, ou um literal de mapa que contém o resultado de um subscrito null-aware.

Se adicionarmos parênteses para tornar esse código não ambíguo, poderíamos escolher colocá-los em volta da expressão inteira, { (e1 ? [e2] : e3) }, tornando-o inequivocamente um literal de conjunto. Ou poderíamos colocá-los em volta da primeira parte, { (e1 ? [e2]) : e3 }, tornando-o inequivocamente um literal de mapa. Mas, na ausência de parênteses, o parser não sabe o que fazer.

Existem várias soluções para essa ambiguidade, mas nenhuma parece muito satisfatória. Uma abordagem é depender de espaços em branco para distinguir as opções. Nela, você sempre trata e1 ? [e2] como o início de uma expressão condicional, porque há um espaço entre o ? e o [. E você sempre trata e1?[e2] como um subscrito null-aware, porque não há espaço entre esses dois tokens. Mas depender de espaços em branco pode prejudicar muito a experiência do usuário.

Em teoria, depender de espaços em branco não é um problema em código formatado. Mas muitos usuários escrevem código Dart não formatado como entrada para o formatador. E esse formato de entrada se tornaria, assim, mais sensível e frágil a espaços nesse canto da linguagem. Até agora, esses tipos de canto são muito raros no Dart, o que é um recurso interessante. (Um desses cantos é que — — a e --a são ambos válidos, mas têm significados diferentes.)

Ignorando a ambiguidade, há outro motivo para usar o ponto: se adicionarmos formas null-aware para outros operadores, como e1?.+(e2), provavelmente vamos querer exigir o ponto, e nesse caso exigi-lo também para o subscrito é consistente com esse futuro.

Outra adição que discutimos para o NNBD é uma sintaxe de chamada null-aware. Se não exigirmos um ponto ali, ela tem exatamente o mesmo problema de ambiguidade:

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

Qualquer correção que encontrarmos para o ?[, também teremos que aplicar ao ?(.

Por fim, considere este exemplo de encadeamento de subscritos:

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

Para nossos olhos, isso não fica muito bom. Parece menos uma cadeia de métodos e mais uma combinação de operadores infixos, um pouco como ??. Compare com este código:

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

Aqui, fica mais claramente uma cadeia de métodos. Comunicar isso visualmente também é importante, porque os usuários precisam entender rapidamente quanta parte de uma expressão será interrompida por null.

Juntando tudo isso, parece que devemos usar a forma ?.[ pelos seguintes motivos:

  • Evita problemas de ambiguidade. (O lexer já trata ?. como um único token “null-aware”.)

  • Se estende naturalmente a uma chamada null-aware.

  • Se estende a outros operadores null-aware.

  • Deixa o Dart como uma linguagem de entrada mais robusta para o formatador.

  • Fica bastante bom em uma cadeia de métodos.

  • Deixa visualmente mais claro quanto de uma cadeia de métodos pode ser interrompido por null.

O que você acha?

#

Estamos sempre interessados em feedback e sugestões. A melhor forma de dar feedback sobre essa sintaxe é comentar (ou dar um joinha em um comentário) na issue de linguagem #376. Já que você está lá, considere também conferir todos os outros recursos legais de linguagem em que estamos trabalhando.

Aqui está onde você pode encontrar mais informações:

NNBD:

Outras mudanças e recursos da linguagem Dart:

Uma declaração de função demonstrando um tipo de retorno anulável e um argumento anulável.
Uma função que aceita um argumento null e pode retornar null.

Mais do Dart