Dart e null-safety: uma alternativa funcional

Mateus Felipe
8 min readApr 3, 2020

--

Na última versão estável de Dart, a saber, a versão 2.7, uma das features anunciadas foi a preview do Null safety. A linguagem está caminhando para uma direção em que os tipos são non-null by default (nnbd). Neste novo modo de tipagem, a não ser que explicitamente declarado, um valor de uma variável qualquer não pode ser null.

Este tipo de controle é o mínimo esperado de qualquer linguagem decente. null sempre foi um problema, em qualquer linguagem de programação, desde o seu surgimento. Não faltam exemplos e fontes que indiquem isso¹², e não é a toa que seu criador o considerou como “o erro que custou bilhões de dólares”. E foi por conta disso que o time do Dart decidiu, finalmente, implementar essa tão necessária feature.

Atualização (10/06/2020): no novo post sobre essa feature, o time do Dart apontou que além dos benefícios de segurança, o nnbd também permite otimizações adicionais no código, possibilitando um programa até 19% mais rápido.

Mas então, como funciona esse tal de non-null by default? Vamos ver um exemplo.

Em Flutter, uma das widgets mais comuns é a widget de texto, que, obviamente, renderiza um texto na tela. Essa widget tem um parâmetro obrigatório data do tipo String, que representa o texto que será desenhado na tela.

O que acontece, no entanto, se tentarmos passar null para esse parâmetro? Ora, o Flutter irá retornar um erro (tanto na tela como no console): A non-null String must be provided to a Text widget. Isso acontece porque, como o Dart não provê mecanismos para evitar isso em tempo de compilação, o componente deve fazer um assert em tempo de execução.

No Flutter, o parâmetro `data` não pode ser `null`, mas isso é checado com um `assert`.

O problema é que, por esse erro acontecer apenas em tempo de compilação, pode ocorrer, muitas vezes, de passar despercebido e acabar em produção. É óbvio que ninguém passa null para a widget propositalmente, mas não é nada difícil passar um valor nulo através de uma variável que deveria ter recebido um valor em algum lugar, ou a propriedade de alguma classe. Isso já aconteceu comigo algumas vezes, e provavelmente vai continuar acontecendo, porque identificar esse tipo de regressão não é algo trivial, principalmente em grandes aplicações.

Às vezes o código compila, mas quando você executa tem uma supresinha…

Com a nova feature do Dart, no entanto, a definição da classe Text no Flutter precisará mais de um assert em tempo de execução para identificar o problema. Neste caso, se tentarmos passar null como argumento, o programa simplesmente não vai compilar: The argument type 'Null' can't be assigned to the parameter type 'String'.

Nota: nnbd é a feature que mais está sendo trabalhada atualmente no Dart, mas a especificação ainda não está completa. Você pode testar o estado atual da implementação configurando o arquivo analysis_options.yaml ou passando a opção --enable-experiment=non-nullable para o compilador.

Muito bom, mas e se eu quiser usar null?

A maior parte dos problemas relacionados ao null acontecem não por conta do null em si, mas sim de quais recursos a linguagem oferece para lidar com este conceito.

Nota: existem outros problemas relacionados ao null, como o nível semântico ou o nível conceitual matemático, mas isso é assunto para outro artigo.

De fato, querendo ou não, ao construir programas, precisamos de uma forma de expressar a possibilidade de um valor não existir, e o null é exatamente isso. Por exemplo, se estou consultando um banco de dados e procurando por um valor que não existe, o que o banco deve retornar? A possibilidade mais razoável parece ser retornar null (ou lançar uma exceção).

Por isso, nessa nova feature de null safety, quando uma variável pode representar um valor que não existe, seu tipo deve ser declarado explicitamente como nullable. Isto se faz colocando um ? após o nome do tipo. Então, por exemplo, poderíamos representar um endereço da seguinte forma:

Representação de um endereço, onde o complemento é opcional

Note que valores non-nullable podem ser usados em seus respectivos tipos nullable, enquanto o oposto não é possível.

Tentar usar um valor do tipo `String?` como `String` resulta em um erro, mesmo que o valor seja explicitamente diferente de null.

Desta forma, considerando a nossa widget de texto que não aceita mais null, passar o complemento da nossa classe de endereço acima para ser renderizada na tela sempre resultaria em um erro de compilação!

É assim que o Dart, a partir desta nova feature, consegue prever erros de referência nula em tempo de compilação e evitar que uma série de problemas sejam identificados tarde demais (isto é, em produção)!

Esta camada de segurança adicional com certeza é muito positiva para o Dart. No entanto, ainda assim, todas as vezes que você esiver lidando com um tipo nulável (T?), torna-se necessário fazer a checagem do valor para tratar o caso de o valor ser null em vez do tipo esperado. Considere o seguinte exemplo:

Exemplo adaptado deste artigo.

Neste caso, usamos o null para representar os casos em que a função sqrt ou log não podem retornar um resultado válido. Como consequência, na nossa função niceCalculation tivemos que checar duas vezes se o resultado da função era null, para então prosseguir com o cálculo. Por conta disso, temos outros dois problemas:

  1. O código fica mais verboso e complicado de entender;
  2. Caso esqueçamos de verificar algum dos nulls, teremos mais erros em tempo de execução.

O segundo problema é o mais chato, e com implicações relevantes. Como falado acima, é comum que programadores, eventualmente, esqueçam de checar o null de algum valor, e isso só traz dores de cabeça.

Diante disso, teria uma alternativa mais segura e viável? A resposta é: sim; se o null nos deixa sujeito a essa série de problemas, podemos simplesmente não usá-los.

Sem nulls, sem erros de referência!

É mesmo possível programar sem utilizar nulls?

Certamente. Se você já teve contato com alguma linguagem funcional, é óbvio do que estou falando. Caso não seja o seu caso, te apresento a mônada Option (conhecida também, em outros contextos, como Maybe).

Nota: para este artigo, estarei usando a biblioteca dartz. Os conceitos apresentados aqui, no entanto, são construtos funcionais que podem ser aplicados de forma independente.

Todos sabem que uma mônada é apenas um monóide na categoria dos endofunctores. Mas o que isso tem a ver com null?

O Option<T> é um tipo que pode representar dois casos: (1) Some<T> representa o caso da presença de um valor do tipo T, enquanto None representa o caso da ausência de um valor. Neste sentido, o Option funciona de forma muito similar ao null:

O método `log`, do nosso exemplo anterior, ficaria assim com `Option<T>`.

Não parece muito vantajoso; é até um pouco mais verboso do que usar T?. Mas lembre-se que o Option pode nos ajudar a resolver nossos problemas com o null.

Acontece que, como o valor T está encapsulado em uma Option, não é possível acessá-lo normalmente. Não posso, por exemplo, atribuí-lo a uma variável do tipo T? como é possível fazer com os tipos nuláveis. Para acessar o valor que está dentro do Option é necessário desestruturá-lo.

Perceba que, como estamos lidando com um caso onde é possível que o valor não exista, ao lidar com uma Option você é obrigado a tratar ambos os casos de presença ou ausência de valor. Diferentemente do null, por fold ser um método, ele é tratado em tempo de compilação. Fazendo desta forma, se torna impossível se deparar com um erro de referêcia nula em tempo de execução.

O nosso exemplo anterior, então, ficaria assim:

Usando o fold para desestruturar nosso Option temos gartantia em compilação que nunca vamos esquecer de checar se o valor de log, sqrt ou niceComputation existe ou não!

Nota: existem outras formas, também seguras, de lidar com o valor encapsulado do Option além da desestruturação. Por exemplo, o método getOrDefault (ou o operador |) permite que você passe um valor para o caso de None e retornar diretamente o valor do Option. Por exemplo, double calculation = niceCalculation(100) | 0.0 irá retornar o valor de calculation caso ele exista ou 0.0 caso seja None.

Isto é suficiente para resolver o principal dos problemas do null e, consequentemente, ter um código mais seguro. Mas esses métodos aninhados da função niceCalculation estão um tanto confusos e ilegíveis… E é nesse momento que as propriedades monádicas do Option se tornam úteis.

Vamos fazer algumas mudanças na função niceCalculation para ela ficar mais legível:

Que lindo! 😍

Agora temos um método perfeitamente legível e claro! Além de resolver o problema do aninhamento e verbosidade, os métodos agora são lidos na ordem sequencial.

Mas o que está exatamente acontecendo aqui? Dois métodos especiais que são propriedades de toda mônada foram usados: map e bind.

O método map é uma propriedade de todos os functores. Como o nome sugere, ele faz o mapeamento do conteúdo de um functor. No nosso caso, por exemplo, se fizermos um Some(10).map(inverse), o resultado será Some(-10). Já no caso de map ser aplicado em um None, ele retorna None.

É por isso que os métodos podem ser encadeados sem nenhum problema. Se, por exemplo, no sqrt ele retornou um None, ele vai ignorar todos os outros métodos encadeados e retornar None.

Já o método bind faz parte de todas as mônadas, e é apenas um pouco mais complexo que o map. Em vez de receber uma função T → T ele recebe uma função que retorna uma mônada, ou seja T → Option<T>. É exatamente esta a assinatura das funções sqrt e log, e exatamente por isso que tivermos que usar bind em vez de map para encadear o log. Fora isso, o funcionamento de bind em Option é praticamente igual ao map.

Ao nos utilizarmos destes construtos funcionais pudemos recriar nosso método não apenas de forma segura, evitando uma série de erros em tempo de execução, como estruturar nosso código de forma prática e elegante.

Esta é apenas a superfície do que construtos funcionais podem fazer. No geral, eles garantem um código mais seguro e bem estruturado, e pudemos ver um exemplo ao resolvermos de forma inteligente um problema comum.

Espero que você possa ter aprendido alguma coisa nesse artigo! Caso haja alguma dúvida ou contribuição, deixe nos comentários!

Fontes e artigos relacionados:

http://www.cs.man.ac.uk/~johns/npe.html

--

--

Mateus Felipe

Theology bachelor and tech adept. Currently, work as a developer, mainly with Flutter.