Pular para o conteúdo

Racional · ensaio 06

Ausência

rationale.md · 75 linhas · 3 min de leitura

A redefinição: sem null, sem zero-value implícito. Três conceitos que outras linguagens confundem ficam separados: ausência, default e null.

“E se não tiver valor?” é uma das perguntas mais antigas de programação, e a resposta mais comum, o null/nil/NULL, é reconhecidamente o “erro de um bilhão de dólares”: um valor que habita qualquer tipo e estoura no acesso. Go suaviza com o zero-value (todo tipo tem um default implícito), mas troca um problema por outro.

Três conceitos, três respostas distintas, sem sobreposição:

  1. Ausência = Optional[T], com o valor builtin none. O tipo diz que pode faltar. Você desempacota com match ou com a API honesta (is_present/or_panic/or). Um ponteiro é garantidamente válido; “pode não ter ponteiro” é Optional[*T].
  2. Default = RHS explícito. Todo binding nasce com um valor que você escreve (x := 0, var nome := ""). 0 e "" só aparecem quando você os digita.
  3. Null = não existe. Não há sentinela “isto é nada” habitando os tipos.

E o caso que parece contradizer mas confirma: o Value.Null de JSON/binário (cap. encoding). Ele é um token do formato (o literal null que o JSON tem), não o null da linguagem. É uma variante presente e normal de um enum; mapeia para Optional.none só na fronteira de serialização. Três conceitos, e nem o token de formato fura a regra.

A separação ataca uma ambiguidade específica do zero-value: “é 0 porque alguém pediu 0, ou porque ninguém inicializou?” Em Go, var x int é 0 e var p *T é nil sem você ter pedido, e depois não há como distinguir “faltou” de “é o default”. Makoto fecha a porta: como todo binding tem RHS, um 0 é sempre intencional, e a ausência é sempre um Optional explícito. A classe inteira de bugs “esqueci de inicializar” desaparece porque não há o que esquecer; não se escreve variável vazia.

Isso encaixa no modelo de ponteiros (cap. 02): o ponteiro não-anulável só é possível porque a ausência mora num tipo separado (Optional[*T]). E encaixa no de erros (cap. 05): alocação que pode falhar não devolve um ponteiro nulo silencioso; devolve um Result com erro. Ausência, default e falha são três coisas, e cada uma tem sua forma tipada.

  • null/nil implícito (C/Java/Go-pointers). Habita todo tipo, estoura no acesso, e não aparece na assinatura. Optional[T] põe a possibilidade no tipo e força o desempacotamento.
  • Zero-value (Go). Resolve “sempre tem um valor” mas cria a ambiguidade “0 ou faltou?”. O RHS obrigatório dá o “sempre tem valor” sem a ambiguidade, porque o valor é sempre escolhido.
  • Coerção por omissão (campo omitido vira default invisível, C++). Reabre o “valor que ninguém pediu”. Em Makoto, omitir é erro; você escreve o que quer.
  • nil, nil como retorno (Go). “É nil porque não importa, ou porque falhou?” Ambíguo. Result[(...), E] separa: sucesso é Ok, falha é Err; um não se disfarça de outro.

O NullPointerException/segfault que sobe de um campo que ninguém inicializou. O struct Go que você acha estar preenchido e está cheio de zeros silenciosos. O if (x === null || x === undefined) repetido em JavaScript, onde dois “nadas” diferentes fazem a mesma coisa. Cada uma é a confusão entre ausência, default e null cobrando o preço.

Ausência é tipada; default é escrito; null não existe. Se algo pode faltar, o tipo diz (Optional[T]). Se tem um valor inicial, você o escreve. Não há terceiro caso mágico.

O preço é verbosidade no RHS: você inicializa tudo, e às vezes escreve uma fileira de x := 0; y := 0. E Optional precisa ser desempacotado em todo uso, em vez de lido como se fosse o valor. Mais boilerplate, em troca de segurança garantida em tempo de compilação: a ausência nunca te pega de surpresa, porque ela está sempre escrita no tipo.

Próximo: 07 · Tipos