Cómo arruinar un proyecto con pensar de más, ampliar el alcance y un diff estructural
(kevinlynagh.com)- Los proyectos suelen dividirse entre un flujo de construirlo ya y terminarlo y otro en el que la investigación y el diseño crecen hasta perder de vista el problema original; en el avance real, muchas veces simplemente hacerlo lleva la delantera
- Incluso al crear una búsqueda fuzzy de rutas para Emacs, las funciones extra de una buena librería terminaron generando nuevos requisitos y agrandando el diseño; al final se desechó por completo el código de anclajes, que no hacía falta, reafirmando YAGNI
- En los diff de código, la comparación por líneas no captura bien estructuras superiores como funciones o tipos, y hasta las herramientas basadas en treesitter pueden volverse difíciles de leer si falla el emparejamiento de entidades entre versiones y muestran largas secciones como borradas y agregadas
- La dirección necesaria es crear primero una herramienta de alcance mínimo para la revisión por turno de salidas de LLM; empezar con extracción de entidades para Rust y un emparejamiento simple, priorizando un flujo de trabajo para ver rápido un resumen de cambios de alto nivel
Pensar de más y la ampliación del alcance
- Los proyectos suelen dividirse entre un flujo de construirlo ya y terminarlo y otro en el que, al profundizar en precedentes y referencias, el alcance crece tanto que al final no se resuelve el problema original
- Un estante de cocina hecho en un fin de semana se completó dentro de ese mismo fin de semana: se definió el diseño mientras se tomaba café, se ajustó varias veces un colgador impreso en 3D y se usaron materiales sobrantes y pintura que ya había
- El CAD para el colgador del bin de Ikea está publicado en OnShape CAD
- Los materiales se reutilizaron de lo que había sobrado en el banco de trabajo, y las esquinas se suavizaron a ojo con una palm sander
- En ese estante, el principal criterio de éxito no era fabricar algo perfectamente ajustado a la cocina, sino disfrutar la carpintería con un amigo; eso redujo la necesidad de obsesionarse con criterios demasiado detallados
- En cambio, durante la búsqueda de una herramienta de diff estructural, los resultados de difftastic dejaron insatisfecho al autor, que pasó 4 horas investigando herramientas y flujos de trabajo relacionados, para terminar volviendo al criterio original: un mejor flujo de diff para usar desde Emacs
- Intereses de largo tiempo como una interfaz para prototipado de hardware, un lenguaje que mezcle Clojure y Rust, o un lenguaje para CAD, han consumido cientos de horas en investigación previa y pequeños prototipos, pero todavía no han desembocado en resultados que resuelvan directamente la motivación inicial
- La interfaz para prototipado de hardware aparece en septiembre de 2023, el diseño de lenguajes en noviembre de 2023, y las ideas relacionadas con CAD continúan en constraints, bidirectional editing y other dubious ideas
- En los proyectos de lenguaje y CAD, los criterios de éxito son difusos: si deberían reemplazar Rust o Clojure, si solo deberían cubrir algunos problemas, si basta con que sean un playground de aprendizaje, si deberían sustituir un CAD comercial o si también tendrían que ser útiles para otras personas
- Revisar estas preguntas tiene valor, pero parece mejor construir mucho en la práctica que quedarse evaluando demasiadas cosas
- Incluso si con el tiempo salen resultados claramente imperfectos, en términos generales simplemente hacerlo termina adelantando más el proceso
La ley de conservación de la ampliación del alcance
- También hay límites para el tiempo dedicado a construir sin pensar demasiado, y hace falta equilibrio; después de escribir mucho código con agentes LLM para luego tirarlo todo, volvió a aparecer la idea de YAGNI
- La intención era crear una búsqueda fuzzy de rutas de todo el sistema de archivos, estilo Finda, para usar desde Emacs; como antes ya se había implementado algo parecido a mano, parecía posible terminarlo en unas pocas horas supervisando al LLM
- Al principio, durante una conversación de planificación, se recomendó Nucleo, y como estaba bien diseñada y documentada, se adoptó para aprovechar funciones como smart case y Unicode normalization
- Por ejemplo, la consulta
foocoincide tanto conFoocomo confoo, peroFoono coincide confoo - El manejo de
cafeycaféva en la misma línea
- Por ejemplo, la consulta
- El problema no era la buena librería en sí, sino que Nucleo también soportaba anclajes
- Como en un corpus formado solo por rutas de archivos los anclajes al inicio de línea parecían poco útiles, se intentó reinterpretarlos como anclajes por segmento de ruta
- Por ejemplo, se quería que
^foocoincidiera con/root/foobar/, pero no con/root/barfoo/
- Por ejemplo, se quería que
- Para procesarlo con eficiencia, el índice tenía que guardar los límites entre segmentos y permitir verificar consultas rápidamente para cada segmento
- A eso se sumó la necesidad de manejar consultas ancladas con
/, como^foo/bar; con una verificación por segmento ya no resultaba fácil emparejar correctamente rutas como/root/foo/bar/baz/ - Se invirtieron varias horas más en ese diseño, intercambiando ideas con el LLM, creando código que envolvía los tipos de Nucleo y, al final, al ver que el código había crecido demasiado y no convencía, se reescribió un wrapper más pequeño desde cero
- Después de descansar, cayó en la cuenta de que no recordaba haber necesitado nunca anclajes en Finda, y de que en un corpus de rutas, agregar
/al inicio o al final de la consulta cubría la mayoría de los usos de anclajes- La única excepción que quedaba era el anclaje al final del nombre de archivo
- Al final se eliminó por completo todo el código relacionado con anclajes, y sigue siendo difícil saber si aun así hubo una ganancia frente a haberlo escrito directamente desde el principio sin discusiones con LLM ni con otras personas
- Cuanto más aumenta la velocidad de programación, parece existir una especie de ley de conservación por la cual también aumentan las funciones innecesarias, los rabbit holes y los desvíos
Diff estructural
- En código, un diff suele referirse a un resumen de cambios por líneas entre dos versiones de un archivo; en la vista unified, las adiciones y eliminaciones se marcan con
+y- - Ese mismo diff también puede renderizarse en formato de comparación lado a lado, y cuanto más complejos son los cambios, más fácil puede ser leerlos así
- El problema del diff por líneas es que no reconoce estructuras superiores como funciones o tipos; si las llaves llegan a cuadrar de alguna manera, incluso puede omitir marcas aunque pertenezcan a funciones distintas
- difftastic intenta reducir ese problema usando el concrete syntax tree que proporciona treesitter, pero el emparejamiento de entidades entre versiones no siempre sale bien
- En el diff que motivó directamente esta exploración,
struct PendingClickno se correspondía entre ambos lados, y aparecía como borrado a la izquierda y agregado a la derecha - Sin investigar a fondo por qué falló el emparejamiento, se concluyó que sería mejor ver
PendingClickRequestyPendingClickcomo entidades emparejadas entre ambos lados, incluso si eso alargaba el diff completo
Herramientas de diff estructural y materiales de referencia
- Entre las herramientas de semantic diff más pulidas y cuidadosamente refinadas, destaca semanticdiff.com
- La ofrece una pequeña empresa alemana, con un plugin gratuito para VSCode y una web app que muestra diff de PR de GitHub
- Sin embargo, no ofrece una librería de código que sirva como base para el flujo de trabajo buscado
- El artículo semanticdiff vs. difftastic tiene muchos detalles útiles, incluido el problema de que difftastic no logra mostrar ni siquiera cambios de indentación significativos en Python
- Uno de sus autores comentó en HN que dejaron de usar treesitter para el procesamiento semántico, y explicó que, por palabras clave dependientes del contexto y el comportamiento del lexer, el parseo podía fallar al punto de hacer que la herramienta se detuviera incluso si se usaba un nombre como
asynccomo parámetro
- diffsitter está basado en treesitter e incluye un servidor MCP
- Tiene muchas estrellas en GitHub, pero su documentación no parecía particularmente buena, y fue difícil encontrar materiales que explicaran cómo funciona
- En la wiki de difftastic se indica que realiza longest-common-subsequence sobre las hojas del árbol
- gumtree es una herramienta con origen en investigación académica de 2014
- Requiere Java, así que no encaja con el uso personal de una herramienta rápida para Emacs
- mergiraf es un merge-driver basado en treesitter, escrito en Rust
- Su architecture overview está muy bien organizada, y por dentro usa el algoritmo de Gumtree
- Tanto la documentación como los diagramas dan la impresión de un proyecto escrito con mucho cuidado
- El autor de semanticdiff.com comentó en HN que GumTree produce resultados rápido, pero que incluso aplicando mejoras propuestas en varios artículos posteriores seguía devolviendo con bastante frecuencia emparejamientos malos, por lo que terminaron cambiando a un enfoque basado en dijkstra que minimiza el costo del mapeo
- weave es otro merge-driver basado en treesitter, escrito en Rust
- Su landing page vistosa, la gran cantidad de estrellas en GitHub y el servidor MCP daban una impresión general algo exagerada
- Se revisó el crate de extracción de entidades sem
- El código central del diff está bien, pero resulta algo verboso, y el emparejamiento de entidades usa un algoritmo greedy
- Su modelo de datos no detecta movimientos dentro del archivo, aunque esos movimientos pueden ser importantes
- También incluye bastante análisis de impacto heurístico que, para ser confiable, parecería necesitar una integración más fuerte con cada lenguaje
- Al ejecutar
sem diff --verbose HEAD~4, incluso apareció una salida con bugs donde líneas que en realidad no habían cambiado se marcaban como modificadas
- Al ejecutar
- Tiene demasiadas funciones hipotéticamente útiles al 80% de avance como para servir bien de base, aunque se valora mucho haber llegado tan lejos en solo 3 meses
- diffast calcula la tree edit-distance de un AST a partir de un algoritmo de un artículo académico de 2008
- Soporta Python, Java, Verilog, Fortran y C/C++ mediante parsers dedicados
- La example AST differences gallery está muy bien organizada
- Puede exportar la información en forma de tuplas para usarla en datalog
- autochrome es una herramienta de diff exclusiva para Clojure y usa dynamic programming
- Su explicación visual y el walkthrough con ejemplos son excelentes
- Designing a Tree Diff Algorithm Using Dynamic Programming and A* de Tristan Hume es una lectura muy valiosa sobre diseño de algoritmos de tree diff
Flujo de trabajo deseado y plan de alcance mínimo
- El caso de uso principal es la revisión por turno de salidas de LLM, y no se deja que el agente genere de una sola vez más de 10 mil líneas de código sin control
- Se le asignan tareas de alcance definido al agente, se vuelve unos minutos después para mirar una visión general de todos los cambios, y luego se quiere poder corregir directamente en Emacs, desecharlo todo y volver a intentar, o incluso reescribirlo personalmente desde cero
- El flujo deseado empieza por ver primero un resumen de alto nivel de qué tipos, funciones o métodos fueron agregados, eliminados o modificados
- Encima de eso, debería poder desplegarse rápidamente el diff textual de cada entidad y expandir el resumen hacia el diff detallado de manera natural
- También se quiere poder editar los cambios en el acto, sin tener que ir a otro lugar, es decir, con edición inline sin cambiar de la vista de diff a la vista de archivo
- La idea es trasladar el flujo de revisión de cambios y staging de Magit desde el nivel de archivo o línea al nivel de entidad
- Siguiendo la lección recuperada esta vez sobre el alcance mínimo, el plan es empezar construyendo rápidamente un framework de extracción de entidades basado en treesitter solo para Rust
- El emparejamiento comenzará con un método greedy simple, y el diff se renderizará primero en la línea de comandos
- Si con eso se logra un mejor resultado que difftastic para cierto commit, después se buscará conectarlo con un flujo de Emacs más interactivo, al estilo Magit
- Si es posible, también queda abierta la opción de reutilizar el propio Magit
- El soporte para nuevos lenguajes se agregaría solo cuando haga falta
- Más adelante también podría explorarse un emparejamiento global basado en puntajes, en lugar del enfoque greedy simple
- Si el resultado llega a ser lo bastante satisfactorio, tal vez se publique, pero juntar estrellas en GitHub o karma en HN no es el objetivo; también podría quedarse como una herramienta personal y silenciosa
- La idea se cierra con la frase de que, a veces, uno solo quiere un estante: una reafirmación de la actitud de hacer solo lo necesario en vez de expandir de más
1 comentarios
Opiniones en Hacker News
Creo que esto muestra muy bien la mayor dificultad de la investigación de doctorado
Cuando eliges un tema interesante y lees tanta bibliografía previa como sea posible, es fácil darte cuenta de cuántas cosas ya existen sobre lo que querías hacer, y el scope creep tiende a volverse grave
Después de gastar toda la energía y emoción del inicio, hay que empujar a la fuerza el 20~30% restante hasta dejarlo en un estado publicable
Para el día 400, casi ya explicaste la teoría del todo y estás intentando construir un dispositivo experimental en órbita del punto de Lagrange para detectar una partícula universal que medie todas las fuerzas del universo conocido
Me pregunto cómo se puede aliviar esto
En la práctica, suele ser un trabajo como aumentar la observabilidad de un sistema del 1% al 1.001%, y se parece más a una puerta de entrada para una carrera académica
Por eso casi nunca veo tesis que sean realmente interesantes, enormemente novedosas o directamente aplicables a la ciencia
De hecho casi no he visto gente investigar así; normalmente lees dos o tres papers y construyes a partir de ahí
Creo que revisar la literatura en profundidad tiene más sentido después de obtener algunos resultados, cuando ya empiezas a redactar
No dejo de pensar en la idea de que algo mejor ya es suficiente
Incluso las mejoras pequeñas se acumulan con el tiempo, y como nada nace completamente perfecto, sentarse a intentar diseñar algo impecable desde el principio suele ser contraproducente
La frase de que el obstáculo es el camino también encaja muy bien aquí
Un colega con el que trabajaba, cuando criticaba cambios de código, si sentía que estaba señalando algo demasiado menor, decía: "es mejor que antes"
Así marcaba los puntos de mejora, pero al mismo tiempo daba permiso para seguir adelante aunque quedaran defectos pequeños, y apoyo mucho esa actitud
Antes pensaba que el perfeccionismo era solo perseguir logros demasiado altos de forma exagerada, pero también puede ser no aceptar nada si no es perfecto y abandonar sin avanzar
La procrastinación en tareas grandes muchas veces viene de la misma raíz
Me gustó algo que dijo el CEO de Rec Room
Los equipos siempre dicen que ojalá hubieran hecho el proyecto más corto; casi nunca dicen que ojalá hubieran retrasado más el lanzamiento, lo hubieran hecho más complejo o lo hubieran pulido más
No aplicará al 100% en todos los casos, pero si vas a equivocarte, me parece mejor hacer algo pequeño y lanzarlo temprano que expandirse demasiado y perder tiempo
Los humanos tienden por naturaleza a llegar a ideas parecidas, así que si completas un proyecto sin saberlo, al final es fácil que hasta cierto punto sea una reinvención
En cambio, si investigas antes, puedes desanimarte al darte cuenta de que en parte ya estás repitiendo algo que existía
Aun así, puede que lo más importante sea terminarlo por tu propio aprendizaje
Claro, es más difícil cuando necesitas producir un resultado académico nuevo o ganar dinero con un proyecto verdaderamente original, pero incluso en esos ámbitos suele haber más tolerancia de la que uno cree con solo torcer un poco lo existente
Justo ahora estoy pasando por esto en un side project
El área es Information Retrieval, así que tengo poca experiencia y obviamente hay muchísimo prior art del que aprender o que integrar
Después de leer esto, me inclino más por primero hacer lo mío y mirar casos previos solo cuando me atasque o necesite ideas
Por otro lado, viendo el documental reciente de Clojure, Rich Hickey parecía más bien haber trabajado después de pasar mucho tiempo profundizando en bibliografía previa, papers y otros lenguajes
Pero como él ya había creado otros lenguajes antes, al final el panorama general también partía de aprender construyendo directamente
Quizá la clave sea no quedarse pensando demasiado tiempo, sino construir primero, sacar lecciones en la práctica, chocar contra la pared y recién ahí ver si hace falta investigar más a fondo
Luego vi también "Easy made Simple" y "Hammock Driven Development", y me dieron ganas 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
Fijar una fecha límite me ha resuelto la mayoría de los problemas de scope creep
En mi experiencia, proyectos con deadlines duros como los game jams o concursos de programación son fáciles de terminar, mientras que los proyectos abiertos son mucho más difíciles de completar
Me parece similar a por qué el estándar de C++ también sale cada 3 años en vez de esperar a que estén listas todas las funciones deseadas
https://news.ycombinator.com/item?id=20428703
El texto me pareció interesante, pero el pensamiento del autor se veía un poco disperso
Para alguien que dice sentirse abrumado por el scope creep, parece una persona que de todos modos hace muchísimas cosas, al punto de llenar el final del texto con enlaces sobre toda clase de temas
Al final parece el tipo de persona a la que realmente le encanta aprender y probar de todo, y que disfruta mentalmente el proceso mismo de meterse en rabbit holes
Como alguien que trabaja solo, tuve una revelación que me ayudó bastante
La mayoría de las cosas que parecían abstracciones necesarias eran solo scope creep con otro nombre
Después de ir agregando flags a cada función, empecé a ver patrones en mi código y me puse una regla
No liberar una función si no tenía tests del comportamiento con el flag apagado
Eso me hizo ver los flags no como una vía de escape sino como parte del producto, y tres funciones que estaban en el backlog desaparecieron naturalmente en cuanto empecé a pensarlas así
Es cierto que el exceso de planificación y el scope creep son un problema, pero también hay que cuidarse de inclinarse demasiado hacia el desarrollo improvisado
Algunos de mis proyectos más exitosos fueron casos en los que modelé los datos y planifiqué y revisé la mayoría de las funciones antes de construir software que realmente funcionara
En esa etapa muchas veces no sabes bien qué es excesivo, y si quitas funciones que yo o los usuarios probablemente queríamos, luego terminas perdiendo mucho tiempo rediseñando a fondo el núcleo del código
En cambio, si te equivocas hacia el otro lado, el proyecto se vuelve demasiado grande y entonces lo llamas scope creep
Al final, este juicio depende de qué tan bien conozcas el dominio
Si lo conoces menos de lo que creías, hay mucho retrabajo; si lo conoces más de lo que pensabas, en realidad podrías haber ido a lo grande pero terminaste perdiendo tiempo con baby steps
Vayas por donde vayas, queda arrepentimiento, así que al final se siente como una gran cuestión de criterio
No hay que caer en la falacia del costo hundido, y que hayas investigado un tema a nivel doctorado durante unas horas no significa que tengas que usarlo sí o sí en el proyecto
Si no encaja exactamente con el problema actual, lo correcto es descartarlo sin miedo