
Implementada por muitas linguagens de programação, é fato absoluto que a referência nula (null, Nothing, nil, etc) causou e causa muitas dores de cabeça, tanto nos programadores quanto nos usuários. Quem nunca tomou uma NullPointerException em Java, ou uma NullReferenceException em C#? E os ponteiros nulos de C/C++ então?
O próprio criador do null, Tony Hoare, reconheceu o problema e o chamou de “erro de um bilhão de dólares”:
“I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years”.
O principal problema do null é que seu tratamento depende unicamente da disciplina do programador. Geralmente o compilador não ajudará neste sentido, na melhor das hipóteses emitirá um warning (que quase sempre é ignorado). As consequências logo chegam no formato de erros de runtime em produção, muitas vezes difíceis de resolver.
Veja o código C# abaixo:
public Usuario ObterUsuario(int id)
{
return repository.GetById(id);
}
O retorno esperado é um objeto do tipo Usuario, mas e se não existir um usuário no banco com o ID passado? Será retornado null, e se o método chamador não verificar a possibilidade de null, inevitavelmente ocorrerão erros em runtime. O ideal é deixar claro que um método ou função pode retornar null, se este for o caso. Mas como fazer isso?
Funções puras e impuras
Em programação funcional, existem os conceitos de funções puras e impuras. Uma função pura é aquela que, dado um conjunto de parâmetros, o retorno será sempre o mesmo. Por exemplo, uma função chamada Soma que recebe dois parâmetros inteiros e retorna a soma dos dois:
public int Soma(int x1, int x2) => x1 + x2;
Independente da situação e do momento, passando os mesmos parâmetros você terá sempre o mesmo resultado, ou seja, não existem efeitos colaterais (side effects) que interferem no resultado da função.
Diferente de uma função ou método que obtém um objeto do banco de dados, por exemplo. Não existe certeza absoluta que, dado um determinado ID de um registro, ele sempre estará lá. Alguém pode ter excluído este registro, o que faz com que essa função seja impura. Funções e métodos que dependem de operações de IO (banco de dados, sistemas de arquivos, operações de rede, etc) são, por natureza, impuras, pois podem ser afetadas por “efeitos colaterais” que alteram o estado da informação.
Como proceder com funções impuras?
Existe um pattern no paradigma funcional que torna seguro o trabalho com funções impuras. O nome deste pattern é Monad, e basicamente visa trabalhar com uma função impura de modo a envolver o resultado em um contexto seguro, tanto para a presença de valor quanto para um retorno nulo, ou ocorrência de erros.
Uma Monad nada mais é do que um container que envolve o valor em um contexto, fornecendo meios de obtê-lo (ou não, caso seja um valor nulo, ou um erro) de forma segura e tratável.
Muito provavelmente você já trabalhou com Monads e nem se deu conta. Por exemplo, os tipos opcionais de Java (Optional) e F# (Option), o tipo Maybe de Haskell, a própria API Linq do C#, etc.
No caso do F#, o tipo Option é usado para um valor que pode existir (Some) ou não (None):
type Option<'a> = // use a generic definition
| Some of 'a // valid value
| None // missing
O valor do tipo opcional deve ser tratado com pattern matching, em que cada possibilidade terá obrigatoriamente um tratamento:
match valorOpcional with
| Some x -> printfn "Valor retornado: %A" x
| None -> printfn "Não encontrado."
Veja que o uso de Option no F# garante segurança, independente do resultado ser nulo ou não. Para obter o valor, é necessário tratar todas as situações possíveis. Não há como negligenciar o tratamento de um retorno nulo, neste caso.
Um tipo Monad deve também ser capaz de permitir execuções encadeadas (pipeline) através de aplicações de funções, através dos métodos (operações) return e bind.
Veja a implementação de uma classe Monad genérica para C#:
/// <summary>
/// Implementação mínima de um tipo Monad (mônada).
/// </summary>
/// <typeparam name="T">Tipo do valor</typeparam>
public abstract class Monad<T>
{
private readonly T value;
public Monad(T value)
=> this.value = value;
public Monad()
{ }
/// <summary>
/// Encapsula um valor em uma monad.
/// </summary>
public Monad<T> Return(T value)
=> new Monad<N>(value);
/// <summary>
/// Possibilita a transformação do valor através da aplicação de uma função, retornando uma nova monad.
/// </summary>
public Monad<TOut> Bind<TOut>(Func<T, Monad<TOut>> func)
=> func(value);
}
Esta é uma classe que segue o pattern Monad. Ela possui dois métodos, Return e Bind. O método Return recebe um valor comum e o envolve em um contexto (monad). O método Bind recebe uma função que transforma o valor em uma nova monad, permitindo o encadeamento (pipeline).
Pode parecer confuso, mas imagine o seguinte cenário: uma sequência de processamentos, onde cada etapa gera uma saída, e cada saída será a entrada para outro processamento. Imagine todos os tratamentos para evitar problemas com valores null, exceções, validações de dados, etc. Usando monads, é possível organizar tudo isso em um pipeline de execução de forma extremamente enxuta e organizada, com toda segurança. Veremos exemplos em um próximo artigo.
Um pensamento em “Null, o erro de um bilhão de dólares”