De Volta às Origens da Programação
Programar é Legal
Controlando o Computador
É apenas quando você sabe programar que você realmente começa a se tornar o mestre do computador. Isso é algo que o mero uso de aplicativos produzidos por terceiros jamais conseguirá alcançar.
Uma pessoa que não sabe programar não consegue sequer fazer aparecer livremente uma janela no sistema Windows para exibir uma sequência de texto especificada. Para quem sabe programar, basta criar um arquivo de texto com a extensão
.vbs contendo o seguinte:MsgBox "Hello"E então clicar duas vezes nele para executá-lo.
O VBS já é uma memória muito distante para mim, mas quando recrio e executo esse script, ele ainda exala um certo charme, embora eu saiba perfeitamente que se trata apenas do interpretador chamando nos bastidores uma API Win32 como o MessageBox.
Até certo ponto, programar pode ser visto como a satisfação de uma necessidade de poder: executar um programa que você escreveu é como um imperador dando ordens aos seus súditos. Contudo, como o custo cognitivo para satisfazer essa necessidade de poder pode ser por vezes muito elevado, isso acaba nos permitindo abandonar esse gosto de baixo nível e avançar rumo à superação de dificuldades e à busca pela transcendência.
Sabendo o Porquê das Coisas
Para os leigos, o computador é cercado de mistério: é um dispositivo cujos princípios internos são desconhecidos, assim como outros aparelhos da vida cotidiana.
Você aperta o botão da descarga e a privada dá descarga; não é preciso saber como a privada funciona. Os seres humanos só precisam aprender a apertar o botão para que a privada execute essa rotina de descarga pré-programada. O computador nada mais é do que uma privada com uma interface interativa; o PC é uma privada, e o celular também.
Os leigos sabem que há uma ligação entre a ação e o fenômeno, mas não sabem por que a ação desencadeia o fenômeno, o que está por trás da ação ou o que está por trás do fenômeno. Se os entusiastas de informática não entendem de programação, continuam sendo meros leigos, apenas sabendo um pouco mais de boatos do que outros leigos ou sendo melhores em imitar cegamente o que veem —o que não é muito diferente dos sacerdotes que presidiam rituais nos primórdios da civilização.
Dominar a programação nos permite levantar esse véu e entender plenamente o que acontece "sob o capô" do computador. Você gradualmente perceberá que, em muitos casos, o programa escrito é apenas um subproduto do processo de compreensão, ou melhor, uma prova dessa compreensão. Se você for confiante o suficiente, sua motivação para programar diminuirá bastante por causa disso. Se você for experiente o suficiente, perceberá que essa confiança infundada é frequentemente derrotada pela complexidade da realidade, e assim você ainda optará por escrever programas para realizar validações.
Além disso, você também pode obter percepções de nível superior por meio da atividade de programar, adotando uma nova perspectiva para enxergar o mundo. Por exemplo, Stephen Wolfram escreveu o livro A New Kind of Science.
Programar Não É Legal
A Beleza e a Feiura do Código
Eu realmente consigo sentir "beleza" e "feiura" no código:
- Uma boa abstração é bela; uma abstração ruim é feia; a falta de abstração é feia.
- O código escrito por alguém que compreende a fundo é belo; o código escrito por quem entende pela metade é feio; o código fruto de programação de culto à carga é feio.
- Convenções bem pensadas e fundamentadas são belas; convenções decididas levianamente para seguir modismos cegamente são feias.
- Funções puras são belas; efeitos colaterais são feios.
- A consistência é bela; a inconsistência é feia.
- ...
Um dia, já não me lembro qual, tive uma epifania. Percebi que toda a beleza no código provém, na verdade, do esforço de quem o escreve em lutar contra a complexidade, mesmo que esse esforço seja apenas adicionar ou remover alguns espaços para alinhar o texto. Por outro lado, se a pessoa que escreve o código desiste de combater a complexidade (ou se nem sequer for uma pessoa), então o código parecerá feio.
Considerando o tormento necessário para formar essa estética, é difícil esperar que as pessoas a desenvolvam de forma generalizada. Portanto, na maioria das vezes, nos depararemos com projetos compostos por códigos feios ou que apenas mal conseguem rodar. Uma grande característica desse tipo de projeto é que os desenvolvedores sempre parecem muito dedicados, pois são forçados a compensar o fracasso estratégico com diligência tática. Assim, a quantidade de alterações de código e a contagem de commits atingem facilmente ordens de magnitude astronômicas, mas isso não significa de forma alguma que o projeto seja mais maduro.
Cedendo a Abstrações Fracassadas
Algumas abstrações na computação são um desastre absoluto; olhando em retrospecto, parece que foram desenhadas por pessoas sem juízo.
Tomemos o SQL, por exemplo: uma camada de abstração completamente fracassada. O design sintático dessa coisa é tão ruim que diagramas de sintaxe ferroviários tornaram-se indispensáveis na documentação, e sua reutilização é tão baixa que os desenvolvedores frequentemente precisam copiar seu próprio código antigo. Hoje, o único papel positivo do SQL é talvez criar a ilusão de que as habilidades de gerenciamento de banco de dados são transferíveis entre diferentes SGBDs —fazendo com que alguns acreditem erroneamente que um DBA focado em MySQL também tenha a capacidade de lidar com PostgreSQL.
Entre os bancos de dados relacionais, exceto por alguns conceitos básicos comuns, o design subjacente de cada SGBD é totalmente diferente. Para se adaptar ao SQL sem perder suas próprias vantagens, os SGBDs sofrem de vazamento de abstração por todos os lados. Para deixar vazar a implementação subjacente, cada SGBD opta por inventar seu próprio dialeto sobre o SQL —o que, infelizmente, é outra decisão errada —pois se o SGBD precisar se adaptar a padrões SQL mais recentes mais tarde, surgirá a situação em que "existem duas maneiras de fazer a mesma coisa", e o padrão SQL costuma ser a opção com a pior experiência entre as duas. Por razões como "vamos seguir o padrão", você ainda precisa ignorar a opção boa para escolher a ruim. Se ainda houver conflitos sintáticos entre o SQL padrão e os dialetos, esse caos vergonhoso atingirá o seu ápice.
Se todos os SGBDs tivessem projetado suas próprias interfaces do zero e descartado a camada de abstração fracassada do SQL, todos esses problemas teriam sido evitados, e cada SGBD seria muito mais fácil de usar do que é hoje. Claro, desenhar novas abstrações é difícil e cansativo, exige muita tentativa e erro e pode custar participação de mercado. Por que correr esse risco? Então, continuamos usando lixo.
O mais assustador é que, quando uma abstração já é um fracasso, ela pode piorar ainda mais: construindo novas abstrações sobre uma abstração fracassada. Ora, colocar glacê em um bolo estragado fará com que ele volte a ser um bolo bom? ORMs são basicamente o "I can fix him/her" do mundo dos bancos de dados; não é à toa que alguém já os comparou à Guerra do Vietnã da Ciência da Computação. Se o SQL apenas cria a ilusão de generalidade, os ORMs tentam transformar essa ilusão em realidade —o que, infelizmente, é impossível enquanto o design subjacente dos SGBDs for diferente. Assim, a situação passa de péssima a incompreensível: problemas que originalmente exigiam que você atravessasse apenas uma camada de abstração fracassada para resolver, agora exigem que você atravesse duas ou mais camadas de abstrações fracassadas.
A Questão da Abstração
A abstração pode ser vista como o encapsulamento da complexidade: ou ela esconde a complexidade, ou faz com que ela seja expressa de maneira um pouco mais suave.
O sistema operacional é uma boa abstração — abstrai os detalhes do hardware e deixa espaço suficiente para os usuários explorarem. No entanto, as APIs fornecidas pelos sistemas operacionais ainda não são fáceis de usar, e é por isso que existem abstrações de nível superior.
O Visual Basic também foi uma excelente abstração —quando dizemos isso hoje, referimo-nos principalmente à sua IDE. Muitos dos conceitos de design de Alan Cooper, o pai do VB, estenderam-se desde o VB até os primeiros anos da era .NET, o que forjou a era de ouro do desenvolvimento de aplicativos nativos para Windows. O designer de formulários WinForms do Visual Studio continua sendo, até hoje, o melhor produto da Microsoft que já usei —embora, no fim das contas, a Microsoft tenha estragado tudo.
Não é difícil perceber que o charme da programação está, na maioria das vezes, relacionado a boas abstrações. Podemos deduzir ainda que a maior parte dos códigos ruins está relacionada a más abstrações. Para se adaptar e tirar proveito de abstrações ruins, o código é inundado de ruídos desnecessários.
Abstrações de Granularidade Fina
O foco da programação puramente funcional reside, basicamente, em fornecer uma caixa de ferramentas de abstrações matemáticas genéricas e de granularidade fina, com as quais os programadores constroem blocos. A essência dessas abstrações matemáticas genéricas é sintática, razão pela qual a fronteira entre sintaxe e funções é muito tênue na programação puramente funcional. Teoricamente, contanto que a caixa de ferramentas seja portada, os programadores podem usar a mesma sintaxe para programar em linguagens diferentes.
Isso é legal? Talvez um pouco. Contudo, mesmo que haja algo legal nisso, a experiência dura pouco: assim que você realmente começa a programar com uma mentalidade puramente funcional, o encanto desaparece rapidamente.
A programação puramente funcional nunca se popularizou de verdade:
- Há abstrações matemáticas demais, difíceis demais de aprender.
- Mesmo dominando essas abstrações matemáticas, não há vantagem expressiva em comparação com o uso direto da sintaxe de uma linguagem de programação.
- Durante o processo de programação, cria-se constantemente um grande número de funções customizadas, o que equivale quase a criar uma enorme quantidade de sintaxes. Isso prejudica gravemente o compartilhamento de código-fonte, pois o código passa a ser escrito em uma DSL criada especificamente para aquela tarefa, a ponto de carecer totalmente de legibilidade para qualquer pessoa que não seja o autor. E mesmo o próprio autor talvez não consiga reconhecer o que escreveu três meses depois.
Abstrações de Granularidade Grossa
Pensemos em uma função embutida
GetWeather() que poderia existir em uma linguagem de altíssimo nível, a qual retorna um tipo embutido Weather como resultado.Em qualquer linguagem que não seja de altíssimo nível, ter uma função embutida dessas seria um problema, e você questionaria seriamente a competência de quem decidiu incluí-la como função nativa:
- A abstração da função mascara completamente a complexidade do problema.
- A implementação da função exige conexão a um servidor remoto como fonte de dados, o que fatalmente carece de estabilidade.
- Como funcionalidade embutida, a função e seu valor de retorno precisam permanecer estáveis entre as versões. Quando novas demandas surgirem no futuro, ela muito provavelmente não conseguirá se adaptar a tempo.
- ...
Agora, vamos revelar algumas informações adicionais sobre essa função:
- Ela é mantida nos bastidores por uma equipe dedicada de mil pessoas com recursos ilimitados, oferecendo suporte a todas as funcionalidades meteorológicas padrão imagináveis.
- É tão testada pelo tempo, adaptada a todos os casos extremos e roda com tanta perfeição há trinta anos que quase todo mundo a utiliza, sendo um padrão de fato da indústria.
- A meta oficial da linguagem é mantê-la funcionando de forma estável pelo menos até o final deste século, e eles estão extremamente confiantes de que conseguirão.
- ...
Sua perspectiva mudou um pouco? Imagino que sim, especialmente se essa função for fornecida de forma totalmente gratuita. Na verdade, agora essa função se tornou legal, porque é bem provável que você não queira implementar
GetWeather() por conta própria —você quer gastar seu tempo em coisas mais interessantes, em vez de lidar com os casos de borda gerados por uma tarefa maçante semelhante a um ETL.Sob essa ótica, não rejeitamos abstrações de caixa-preta mágicas; rejeitamos apenas as abstrações de "óleo de cobra" que fingem ser mágicas. O problema é que o óleo de cobra é comum, enquanto a mágica é rara. Como discernir entre óleo de cobra e mágica é algo cansativo, com o tempo deixamos de acreditar que a mágica existe.
O Peixe e a Pata do Urso
Sabemos que os parâmetros de
MessageBox() são mais complexos que os de MsgBox(), mas não nos importamos. Em alguns casos, você subitamente precisa se importar, e é aí que os problemas começam. Assim, a abstração de MsgBox() acaba se tornando um obstáculo, impedindo você de acessar diretamente MessageBox().Uma boa abstração, ao mesmo tempo em que oferece conveniência, não impede que você personalize detalhes, o que significa combinar granularidade fina e grossa até certo ponto. É possível ter o peixe e a pata do urso ao mesmo tempo? Ocasionalmente sim, mas na maioria dos casos, a menos que a abstração tenha sido construída por você mesmo, a probabilidade disso diminui drasticamente. Isso ocorre porque há um ciclo de feedback entre a construção e o uso da abstração; esse ciclo só funciona de forma eficaz quando você pode controlar simultaneamente a construção e o uso dela.
Não se esqueça de que a abstração em si também é um fardo. Quanto mais você investe em uma abstração de terceiros, maiores são os custos irrecuperáveis que você acumula, e mais fracassado se torna esse investimento quando você encontra um obstáculo. No pior dos cenários, você precisa de duas ou mais abstrações que atendem a um propósito semelhante, diferindo apenas em detalhes. Elas têm interfaces independentes, custos de aprendizado independentes, custos de manutenção independentes, riscos de segurança independentes, coberturas de recursos independentes, e podem até interferir umas nas outras — afinal, são abstrações distintas.
A Otimização Prematura Não Existe
A frase "a otimização prematura é a raiz de todos os males" tornou-se uma tremenda bobagem por ter sido tirada de contexto. Uma forma mais precisa de expressar isso seria: "Micro-otimizações ineficazes desperdiçam tempo de desenvolvimento e sacrificam a qualidade do código; portanto, não faça isso."
Gostaria de acrescentar mais uma coisa sob a perspectiva da abstração, algo que apenas quem já trabalhou em projetos focados em alto desempenho sabe: a razão pela qual projetos de alto desempenho conseguem atingir essa alta performance é que eles adotam um design voltado para alto desempenho desde o início. Para projetos de alto desempenho, a otimização prematura simplesmente não existe; as abstrações desses projetos são desenhadas especificamente para o desempenho desde o primeiro dia. Projetos que decidem otimizar a performance apenas depois do fato consumado não têm sequer o direito de se comparar a esses projetos —a distância entre eles já foi definida no momento em que se decidiu quais abstrações utilizar; a menos que todo o projeto seja reescrito, não há como alcançá-los.
Assim, nossa compreensão sobre uma boa abstração sobe mais um degrau: uma boa abstração deve ser também uma abstração que considere o limite máximo de desempenho. Portanto, o código despretensioso escrito por um engenheiro de software experiente o suficiente já pode levar em conta, ao mesmo tempo, a abstração e o desempenho. Contudo, buscar esse nível de excelência exige demais de um programador comum; então é melhor esquecer esse assunto e continuar usando "a otimização prematura é a raiz de todos os males" como desculpa para despachar os outros.
A Questão da Linguagem
A própria linguagem de programação é um problema.
Muitas linguagens de programação transmitem uma sensação de desconexão, que decorre do fato de que seus projetistas não são seus utilizadores. Isso faz com que o desenvolvimento das linguagens rume para dois extremos: uma linguagem torna-se extremamente sobrecarregada de recursos ou extremamente minimalista.
Uma linguagem com sobrecarga extrema de recursos muito provavelmente apenas empilha um monte de recursos sem sentido de programação orientada a objetos, enquanto as funcionalidades de linguagem que são realmente úteis permanecem indisponíveis nela. Um exemplo típico são as corrotinas: você descobrirá que muitas linguagens de programação não oferecem suporte a corrotinas que sejam úteis na engenharia — mais precisamente, corrotinas sem pilha (stackless) com capacidade de comunicação bidirecional. Essas linguagens "avançadas" ou suportam apenas corrotinas com pilha (stackful), mais pesadas e com transferência de controle opaca, ou suportam apenas corrotinas unidirecionais (iteradores), ou simplesmente fingem oferecer suporte ao despachar a sintaxe async/await de qualquer jeito. Esse único recurso de linguagem já seria suficiente para eliminar 95% das linguagens de programação da história; se eu exigir ainda mais recursos, quase nenhuma restará disponível para uso.
Linguagens extremamente minimalistas são, em essência, uma reação ao design sobrecarregado, mas que infelizmente sempre rebate na direção errada. O minimalismo cria mais problemas do que a sobrecarga. Em linguagens sobrecarregadas, os usuários podem ao menos escolher ignorar coletivamente os recursos ruins da linguagem. Já as linguagens minimalistas geralmente acabam precisando de código boilerplate para substituir os recursos ausentes da linguagem, e como o boilerplate pode ser inventado por qualquer um, isso só gera ainda mais problemas.
Por Que Não a Linguagem Natural?
No que diz respeito à programação, a linguagem natural tem uma única vantagem: sua língua materna é uma linguagem natural. De resto, são apenas desvantagens.
Como quase todas as linguagens de programação utilizam o inglês, de tempos em tempos surge no mundo de língua chinesa uma onda defendendo linguagens de programação em chinês. As tentativas de linguagens de programação em chinês nunca puderam ser chamadas de bem-sucedidas e, fatalmente, nunca o serão, pois a maioria apenas traduz as palavras-chave da sintaxe para o chinês, e a minoria restante também não evidenciou qualquer vantagem do chinês sobre o inglês. O ponto crucial é que a própria ideia de que adotar a língua materna em uma linguagem de programação traria alguma mudança já é um erro em si; uma linguagem de programação baseada na língua materna continua sendo uma linguagem de programação, não uma linguagem natural.
Em relação à linguagem natural, uma linguagem de programação pode quase ser vista como uma DSL, apenas mais genérica do que as DSLs no sentido estrito convencional. Nesse tipo de DSL, você é obrigado a usar um formato sintático prescrito para escrever o código e não há tolerância alguma para ambiguidades. No fundo, as linguagens de programação são matemática aplicada executável; todos os elementos da linguagem servem para representar símbolos matemáticos, e descrever símbolos matemáticos usando linguagem natural nunca foi mais conciso e preciso do que usar a própria linguagem da matemática.
O que aconteceria se forçássemos a substituição das linguagens de programação pela linguagem natural? O jogo de cartas colecionáveis derivado do mangá japonês Yu-Gi-Oh! é um excelente exemplo. Pegue qualquer carta lançada nos últimos anos e verá os efeitos da carta descritos em uma linguagem natural densa e minuciosa. No entanto, esses efeitos não são fáceis de entender, pois na verdade utilizam um sistema formal próprio, apenas redigido como texto na linguagem natural da região onde a carta foi lançada. Entre os jogadores, esse formato textual estranho é chamado ironicamente de "Linguagem K". A Linguagem K é repleta de palavras-chave escritas em linguagem natural, o que está longe de ser tão fácil de analisar quanto uma linguagem de programação, sem falar na falta de destaque de sintaxe (syntax highlighting). Isso faz com que o jogador, ao ler uma carta desconhecida, precise primeiro analisar mentalmente o texto em linguagem natural para a sintaxe da Linguagem K e, em seguida, converter a sintaxe da Linguagem K para a sua própria compreensão do jogo. Isso não apenas é ineficiente, mas também extremamente propenso a erros. Quando os jogadores tentam criar suas próprias cartas personalizadas, percebem também como é difícil redigir textos na Linguagem K. Como pequenas diferenças no texto se transformam em significados completamente distintos, o autor precisa ter uma compreensão bastante profunda da Linguagem K para escrever um texto adequado. Você pode imaginar que, se os efeitos das cartas fossem escritos em uma DSL, lê-los e escrevê-los seria infinitamente mais fácil.
Linguagens de Altíssimo Nível
Na verdade, bastaria uma linguagem de altíssimo nível de propósito geral, que superasse as linguagens de alto nível, para nos levar de volta àquela tarde em que clicávamos duas vezes no script VBS —na superfície, a implementação de qualquer necessidade seria tão simples e direta quanto chamar
MsgBox() no VBS para abrir uma caixa de mensagem; sob o capô, a forma como essas demandas seriam implementadas seria extremamente engenhosa, adaptando-se com perfeição à intenção do programador.Uma linguagem de altíssimo nível certamente expandiria ainda mais a definição de "detalhes com os quais não é preciso se preocupar ao programar". Considerando que já delegamos em grande parte ao compilador o gerenciamento de memória e a otimização da eficiência de execução do código, não seria de todo inaceitável se uma linguagem de altíssimo nível nos fizesse abrir mão de ainda mais decisões.
Por Que o Python Ainda Não É uma Linguagem de Altíssimo Nível
Mesmo entre a gama de linguagens de alto nível, o Python está entre as de nível mais alto:
- A sintaxe do Python é extremamente próxima da linguagem natural, sendo chamada de "pseudocódigo executável". Entre as linguagens de programação de propósito geral, a única que me vem à mente que seja mais próxima da linguagem natural do que ele é o BASIC, mas os recursos de linguagem do BASIC estão muito aquém dos seus.
- A biblioteca padrão e o ecossistema do Python são extremamente poderosos; quase todas as demandas comuns já foram atendidas. Entre as linguagens de programação de propósito geral, talvez apenas o ecossistema do JavaScript possa se comparar ao dele, mas a biblioteca padrão do JavaScript está muito aquém da sua.
Quão alto nível é o Python?
Para dar um exemplo, certa vez escrevi um programa de criptografia para fins de entretenimento, sem usar Python. Na criptografia, é necessário realizar a exponenciação modular de inteiros gigantescos, ou seja,
base ^ exponent % modulus. Essa operação é intratável de calcular com parâmetros muito grandes, o que significa que você não pode simplesmente realizar a exponenciação primeiro e depois aplicar o operador de módulo. Sua linguagem de programação suporta tipos inteiros grandes? Que pena, sob a escala de inteiros gigantescos exigida pela criptografia, ela ou apresentará um erro diretamente, ou será insuportavelmente lenta. Para realizar a exponenciação modular de números extremamente grandes, você precisa aplicar princípios matemáticos e encontrar ou escrever uma função modPow() eficiente. No Python, o tipo inteiro se expande automaticamente e de forma transparente para inteiros arbitrários, resolvendo o problema de inteiros comuns e inteiros grandes serem dois tipos diferentes. Além disso, sua função embutida pow() possui um terceiro parâmetro opcional, mod, além de base e exp —o que significa que essa função oferece suporte nativo e direto à exponenciação modular eficiente. Além disso, como pow() é implementada em C, ela é invariavelmente mais eficiente do que naquelas linguagens em que você precisa implementar a função manualmente. A maior parte da conveniência de que você precisa já foi preparada com antecedência pelo Python, enquanto em outras linguagens de programação a probabilidade é de que isso não tenha sido feito. "Life is short, you need Python" não é um mero exagero; se eu tivesse escrito a mesma aplicação em Python, provavelmente teria economizado um bom tempo.Então, o que impede o Python, que já é tão conveniente, de ser uma linguagem de altíssimo nível?
Primeiro, excluímos comparações com DSLs, pois esse tipo de comparação não tem muito sentido: o que prova o fato de o SQL ser mais adequado para consultas a bancos de dados do que linguagens de propósito geral? Depois, não consideramos a capacidade de criar DSLs dentro da linguagem, pois essa habilidade equivale quase ao nível de suporte a macros higiênicas ou a paradigmas de programação funcional, e o Lisp, que é o melhor nisso, ainda assim não é uma linguagem de altíssimo nível. Assim, percebemos que, além dos recursos de linguagem, da biblioteca padrão e do ecossistema, ele não difere em essência da linguagem BASIC; ele é apenas uma linguagem de alto nível que oferece abstrações de nível superior —um mero aprimoramento linear das linguagens de alto nível.
Tomando a exponenciação modular como exemplo novamente: uma linguagem de altíssimo nível deveria "perceber", em tempo de compilação, que o programa precisa realizar a exponenciação modular de números gigantescos. Portanto, quando o padrão
base ^ exponent % modulus aparecesse no código, ele deveria ser tratado automaticamente, em vez de exigir que o programador chame ativamente ou até implemente uma função modPow(). Como todos sabem, o Python já possui o operador dedicado ** para exponenciação; quem pensaria em usar a função pow()? Quando você descobre que precisa de uma operação dedicada de exponenciação modular para resolver o problema, já é tarde demais; o mero atrito gerado pela aquisição do conhecimento já foi excessivo.Além disso, quando você se depara com demandas não triviais para as quais nem a biblioteca padrão nem o ecossistema do Python oferecem boas soluções, a magia do Python também desaparece. E demandas não triviais são sempre muito mais comuns do que imaginamos.
LLMs
É fácil depositar as esperanças de uma linguagem de altíssimo nível nos LLMs, pois esse parece ser o único caminho promissor.
Ao utilizar LLMs para essa finalidade, podemos dividir o uso em dois modos:
- O LLM atua como um transpilador da linguagem de altíssimo nível para uma linguagem de nível inferior. Isso significa que o LLM criará a implementação da abstração.
- O LLM atua como o runtime da linguagem de altíssimo nível. Isso significa que o LLM executará o comportamento representado pela abstração.
Se encapsularmos o LLM em uma função de granularidade grossa, dependendo de sua implementação, ele pode ser um transpilador ou um runtime.
Ao atuar como transpilador, o código gerado pelo LLM é feio e ultrapassa facilmente a capacidade humana de revisão —se tudo o que eu preciso é de um lixo que funcione, então seria melhor simplesmente usar projetos de código aberto. Ao atuar como runtime, a eficiência de execução do LLM é baixa e carece da garantia de ter o mesmo comportamento ao executar repetidamente a mesma categoria de tarefas.
Atualmente, as capacidades dos LLMs são tão limitadas que usá-los para esses fins pode criar mais problemas do que resolver. Se os LLMs fossem metade tão poderosos quanto os provedores de modelos propagandeiam, implementações de código aberto substitutas diretas do pacote Adobe e do pacote Office já teriam sido criadas, tal como o Linux foi para o Unix.
O Paradoxo do Conhecimento
Quanto mais se quer que o LLM atue como um transpilador, mais se esbarra neste problema: é impossível garantir que a abstração construída pelo LLM corresponda às nossas expectativas sem revisar minuciosamente o código gerado. Mas se o revisor possuir conhecimento suficiente, poderá descobrir que é mais eficiente escrever o código ele mesmo do que revisar o código gerado. O mais provável é que o revisor já tenha resolvido problemas semelhantes há muito tempo, e foi justamente assim que adquiriu tal conhecimento. Nesse caso, escrever o código é muito simples para o revisor, tão simples que ele não consegue entender por que usaria uma camada intermediária adicional como um LLM para degradar a qualidade do resultado.
Esse paradoxo pode ser resumido da seguinte forma:
- Se o revisor não possui conhecimento suficiente para revisar o código gerado (no estado ideal, obviamente conhecimento zero), então ele não está fazendo engenharia de software — não há como colocar isso em produção.
- Se o revisor possui conhecimento suficiente para revisar o código, então é bem provável que ele não precise do código gerado por um LLM.
O TDD não consegue resolver esse paradoxo e, na verdade, cria ainda mais problemas:
- Testes podem ser muito difíceis de escrever; criar testes suficientemente exaustivos consome mais tempo do que revisar o código gerado.
- A menos que as abstrações sejam tão limpas quanto funções puras ou que os testes sejam tão distantes quanto os testes ponta a ponta (E2E), é impossível escrever testes significativos sem conhecimento suficiente.
- Atualizações posteriores podem fazer com que uma quantidade enorme de testes falhe, e atualizar esses testes é igualmente demorado e trabalhoso.
- Na realidade, existem cenários que são difíceis de testar de forma automatizada, ou até mesmo impossíveis de automatizar por completo.
Quando o TDD complica tanto as coisas, você não quer mais praticar o TDD manualmente, e o desfecho inevitável é usar testes gerados por LLMs para assegurar o código gerado por LLMs —caindo finalmente na situação de ter que revisar uma quantidade massiva de código de teste gerado (pelo menos várias vezes maior que o código testado), sendo o único ganho o fato de que o conhecimento necessário para revisar o código de teste talvez seja comparativamente menor.
Isso rebaixa em grande parte o LLM de um implementador de abstrações para um mero usuário de abstrações. Suas funções ficam confinadas à geração de código de cola (glue code) fácil de revisar, distanciando-se consideravelmente do que esperamos de uma linguagem de altíssimo nível.
Fingindo o Sucesso
Façamos um experimento mental fingindo que os LLMs já se desenvolveram até o ápice. Por conveniência, vamos chamá-lo de LLM Definitivo. Agora, esqueçamos temporariamente se o LLM Definitivo é teoricamente viável.
Atuando como transpilador, o LLM Definitivo seria capaz de escrever códigos perfeitos, de modo que revisar o resultado da transpilação seria pura perda de tempo. Atuando como runtime, a eficiência de execução do LLM Definitivo seria extremamente alta, apresentando sempre o mesmo comportamento ao executar tarefas da mesma categoria repetidamente.
Os problemas de programação estariam resolvidos agora? Ainda não, porque ainda não passamos a tarefa para o LLM Definitivo. A menos que a tarefa seja tão superficial quanto um CRUD típico, ela pode não existir no conjunto de dados do LLM Definitivo. Se a tarefa tiver surgido ou sido atualizada após o término do treinamento do LLM Definitivo, ele poderá enfrentar dificuldades durante a execução. Isso não significa que a interpolação ou extrapolação do LLM Definitivo seja totalmente incapaz de lidar com isso, mas sim que ele se degradará para os níveis ruins anteriores nessas tarefas.
Contudo, já que ele é o LLM Definitivo, é bem provável que já tenha superado os obstáculos do aprendizado em contexto (in-context learning). Isso significa que as deficiências do LLM Definitivo poderiam ser supridas injetando conhecimento no contexto, e o problema então passaria a ser como organizar esse conhecimento.
Além disso, mesmo que o LLM Definitivo tenha provado ser competente para determinada tarefa, você ainda pode querer preparar conhecimento para ela. Os motivos podem ser variados, como pura falta de confiança, facilidade para que humanos assumam o controle no meio do caminho, logging estruturado, conformidade corporativa, etc.
Suponhamos que tenhamos uma tarefa de automação; tarefas de automação sempre têm as seguintes características:
- Quase certamente trazem ganhos diretos de produtividade, o que nos dá motivação suficiente para implementá-las.
- Dependem de conhecimento altamente específico, de modo que não podemos relaxar na entrega do conhecimento; caso relaxemos, o LLM Definitivo sofrerá uma queda vertiginosa de desempenho.
Primeiro, tentemos gravar um vídeo de demonstração da tarefa e entregar esse vídeo como contexto para o LLM Definitivo. Isso pressupõe que o conhecimento dessa tarefa de automação possa ser expresso com clareza por meio de um vídeo de demonstração. Infelizmente, isso ainda causará problemas em tarefas suficientemente complexas, pois tarefas com essa complexidade se deparam com casos de borda imprevistos. Portanto, fatalmente definiremos um comportamento padrão de contingência: ao encontrar uma situação não registrada no vídeo, chamar um humano para resolver. A cada novo caso de borda encontrado, você precisará adicionar um novo vídeo no ponto apropriado ou editar e inserir um novo trecho no vídeo original. No entanto, a essa altura, é bem provável que já tenha sido desenvolvida uma ferramenta que nos permita organizar o contexto visualmente e depois compilá-lo no contexto final, o que trará muita conveniência. Contudo, se houver muitos casos de borda, ou se a resolução de um caso de borda gerar novas situações, organizar o conhecimento por vídeo será francamente ineficiente. Se no futuro ainda houver a possibilidade de mudanças drásticas no fluxo (o que, por experiência, sempre acontece), organizar o conhecimento por vídeo mostrar-se-á insustentável demais.
Considerando que se trata do LLM Definitivo, ele deveria ser capaz de assimilar conhecimento por meio de texto e imagens; portanto, passamos a usar texto e imagens no lugar de vídeos. Como o texto e as imagens fornecem uma densidade de informação menor do que o vídeo, passam a existir exigências quanto ao nível textual e lógico de quem organiza o conhecimento. Os humanos adoram a preguiça, e o LLM Definitivo pode gerar textos e editar imagens; logo, uma proporção considerável dos textos será gerada pelo LLM Definitivo, e as imagens serão editadas por ele a partir de imagens maiores, restando aos humanos apenas revisar esses conteúdos gerados. Não é difícil notar que, devido à existência de ferramentas visuais, regredimos neste momento para uma combinação de linguagem natural com programação visual.
O principal problema anterior da programação visual era a baixa eficiência e a péssima legibilidade ao expressar primitivas de programação por meio de gráficos, encontrando dificuldades em quase tudo além do fluxo de controle mais básico (virando, literalmente, código espaguete). A programação visual potencializada pela linguagem natural teria resolvido esses problemas? Imagino que tenha resolvido uma parte, mas a linguagem natural continuará enfrentando problemas de falta de expressividade em certas áreas; portanto, a parte restante que é difícil de expressar em linguagem natural ainda precisará recuar para linguagens de domínio específico, ou seja, na forma de código. Nesse ponto surge um paradoxo, fazendo-nos encarar o mesmo dilema da programação visual atual: se de tempos em tempos é preciso regredir para a forma de código, por que não manter a consistência e escrever tudo logo em forma de código?
Assim, a combinação de linguagem natural e programação visual colapsa ainda mais na forma de código de uma linguagem de altíssimo nível. Arquivos de mídia como vídeos e imagens agora existem apenas como recursos referenciados pelo código, enquanto a linguagem natural e a linguagem de programação passam apenas a se complementar mutuamente dentro da linguagem de altíssimo nível.