Como arruinar um projeto com excesso de reflexão, expansão de escopo e diff estrutural
(kevinlynagh.com)- Projetos tendem a se dividir entre o fluxo de simplesmente fazer e terminar logo e o fluxo em que pesquisa e design crescem até fazer você perder o problema original; na prática, muitas vezes simplesmente tentar fazer avança mais
- Mesmo ao criar uma busca fuzzy de caminhos para Emacs, recursos extras de uma boa biblioteca geraram novas exigências e inflaram o design; no fim, ao descartar todo o código de ancoragem que não era necessário, a lição de YAGNI voltou a se confirmar
- Em diffs de código, a comparação por linha não captura bem estruturas de nível superior como funções ou tipos, e mesmo ferramentas baseadas em treesitter podem ficar difíceis de ler se o pareamento entre entidades falhar entre versões, exibindo longos blocos como exclusões e adições
- A direção necessária é primeiro criar uma ferramenta de escopo mínimo voltada para revisão por turno da saída de LLM, começando com extração de entidades em Rust e pareamento simples para ver rapidamente um resumo de mudanças em alto nível
Excesso de reflexão e expansão de escopo
- Projetos tendem a se dividir entre o fluxo de simplesmente fazer e terminar logo e o fluxo em que, ao mergulhar em casos anteriores, o escopo cresce e no fim o problema original deixa de ser resolvido
- Uma prateleira de cozinha feita no fim de semana foi planejada tomando café, teve um suporte impresso em 3D ajustado algumas vezes e ficou pronta dentro do próprio fim de semana com materiais e tinta que já sobravam
- O CAD do suporte para o bin da Ikea está publicado em OnShape CAD
- Os materiais reaproveitaram sobras da bancada de trabalho, e os cantos foram lixados no olho com uma palm sander
- Nessa prateleira, o principal critério de sucesso não era fazer algo perfeitamente ajustado à cozinha, mas curtir marcenaria com um amigo, o que reduziu a necessidade de pensar demais em critérios detalhados
- Em contraste, na busca por uma ferramenta de diff estrutural, o resultado de difftastic deixou a desejar, então houve 4 horas de pesquisa sobre ferramentas e workflows relacionados, até voltar ao critério original: um workflow de diff melhor para usar no Emacs
- Interesses antigos como interfaces para prototipagem de hardware, uma linguagem misturando Clojure e Rust, e uma linguagem para CAD consumiram centenas de horas em pesquisa de base e pequenos protótipos, mas ainda não viraram algo que resolva diretamente a motivação inicial
- A interface de prototipagem de hardware apareceu em setembro de 2023, o design de linguagem em novembro de 2023, e as ideias ligadas a CAD seguem por constraints, bidirectional editing e other dubious ideas
- Nos projetos de linguagem e CAD, os critérios de sucesso são nebulosos: substituir Rust ou Clojure, atacar só parte do problema, bastar como playground de aprendizado, trocar um CAD comercial, ou ainda precisar ser útil para outras pessoas
- Examinar essas perguntas tem valor, mas a visão aqui é que fazer muita coisa de verdade costuma ser melhor do que apenas analisar muita coisa
- Mesmo que, olhando depois, o resultado claramente não seja bom, simplesmente tentar fazer acaba levando mais longe no geral
A lei da conservação da expansão de escopo
- O tempo de sair construindo sem pensar também tem limites e exige equilíbrio, e a experiência de fazer um LLM agent escrever muito código para depois jogar tudo fora trouxe YAGNI de volta à cabeça
- A ideia era criar uma busca fuzzy de caminhos em todo o filesystem no estilo do Finda para usar no Emacs; como essa mesma função já havia sido implementada à mão antes, parecia plausível terminar tudo em poucas horas supervisionando um LLM
- No começo, numa conversa de planejamento, veio a recomendação de Nucleo, que parecia bem projetado e bem documentado, então foi adotado para obter smart case e Unicode normalization
- Por exemplo, a query
foocasa tanto comFooquanto comfoo, masFoonão casa comfoo - O tratamento de
cafeecaféentra no mesmo contexto
- Por exemplo, a query
- O problema não era a boa biblioteca em si, mas o fato de o Nucleo também suportar ancoragem
- Como, num corpus só de caminhos de arquivo, uma âncora de início de linha parecia inútil, surgiu a tentativa de reinterpretá-la como âncora por segmento de caminho
- Por exemplo, a ideia era que
^foocasasse com/root/foobar/, mas não com/root/barfoo/
- Por exemplo, a ideia era que
- Para tratar isso de forma eficiente, o índice precisaria armazenar limites de segmento e permitir verificar a query rapidamente em cada segmento
- Além disso, seria preciso lidar com queries ancoradas contendo barra, como
^foo/bar, e só a checagem por segmento já não bastaria para casar corretamente caminhos como/root/foo/bar/baz/ - Mais algumas horas foram gastas nesse design, trocando ideias com o LLM e escrevendo código que encapsulava os tipos do Nucleo, até que o código ficou grande demais e desagradou, levando à reescrita manual de um wrapper menor
- Depois de uma pausa, veio a constatação de que não havia lembrança de realmente precisar de ancoragem no Finda, e que num corpus de caminhos muitas vezes basta colocar
/no início ou no fim da query para cobrir a maior parte desse papel- A única exceção restante é a âncora para fim de nome de arquivo
- No fim, todo o código de ancoragem foi descartado, e é difícil ter certeza se ainda assim houve ganho em relação a ter escrito tudo diretamente desde o começo, sem LLM nem discussões com outras pessoas
- Parece existir uma espécie de lei da conservação segundo a qual, quanto mais a velocidade de programação aumenta, mais também aumentam funcionalidades desnecessárias, rabbit holes e desvios
Diff estrutural
- Em código, diff normalmente significa um resumo de mudanças por linha entre duas versões de um arquivo, e na visualização unified adições e remoções aparecem com
+e- - O mesmo diff também pode ser renderizado em comparação lado a lado, e quanto mais complexa a mudança, mais fácil esse formato pode ficar de ler
- O problema do diff por linha é que ele não reconhece estruturas de nível superior como funções ou tipos; se as chaves por acaso “fecham”, a exibição pode omitir que as linhas pertencem a funções diferentes
- difftastic tenta reduzir esse problema usando a concrete syntax tree fornecida pelo treesitter, mas o pareamento de entidades entre versões nem sempre funciona bem
- No diff que motivou tudo isso,
struct PendingClicknão foi reconhecida como correspondente entre os dois lados, aparecendo como removida à esquerda e adicionada à direita - Sem investigar a fundo por que o pareamento falhou, a conclusão foi que seria melhor ver
PendingClickRequestePendingClickcorrespondendo entre os dois lados, mesmo que isso tornasse o diff total mais longo
Ferramentas e referências de diff estrutural
- A ferramenta de semantic diff mais acabada e cuidadosamente polida, na avaliação do autor, é semanticdiff.com
- Ela é oferecida por uma pequena empresa alemã, com plugin gratuito para VSCode e um web app que mostra diff de PRs no GitHub
- Porém, não oferece uma biblioteca de código que possa servir de base para o workflow desejado
- O texto semanticdiff vs. difftastic tem muitos detalhes úteis, incluindo o problema de o difftastic não mostrar nem mesmo mudanças significativas de indentação em Python
- Em um comentário no HN, um dos autores disse que abandonou o uso de treesitter para tratar semântica, relatando que parsing podia falhar por causa de palavras-chave dependentes de contexto e comportamento do lexer, a ponto de a ferramenta travar quando nomes como
asynceram usados como parâmetro
- diffsitter é baseado em treesitter e inclui um servidor MCP
- Tem bastante estrela no GitHub, mas a documentação não pareceu especialmente boa, e foi difícil encontrar materiais explicando como funciona
- A wiki do difftastic diz que ele executa longest-common-subsequence sobre as folhas da árvore
- gumtree é uma ferramenta surgida de pesquisa acadêmica em 2014
- Como exige Java, não combina com o uso pessoal de algo rápido para acionar dentro do Emacs
- mergiraf é um merge-driver em Rust baseado em treesitter
- A architecture overview é bem organizada, e internamente ele usa o algoritmo Gumtree
- Pela documentação e pelos diagramas, passa a impressão de um projeto escrito com bastante cuidado
- O autor do semanticdiff.com escreveu num comentário no HN que o GumTree entrega resultados rapidamente, mas mesmo com melhorias propostas em artigos posteriores ainda retornava pareamentos ruins com frequência considerável, o que acabou levando a uma abordagem baseada em Dijkstra para minimizar o custo do mapeamento
- weave é outro merge-driver em Rust baseado em treesitter
- O conjunto da obra — landing page chamativa, muitas estrelas no GitHub, servidor MCP — pareceu um tanto exagerado
- Houve uma olhada também no crate de extração de entidades sem
- O código central de diff parece razoável, mas um pouco verboso, e o pareamento de entidades usa um algoritmo guloso
- O modelo de dados não detecta movimentações dentro do arquivo, embora esse tipo de mudança possa ser importante
- Há também muita análise de impacto baseada em heurísticas que parece exigir integrações de linguagem mais fortes para inspirar confiança
- Ao executar
sem diff --verbose HEAD~4, apareceu inclusive uma saída com bug marcando como alteradas linhas que na prática não tinham mudado
- Ao executar
- Havia funções potencialmente úteis demais, num estado que parecia 80% pronto, então não serviu como base; ainda assim, fazer tudo isso em 3 meses foi considerado impressionante
- diffast calcula a tree edit-distance de ASTs com base em um algoritmo descrito num artigo acadêmico de 2008
- Suporta Python, Java, Verilog, Fortran e C/C++ por meio de parsers dedicados
- A galeria de exemplos de diferenças em AST é bem organizada
- Exporta informações em forma de tuplas, úteis para uso com datalog
- autochrome é uma ferramenta de diff específica de Clojure e usa programação dinâmica
- A explicação visual e o walkthrough com exemplos são muito bons
- O texto de Tristan Hume, Designing a Tree Diff Algorithm Using Dynamic Programming and A*, é uma referência valiosa sobre projeto de algoritmos de tree diff
O workflow desejado e um plano de escopo mínimo
- O principal caso de uso é a revisão por turno da saída de LLM, e não se deixa um agent gerar de uma vez mais de 10 mil linhas de código sem controle
- A ideia é delegar ao agent tarefas com escopo definido, voltar alguns minutos depois para ver uma visão geral das mudanças e então editar manualmente no Emacs, jogar tudo fora e tentar de novo, ou até reescrever tudo por conta própria
- O workflow desejado começa vendo primeiro um resumo em alto nível de quais tipos, funções e métodos foram adicionados, removidos ou modificados
- Em seguida, deve ser possível expandir rapidamente o diff textual de cada entidade, fazendo a transição natural do resumo para o detalhe
- Também é importante poder corrigir a mudança ali mesmo, sem saltar para outro lugar, com edição inline em vez de trocar da tela de diff para a tela do arquivo
- A inspiração é transportar o workflow de revisão e staging de mudanças do Magit do nível de arquivo e linha para o nível de entidade
- Em sintonia com a lição reaprendida de escopo mínimo, o plano agora é primeiro montar às pressas um framework de extração de entidades baseado em treesitter voltado só para Rust
- O pareamento começará de forma simples, com uma estratégia gulosa, e o diff será renderizado na linha de comando
- Se isso já produzir resultados melhores que o difftastic naquele commit específico, a próxima etapa será ligar a ideia a um workflow mais interativo no Emacs, como o do Magit
- Se possível, fica aberta até a chance de reaproveitar o próprio Magit
- Suporte a novas linguagens será adicionado apenas quando necessário
- Mais adiante, em vez do guloso simples, pode haver exploração de pareamento global baseado em pontuação
- Se o resultado ficar satisfatório, talvez seja publicado, mas juntar estrelas no GitHub ou karma no HN não é o objetivo; também pode continuar só como uma ferramenta silenciosa de uso pessoal
- Às vezes, tudo o que se quer é apenas uma prateleira, frase que amarra novamente a ideia de fazer apenas o necessário em vez de expandir demais
1 comentários
Comentários do Hacker News
Acho que isso mostra muito bem a maior dificuldade de uma pesquisa de PhD
Quando você pega um tema interessante e lê o máximo possível da bibliografia relacionada, é fácil perceber quantas coisas parecidas com o que você queria fazer já foram feitas, e aí o scope creep tende a piorar bastante
Depois de gastar toda a energia e empolgação do começo, você precisa empurrar à força os 20~30% finais para levar aquilo até um estado publicável
No dia 400, depois de praticamente explicar a teoria de tudo, você está tentando construir um dispositivo experimental em órbita no ponto de Lagrange para detectar uma partícula universal que media todas as forças do universo conhecido
Fico me perguntando como dá para aliviar isso
Na prática, é um trabalho do tipo aumentar a observabilidade de um sistema de 1% para 1,001%, e acaba sendo mais uma porta de entrada para uma carreira acadêmica
Por isso, quase nunca vejo teses que sejam realmente interessantes, muito novas ou diretamente aplicáveis à ciência
Na prática, quase nunca vi alguém pesquisar assim; normalmente o certo é ler dois ou três artigos e ir construindo a partir dali
Mergulhar fundo na literatura faz mais sentido depois que já houver algum resultado e você começar a organizar isso por escrito
Continuo pensando na ideia de que melhor já basta
Pequenas melhorias se acumulam com o tempo, e como nada nasce perfeitamente novo desde o início, ficar sentado tentando desenhar a solução perfeita tende a ser contraproducente
A ideia de que o obstáculo é o caminho também se encaixa bem aqui
Um colega com quem eu trabalhava, ao criticar mudanças de código, quando sentia que estava pegando detalhe demais, dizia: "está melhor do que antes"
Isso permitia apontar o que podia melhorar, mas ao mesmo tempo dava permissão para seguir em frente mesmo com pequenas imperfeições, e eu apoio muito essa postura
Eu costumava pensar em perfeccionismo só como a busca exagerada por realizações muito altas, mas também pode ser a incapacidade de aceitar algo que não seja perfeito, a ponto de desistir sem avançar
A procrastinação em grandes tarefas muitas vezes tem a mesma raiz
Gostei de algo que o CEO da Rec Room disse
As equipes sempre dizem que gostariam que o projeto tivesse sido mais curto; quase nunca dizem que gostariam de ter adiado mais o lançamento, deixado mais complexo e refinado mais
Isso não se aplica 100% a todas as situações, claro, mas se for para errar, acho melhor fazer menor e lançar cedo do que abrir demais a escala e desperdiçar tempo
Acho que os seres humanos, por natureza, têm facilidade para chegar a ideias parecidas, então se você conclui um projeto sem saber o que já existe, ele tende a acabar sendo uma reinvenção em algum grau
Por outro lado, se você pesquisar antes, pode perceber que aquilo já existia parcialmente e perder o entusiasmo
Mesmo assim, talvez o mais importante seja terminar de construir pelo próprio aprendizado
Claro, isso é mais difícil quando você precisa produzir resultado acadêmico novo ou lucrar com um projeto realmente único, mas até esses campos são surpreendentemente tolerantes a pequenas torções sobre o que já existe
Estou passando exatamente por isso agora em um side project
A área é Information Retrieval, então tenho pouca experiência e naturalmente existe muito prior art que posso aprender ou integrar
Depois de ler este texto, fiquei mais inclinado a primeiro construir a minha própria versão e só olhar referências anteriores quando travar ou precisar de ideias
Por outro lado, vendo o documentário recente sobre Clojure, o Rich Hickey parece ter feito justamente o oposto: passou muito tempo mergulhando em bibliografia, artigos e outras linguagens antes de começar
Mas ele também já tinha feito outras linguagens antes, então, no quadro geral, o aprendizado dele também começou construindo coisas por conta própria
Talvez a questão seja não passar tempo demais só pensando: fazer logo, aprender com a prática, bater em paredes e só então sentir necessidade de investigar mais a fundo
Depois assisti também a "Easy made Simple" e "Hammock Driven Development", e agora fiquei com vontade de aprender Clojure
Clojure documentary on CultRepo channel: https://www.youtube.com/watch?v=Y24vK_QDLFg
Simple Made Easy: https://www.youtube.com/watch?v=SxdOUGdseq4
Hammock Driven Development: https://www.youtube.com/watch?v=f84n5oFoZBc
Definir um prazo resolveu a maior parte dos meus problemas com scope creep
Pela minha experiência, projetos com deadline rígido, como game jams ou competições de programação, são fáceis de terminar, enquanto projetos com final em aberto são muito mais difíceis de concluir
Vejo isso numa linha parecida com o motivo de o padrão C++ sair a cada 3 anos, em vez de esperar até que todos os recursos desejados estejam prontos
https://news.ycombinator.com/item?id=20428703
O texto foi interessante, mas achei que as ideias do autor estavam um pouco espalhadas demais
Para alguém que diz estar sendo esmagado por scope creep, ele parece ser do tipo que faz coisa demais, a ponto de terminar o texto com uma pilha de links sobre assuntos variados
No fundo, parece alguém que realmente gosta de aprender e experimentar várias coisas, e o processo de cair em rabbit holes em si parece estimular a cabeça dele de um jeito prazeroso
Houve uma percepção que me ajudou muito como alguém que constrói coisas sozinho
A maior parte do que parecia uma abstração essencial era, na verdade, scope creep com outro nome
Eu estava adicionando flags a cada nova funcionalidade, até começar a ver um padrão no meu código e definir uma regra
Uma funcionalidade não seria lançada se não houvesse testes para o comportamento com a flag desligada
Isso me fez enxergar a flag não como rota de fuga, mas como parte do produto, e três itens do backlog simplesmente desapareceram quando passei a pensar assim
É verdade que planejamento excessivo e scope creep são problemas, mas, por outro lado, também é preciso evitar balançar demais para o lado do desenvolvimento improvisado
Alguns dos meus projetos mais bem-sucedidos foram casos em que eu modelei os dados e planejei/revisei grande parte das funcionalidades antes de produzir software de fato funcionando
Nessa etapa, muitas vezes é difícil saber o que é exagero, e se eu remover funcionalidades que eu ou o usuário provavelmente vamos querer, mais tarde acabo gastando muito tempo redesenhando o núcleo do código
Se eu errar para o outro lado, o projeto cresce demais e aí passo a chamar isso de scope creep
No fim, esse julgamento depende de quão bem você conhece o domínio
Se você conhece menos do que imagina, vai ter muito retrabalho; se conhece mais do que imagina, talvez pudesse ter ido mais longe e acabou desperdiçando tempo com baby steps
Em qualquer direção sobra arrependimento, então no fim isso parece uma grande questão de julgamento
Não dá para cair na falácia do custo afundado, e só porque você pesquisou um tema em nível de doutorado por algumas horas não significa que precisa colocar isso no projeto
Se não se encaixa exatamente no problema atual, o melhor é descartar sem dó