A melhor ideia que nunca pegou

Em 1984, Donald Knuth publicou um artigo que propunha uma inversão radical. Programas não deveriam ser escritos para compilers e anotados para humanos. Deveriam ser escritos como literatura para humanos, da qual os compilers extraem as partes executáveis. Ele chamou isso de literate programming, e construiu o TeX dessa forma.

A ideia é bela. Também está quase completamente ausente do desenvolvimento de software moderno.

TeX continua sendo um dos programas grandes mais confiáveis e bem compreendidos já escritos. Esse sucesso se deve em parte ao fato de Knuth tê-lo escrito como um literate program. Portanto, a questão não é se a literate programming pode funcionar. Ela claramente pode. A questão é por que funcionou para Knuth e para quase ninguém mais.

O que a programação literária realmente significa

O sistema WEB de Knuth, mais tarde CWEB para C, funciona dividindo um único arquivo-fonte em duas saídas diferentes. Você escreve um documento narrativo que explica o que o programa faz, por que o faz e como as peças se encaixam. Incrustados nessa narrativa estão fragmentos de código.

A ferramenta tangle extrai os fragmentos de código, os ordena para o compiler e produz um arquivo que a máquina pode executar. A ferramenta weave extrai a prosa, formata os fragmentos de código como blocos bem apresentados e produz um documento que um humano pode ler.

Aqui está como um pequeno programa em CWEB se parece na prática:

@* Introduction. This program computes the sum of a list of integers.
The algorithm is straightforward: iterate through the array and accumulate.

@c
#include <stdio.h>

@<Function prototypes@>;
@<Main program@>;

@*2 The core logic.
We define |sum_array| to take a pointer and a length, returning the total.

@<Function definitions@>=
int sum_array(const int *arr, size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; i++) {
        total += arr[i];
    }
    return total;
}

@*2 Entry point.
A small driver to exercise the function.

@<Main program@>=
int main(void) {
    int data[] = {1, 2, 3, 4, 5};
    size_t len = sizeof(data) / sizeof(data[0]);
    printf("%d\n", sum_array(data, len));
    return 0;
}

Observe o que está acontecendo. O código não está organizado por ordem de compilação. Está organizado por lógica narrativa. A explicação vem primeiro. A implementação segue quando o leitor está pronto para ela. A sintaxe @<Main program@> é um fragmento nomeado que pode ser definido em qualquer lugar do documento e montado pelo tangle na ordem correta para o compiler.

Esta é a premissa central: o humano lê uma explicação de cima para baixo. O compiler obtém um grafo de dependências de baixo para cima. Ambos recebem o formato que preferem.

Por que funcionou para Knuth

Knuth não é um programador típico. Ele é um matemático e escritor que por acaso escreve programas. Ele projeta algoritmos no papel por meses antes de tocar um teclado. Ele escreve livros que são lidos décadas depois. Quando Knuth diz que código deveria ser literatura, ele quer dizer literatura de verdade, e ele tem a habilidade para entregar isso.

TeX também era um candidato perfeito. É um programa batch com uma especificação estável. O núcleo algorítmico, especialmente as rotinas de quebra de linha e quebra de página, beneficia-se enormemente de explicações matemáticas extensas. O código muda lentamente. A documentação sobrevive à implementação.

A maioria dos softwares não é assim. A maioria dos softwares muda diariamente. A especificação é descoberta, não projetada. O público não é um leitor tentando entender um algoritmo. É um desenvolvedor tentando consertar um bug antes da reunião diária.

O problema da edição

A programação literária pressupõe um processo de escrita. A maioria da programação é um processo de edição.

Quando você escreve um ensaio, você redige, revisa e aprimora. A estrutura é planejada. Quando você escreve software, você explora, testa, refatora e entrega. A estrutura emerge. A documentação em um literate program não é uma camada que você aplica depois. É a estrutura primária. Toda vez que você renomeia uma variável, extrai uma função ou reordena a lógica, você está reescrevendo a literatura.

Isso cria um ciclo vicioso. Se a prosa estiver fortemente acoplada ao código, a refatoração se torna cara. Se a prosa estiver fracamente acoplada, ela se desvia do código e se torna incorreta. De qualquer forma, a documentação apodrece.

O controle de versão moderno piora isso. Um literate program é um único documento narrativo. Um pull request é um diff contra uma árvore de fontes. Ferramentas de revisão de código entendem linhas em main.c. Elas não entendem fragmentos narrativos em main.w. O ecossistema de ferramentas, do syntax highlighting à análise estática aos pipelines de CI, espera arquivos-fonte compiláveis. A programação literária exige que você opte por não usar tudo isso.

A incompatibilidade de habilidades

Knuth assumiu que programadores também são escritores. A maioria não é.

Boa escrita técnica é rara porque é difícil. Requer empatia por um leitor que ainda não entende o que você entende. Requer a disciplina de explicar por que, não apenas o que. Requer edição, que é uma habilidade separada da programação.

Quando você pede a uma equipe para escrever literate programs, você está pedindo que ela seja Knuth. Você está pedindo prosa extensa que explique intenção, explore alternativas e guie um leitor através do raciocínio. O comentário de código médio é // TODO: fix this. A lacuna entre isso e literatura não é um problema de ferramentas. É um problema humano.

É por isso que tentativas de reviver a programação literária com ferramentas melhores falharam consistentemente. O gargalo nunca foi a toolchain de weave e tangle. O gargalo é que a maioria dos programadores não quer escrever ensaios, e a maioria das bases de código não recompensa documentação de qualidade de ensaio.

O que realmente a substituiu

A indústria não abandonou o objetivo de Knuth. Ela alcançou uma versão mais fraca dele por meios diferentes.

Sistemas de tipos agora codificam intenção que antes exigia parágrafos de explicação. Quando uma função aceita NonEmptyList<T> em vez de List<T>, o type checker impõe uma garantia que a documentação só podia descrever. Quando o borrow checker do Rust rejeita um escape de referência, ele comunica uma restrição que levaria páginas para explicar em prosa.

Docstrings e ferramentas de documentação de API como Javadoc, Rustdoc e TypeDoc criaram um meio-termo. Elas mantêm a documentação adjacente ao código sem exigir que ela conduza a estrutura. Você pode ler a fonte ou os documentos gerados, e ambos permanecem em sincronia porque vivem no mesmo arquivo.

Suítes de teste se tornaram documentação executável. Um teste bem escrito diz “dada esta entrada, espere esta saída” com mais precisão do que a prosa jamais poderia. Testes property-based codificam invariantes. Testes snapshot capturam intenção. Os testes são executados a cada commit, então não podem se desviar como a prosa.

Talvez o mais importante seja que as próprias linguagens de programação se tornaram mais literárias. Python se lê como pseudocódigo. A sintaxe if let do Rust expressa intenção de pattern matching diretamente. Quando o código é a explicação, você não precisa de uma explicação separada do código.

A versão moderna que deu certo

Há um lugar onde a view de Knuth sobreviveu quase intacta: computational notebooks.

Jupyter notebooks, R Markdown e Observable permitem que prosa e código coexistam em um único documento. A prosa explica. O código executa. A saída aparece inline. Isso é literate programming em tudo menos no nome.

Os notebooks tiveram sucesso onde WEB falhou porque o contexto é diferente. Um cientista de dados explorando um conjunto de dados está envolvido em um processo narrativo. O código muda lentamente. O público é um leitor humano tentando entender uma decisão. O ciclo de edição é exploração, não refatoração. As restrições que mataram a programação literária na engenharia de software são recursos na ciência de dados.

A lição não é que a programação literária estava errada. É que a ideia tem um nicho ecológico estreito. Ela prospera quando o programa é um artefato acabado destinado a ser estudado. Ela sufoca quando o programa é um sistema vivo destinado a ser alterado.

O que tirar disso

Você provavelmente não vai adotar CWEB para seu próximo serviço. Tudo bem. Mas o insight subjacente de Knuth ainda vale a pena aplicar.

Nomeie as coisas para que elas se expliquem sozinhas. Uma função chamada process_data é um fracasso da programação literária mesmo se você nunca tocar em WEB. Uma função chamada remove_expired_sessions_older_than carrega sua própria documentação.

Escreva a explicação que você gostaria de ter tido. Se uma seção de código requer um parágrafo de prosa para ser entendida, escreva o parágrafo. Coloque-o em um comentário, um docstring ou um documento de design. O meio é menos importante do que o ato de explicar.

Separe a estrutura narrativa da estrutura do compiler quando isso ajudar. O desenvolvimento orientado a README, os architecture decision records e os RFCs são formas de escrever a história voltada para humanos sem lutar contra sua toolchain.

Knuth queria que escrevêssemos programas como livros. Acabamos escrevendo livros sobre programas. Isso é uma vitória mais fraca do que ele esperava, mas ainda é uma vitória.