recusado-nao-e-adiado.mdx

Recusado não é adiado, e remover é mais difícil que os dois

A referência do Leaf ficou fechada por meses, e a lista do que não entra na primeira versão é longa. A maior parte dela não é "não deu tempo".

Isso importa porque existem duas listas dentro de uma. Há o que está adiado — vem na v2, quando houver motivo. E há o que está recusado — não vem nunca, porque a linguagem seria pior com aquilo dentro. Roteiro que só sabe adiar é lista de desejos.

Em agosto eu reabri a referência para uma revisão, e ela produziu uma terceira lista que eu não esperava: o que já estava escrito e saiu. Essa é a difícil.

Neste post

Recusar antes é barato

As duas recusas mais antigas do projeto são o try/catch e o operador de identidade de referência. O post sobre a linguagem tem o argumento dos dois por inteiro, então aqui basta o resumo do porquê eles são recusa e não adiamento.

catch não existe porque há dois mecanismos de falha com propósitos diferentes, e um catch deixaria o script engolir o mecanismo errado — o que, num motor de jogo, é script escondendo um overflow e produzindo número errado em silêncio pelo resto do frame.

Operador de identidade não existe porque, sendo todo valor composto imutável, "mesma referência" é detalhe de implementação. Expor isso transformaria uma otimização futura em semântica observável: alguém escreveria código contra o acidente, e a otimização passaria a quebrar código real.

O que as duas têm em comum é o custo: zero. Elas foram recusadas antes de existir uma linha de código, e recusar uma ideia que ainda não custou nada é a coisa mais fácil que existe em projeto de software.

Recusar depois é caro

Na revisão de agosto saíram cinco coisas que estavam escritas, especificadas e com exemplos: pub, static, Action, @no_self e @export.

Nenhuma delas estava quebrada. Todas funcionavam no papel, todas tinham justificativa registrada, e três delas apareciam na primeira página da referência. Foi exatamente por isso que doeu.

O que destravou a decisão foi encontrar um critério, e o critério só apareceu no meio da conversa:

Nenhuma palavra da linguagem deve existir só para descrever o embedding.

pub marcava "esta função é chamável pelo Rust". Era a única palavra da linguagem que não fazia sentido nenhum sem falar da máquina que a hospeda — alguém lendo um .leaf sem saber o que é uma VM não teria como entender por que ela está ali. Com o critério na mão, a discussão parou de ser sobre gosto e passou a ser sobre uma regra: ela reprova, então sai.

static caiu por um segundo critério, mais simples: ela duplicava uma regra que já existia em outro lugar. Duas formas de dizer a mesma coisa é o que a linguagem recusa na indentação, na igualdade e no acesso a estado — não fazia sentido manter uma exceção justamente numa palavra-chave.

E aí veio a parte que me convenceu de que a revisão valeu: com as duas fora, o que sobrou explica melhor. Antes eu precisava de dois parágrafos para dizer quando um método é de instância e quando é do tipo, porque a resposta era uma em bloco de implementação e outra no topo do arquivo. Agora é uma frase, e ela vale nos dois lugares.

Cinco palavras a menos, e a linguagem diz a mesma coisa.

A recusa mais recente

Estado compartilhado entre scripts era uma ausência incômoda. Um placar, um contador de dificuldade, uma semente de número aleatório comum — coisas que qualquer jogo pede, e que sem variável global viram pedido de "escreva em Rust e recompile". Que é justamente o problema que o Leaf existe para resolver.

Passei um bom tempo tentando fazer caber. Três desenhos diferentes, e todos morreram na mesma pergunta: onde o valor mora?

Um Array vive numa arena de memória, e arena pertence a uma instância de script. Um global de tipo composto guardaria um ponteiro para uma arena que pode desaparecer quando uma entidade não relacionada morrer. O comportamento não é inseguro — a máquina detecta e devolve erro. Ele é pior que isso: é incompreensível. Seu placar fica inválido porque um inimigo morreu do outro lado do mapa.

A saída mais óbvia — restringir globais a números — funciona e é barata. Mas eu sabia de antemão o que aconteceria: no dia seguinte alguém pede um texto, depois uma lista, e cada pedido é pequeno e razoável, e o terceiro me obriga à arquitetura que eu tinha acabado de descartar. Projetar uma feature sabendo que vou ser pressionado a estendê-la para onde não quero ir é escolher a briga errada.

Então virou recusa, com o motivo escrito para não ter que ser redescoberto:

Não existe variável global. Valor composto vive numa arena, e arena pertence a uma instância — então estado compartilhado seria uma instância, não uma variável.

Isso é uma regra que se explica sozinha. A versão anterior — "não existe variável global entre scripts" — era um decreto, e decreto tem que ser redefendido toda vez que alguém pergunta.

Recusar uma conveniência

Uma categoria mais sutil: coisas que tornariam a linguagem mais fácil de escrever e que foram recusadas porque a facilidade seria uma mentira.

Quase todo tipo do Leaf tem valor padrão, então declarar sem inicializar funciona. Dois não têm: o tipo que carrega sucesso-ou-falha, e o tipo que guarda uma função.

Para o primeiro, o único valor padrão possível seria "deu certo". Numa linguagem cujo tema é toda falha vira valor, uma variável não inicializada afirmando sucesso é o pior default imaginável — e a alternativa, "deu errado, sem motivo", é igualmente falsa.

Para o segundo, o padrão natural seria uma função que não faz nada e devolve zero. É a mesma armadilha: seu cálculo de dano fica valendo zero para sempre, sem erro, sem log, sem nada. Você não perde a tarde procurando o bug — você perde a tarde procurando no lugar errado.

Nos dois casos, a alternativa recusada era mais confortável. É por isso que essa categoria é a que mais escapa: nada nela parece errado na hora de escrever.

O que é só adiado mesmo

Inferência de tipos, genéricos definidos pelo usuário, tipo indireto que destravaria estruturas recursivas, iteração sobre tipo do usuário, funções geradoras, formatação dentro de interpolação. Custo-benefício comum: cada uma custa implementação e nenhuma impede a v1 de ser usável.

A inferência merece uma linha. Escrever todo tipo explicitamente significa um verificador sem unificação, sem variável de tipo, checando de baixo para cima. É mais para digitar e muito menos para errar — e, quando erra, o erro aponta a linha que você escreveu, não um ponto três funções acima onde a inferência começou a divergir.

E o estado compartilhado da seção anterior entrou aqui com sobrenome: recusado na v1, com dois desenhos anotados para quando houver um caso de uso real empurrando. A diferença entre isso e "vamos ver depois" é que a análise já está escrita — quem voltar ao assunto começa de onde eu parei, não do zero.

Por que a distinção importa

Fechar escopo não é teatro de disciplina. É o que torna barato dizer não depois.

Enquanto a referência está aberta, toda boa ideia é candidata, e recusar custa uma discussão inteira. Com a lista escrita, recusar custa um link — e a ideia não é rejeitada, ela é v2. É essa reformulação que impede um projeto de uma pessoa só de virar uma especificação infinita que nunca compila nada.

Mas a revisão de agosto me ensinou o outro lado, que eu não sabia quando escrevi a primeira versão dessa lista: a lista não é uma jaula. Ela existe para tornar barato dizer não, não para tornar impossível dizer "eu errei".

A diferença entre as duas coisas é ter um critério. Sem critério, reabrir escopo é ceder à ideia mais recente que alguém teve. Com critério, é aplicar uma regra que você escreveu quando estava frio — e descobrir que ela reprova algo que você mesmo tinha aprovado antes de ter a regra.

Foi o que aconteceu com as cinco palavras. E é a única forma que eu conheço de mudar de ideia sem que mudar de ideia vire hábito.