11 puntos por GN⁺ 10 시간 전 | 2 comentarios | Compartir por WhatsApp
  • En sistemas enormes, de decenas de millones de líneas, nadie puede tener todo en la cabeza, así que los ingenieros deben poder trabajar de forma efectiva incluso con una comprensión parcialmente correcta
  • Programming as Theory Building, de Peter Naur, sostiene que si desaparece la comprensión del equipo original, conviene desechar el programa y crear uno nuevo; pero los sistemas grandes, entrelazados con usuarios e innumerables excepciones, son difíciles de reconstruir desde cero
  • Incluso una base de código cuyos responsables ya se fueron puede revivirse si se entiende un flujo completo de punta a punta y luego se amplía con cuidado el alcance de los cambios; en las organizaciones grandes, esta reconstrucción de la comprensión ocurre una y otra vez
  • Los LLM obstaculizan la formación de modelos mentales detallados, pero al mismo tiempo ayudan a crear y aprovechar rápidamente comprensiones parciales; la colaboración, los requisitos legales, las actualizaciones de seguridad y la adopción de dependencias también exigen equilibrar la comprensión del código con otros valores
  • Un modelo mental preciso ayuda a desarrollar de forma más disfrutable y estable, pero no es un criterio absoluto; en la práctica, también es necesario renunciar a la comprensión completa por velocidad, cumplimiento legal o necesidades organizacionales

Comprensión completa y comprensión parcial

  • En bases de código pequeñas y con poca rotación de integrantes, es fácil pensar que hay que entenderlo todo para poder hacer un buen trabajo
  • En bases de código grandes y con mucha rotación, como el backend de Google Web Search o GitHub, no es posible entenderlo todo, así que cada persona trabaja intentando comprender lo mejor posible el área local que le corresponde
  • Ambos entornos difieren mucho en métodos de desarrollo, prácticas y cultura, pero en las discusiones online sobre ingeniería de software está sobrerrepresentada la primera cultura, que valora la comprensión completa
    • Los ingenieros de código abierto tienen más incentivos para compartir su trabajo por escrito, y el trabajo de ingeniería pura suele lucirse más que en los sistemas propietarios de gran escala
    • Los sistemas propietarios son difíciles de publicar por razones legales y, aun cuando se pudieran publicar, explicar una base de código grande requiere demasiado contexto específico
  • En muchos entornos de software, la comprensión parcial no es un estado incorrecto; en sistemas grandes es lo mejor que se puede alcanzar de manera realista
  • Esta diferencia deriva en el choque entre la cultura de comprensión completa y la cultura de comprensión parcial, tal como se distingue en Pure and impure software engineering

La tesis de Programming as Theory Building

  • El artículo de Peter Naur Programming as Theory Building considera que el principal producto de los programadores no es el código, sino una teoría sobre el programa
    • Esa teoría consiste en una comprensión intuitiva de qué ocurre y por qué
    • El código y la documentación solo pueden contener parcialmente esa comprensión
    • Aunque se pierda el código, un equipo que conserva la teoría puede volver a escribir el programa; pero si el equipo se reemplaza por completo y se pierde la comprensión, entender el código existente se vuelve difícil
  • Según Naur, no se puede reconstruir la teoría existente solo con documentación o código, por lo que habría que desechar el programa original y hacer que el nuevo equipo resuelva el problema desde cero
  • The Concept of Mind, de Gilbert Ryle, citado por Naur, plantea un alcance más amplio para la construcción de teorías
    • En la práctica, una teoría o know-how puede formarse de manera natural mientras se realiza algo
    • Por lo tanto, explorar el propio código y entender una base de código existente también es compatible con este enfoque

Por qué no se pueden reescribir desde cero los sistemas grandes

  • En un sistema suficientemente grande y con usuarios, se acumulan miles de casos excepcionales y comportamientos peculiares difíciles de reimplementar
  • Incluso un equipo que conoce bien el sistema no puede tener en cuenta todos los detalles al mismo tiempo, por lo que resulta difícil reescribirlo todo de una vez
  • Una reescritura exitosa avanza dividiendo la base de código existente en partes pequeñas y aisladas, y reemplazándolas una por una
    • Al final, una reescritura también es un trabajo de aplicar una serie de cambios al sistema existente
    • Si no se puede modificar el sistema existente, reemplazarlo por completo por uno nuevo es todavía más difícil

Cómo recuperar una base de código abandonada

  • En empresas tecnológicas con cientos de millones de líneas de código y miles de ingenieros, no es raro que ya no quede nadie que conozca una base de código específica
    • Basta con que algunas personas responsables renuncien en un mal momento o que la base de código pase un año sin mantenimiento para llegar a ese estado
  • Naur consideraba poco probable encargar a nuevos programadores la recuperación de un programa completamente muerto sin nadie que conociera siquiera un poco la teoría existente, pero en las organizaciones grandes esto sí ocurre en la práctica
  • Incluso una base de código abandonada puede volver a un estado en el que sea posible construir una nueva comprensión y trabajar de forma efectiva si se le dedica tiempo
    • Primero hay que entender un flujo de procesamiento de principio a fin
    • Luego, haciendo cambios con cuidado, se amplía el alcance de la comprensión desde ese flujo hacia las áreas vecinas

Todos trabajan con teorías incompletas

  • El software moderno de gran escala es tan grande que ni una persona ni siquiera un equipo entero pueden mantener todos sus comportamientos en la cabeza
  • En una base de código suficientemente grande, todos terminan trabajando con una teoría algo imprecisa del programa
  • Un ingeniero efectivo no espera a que alguien con comprensión perfecta le dé la respuesta; toma la decisión mejor fundamentada con la información disponible y responde a los resultados
  • Este tipo de trabajo requiere capacidad para tomar postura y confianza incluso en situaciones inciertas
  • La realidad de que nadie conoce por completo el comportamiento total de un producto grande es la misma situación tratada en Nobody knows how software products work

El tamaño del código en la época de Naur y en la actualidad

  • En 1985, cuando Naur escribió su artículo, es probable que el tamaño promedio de los programas fuera varios órdenes de magnitud menor que hoy
  • El primer ejemplo de programa grande que dio Naur fue un programa de monitoreo industrial de 200.000 líneas, y el segundo fue un compilador
  • La primera versión de GCC tenía alrededor de 100.000 líneas en 1987, pero para 2015 había crecido a más de 14 millones de líneas
  • Si se pueden reutilizar las pruebas existentes, un programa de 100.000 a 200.000 líneas puede reescribirse con relativa facilidad; pero es difícil aplicar el mismo criterio a sistemas de más de 1 a 2 millones de líneas

La doble cara de los LLM y la construcción de teorías

  • Los LLM suelen recibir críticas como herramientas deficientes porque obstaculizan el proceso general de construcción de teorías
  • Sin embargo, como ocurre con otras herramientas de software, los LLM también tienen una doble cara
    • Pueden hacer más difícil crear un modelo mental detallado del software
    • Permiten construir teorías parciales con rapidez
    • Pueden ayudar a trabajar de forma más efectiva incluso sobre la base de una comprensión incompleta
  • La relación entre los LLM y la comprensión del código no puede resumirse con una simple lista de pros y contras; sigue siendo un trade-off complejo que requiere criterio

Decisiones necesarias aunque dificulten entender el código

  • Además de los LLM, hay muchos otros factores que dificultan mantener una teoría precisa de una base de código
    • Permitir que otras personas escriban código en la misma base de código
    • Implementar funcionalidades exigidas por la ley, como accesibilidad o protección de datos
    • Permitir que colegas renuncien o se cambien de equipo
    • Actualizar versiones de software por parches de seguridad
    • Introducir bibliotecas u otras dependencias
  • No se puede juzgar una herramienta o práctica como mala solo porque interfiera con la construcción de teorías
  • La comprensión de la base de código es uno de varios valores de ingeniería, al igual que la legibilidad, la mantenibilidad y la corrección
    • Según el contexto, se puede priorizar una comprensión precisa sacrificando otros valores
    • A la inversa, también se puede ceder en la comprensión del código por velocidad, cumplimiento legal o razones políticas dentro de la organización
  • La objeción de que entender el código es clave para hacer realidad todos los demás valores también puede aplicarse del mismo modo a la legibilidad, la mantenibilidad y la corrección; en la práctica, incluso estos valores centrales se negocian constantemente

Preferencias personales y responsabilidades laborales

  • En especial, los ingenieros puros suelen preferir trabajar manteniendo un modelo mental preciso
    • El desarrollo resulta más divertido y menos estresante
    • Se siente más cercano a lo que ellos consideran verdadera ingeniería
  • Muchos ingenieros crean pequeños proyectos open source en solitario durante su tiempo libre precisamente porque pueden trabajar manteniendo una teoría al estilo de Naur precisa sobre la base de código
  • En el trabajo, hay que seguir el conjunto de valores que la organización paga y exige, más que los valores personales de ingeniería
    • Aunque se valore el rendimiento, puede tocar escribir código más lento para cumplir un cronograma o aceptar requisitos difíciles
    • La comprensión completa de una base de código tampoco es un estándar absoluto que deba preservarse siempre, sino una opción que puede intercambiarse por otros valores según los objetivos del trabajo

2 comentarios

 
ndrgrd 49 분 전

Por eso uno se obsesiona con la abstracción.

 
Comentarios en Lobste.rs
  • Lamentablemente, el título del texto original se acerca a un clickbait, y el contenido real va más bien por el lado de que hay que poder avanzar aun entendiendo solo parcialmente una base de código grande.
    Desde la perspectiva de construir una teoría, tampoco entra en conflicto con el punto del artículo si se considera que una teoría tiene tanto amplitud como profundidad. La amplitud significa hasta qué alcance del sistema se entiende lo suficiente como para responder preguntas y hacer cambios con soltura; la profundidad, qué tan complejas son las preguntas que se pueden responder sobre una parte específica y qué tan complejos son los cambios que se pueden hacer manteniendo la integridad.
    Si se reescribe el código módulo por módulo con un período de transición, en la práctica se está construyendo una teoría estrecha pero profunda sobre ese módulo y trabajando en función de ella. En una función pequeña puede estar bien agregar un parámetro o un valor de retorno, pero cuando la escala crece, tiene sentido empezar de nuevo desde un estado limpio aunque sea solo para un módulo específico, y conectar las demás partes para que llamen a la nueva implementación. En el fondo, el enfoque de mejorar gradualmente la corrección de una implementación con muchos bugs no suele funcionar bien, y esto a menudo también aplica a propiedades como el rendimiento.
    Dicho eso, cuando Naur dijo que “en vez de revivir el programa, habría que desechar el texto del programa existente y hacer que un nuevo equipo de programadores resuelva el problema dado desde cero”, queda abierto a discusión si quería decir que había que borrar primero el código existente antes de empezar la reescritura, o si podía desecharse después de comprobar que el nuevo programa era un reemplazo adecuado.
    Además, en la realidad, ese problema dado en sí mismo no está dado. Naur supone que la definición original del problema sigue existiendo, pero en bases de código grandes muchas veces no es así. Lo más cercano suele ser la suite de pruebas, por lo que se pueden reemplazar componentes mientras se sigue haciendo que las pruebas pasen.
    En esa época tampoco existía el despliegue continuo. Hoy, mientras el servicio sigue ejecutándose, hay que atender guardias, responder preguntas y resolver bugs urgentes, así que el cálculo de costos y beneficios de una reescritura también cambia.

  • Esta objeción tiene partes bien expresadas, pero la mayor parte se siente obvia. En sistemas lo suficientemente grandes, la comprensión parcial es el único estado posible, pero una organización siempre debería buscar una comprensión más profunda.
    Más allá de la lógica de “te pagan por trabajar”, el software a gran escala con frecuencia no logra satisfacer a los usuarios ni cumplir objetivos de negocio. También se puede decir que Windows y OS X se están pudriendo por la complejidad acumulada.
    Si la gerencia pudiera hacer desaparecer mágicamente la complejidad, lo haría. Pero este es un problema de ingeniería de software aún no resuelto, y más que querer lanzar mal código, la gerencia parece haber aceptado ese compromiso bajo las condiciones actuales.
    También se podría pensar “si tienes un monopolio, alcanza, así que puedes sacar software malo y lleno de bugs”, pero eso es demasiado nihilista. Hay muchas empresas de software que no son monopolios o que no pueden retener a los usuarios por la fuerza, así que en esos casos la calidad sí importa de verdad.

    • En mi experiencia, la gerencia desconfía de las buenas prácticas de desarrollo de software y suele pedir que se abandonen preventivamente para moverse más rápido. La mayoría toma esa decisión solo con suposiciones, sin validarlas.
    • El objetivo de “siempre hay que entender más” puede usarse mal hasta llegar a un estado de no poder hacer nada. Si te exigen comprensión perfecta en vez de comprensión suficiente, no puedes hacer nada y terminas rindiéndote o desesperándote.
      Bajo esa interpretación, el concepto de comprensión también se parece a una especie de mecanismo de gatekeeping.
  • El enfoque descrito en el artículo está estrechamente relacionado con la capacidad de razonar localmente sobre partes del código.
    El razonamiento local, es decir, entender una parte de una base de código grande sin comprender todo lo que la rodea, ha sido un concepto central que la informática ha perseguido desde el principio.
    Una de las principales ventajas de la programación estructurada era que permitía el razonamiento local, y la recomendación de evitar variables globales también venía en gran medida del mismo objetivo. La programación funcional elimina efectos secundarios para permitir el razonamiento local, y una de las ideas de la programación orientada a objetos, combinar estructuras de datos con el código que las manipula, también ofrece otro medio para razonar localmente.
    El verdadero poder de estos conceptos básicos está en que, aunque no sea la situación ideal, permiten razonar y trabajar sobre partes pequeñas sin entender toda la base de código.

  • Lo leí como una refutación interesante al consejo de que “hay que entender cada línea de la base de código”, y como el argumento central del artículo se conecta con las prácticas de desarrollo, le puse la etiqueta #practices.
    Sé que a esta comunidad no le gustan los LLM, pero dudo que este artículo realmente corresponda a #vibecoding.

    • Tal vez ya lo sepas, pero en lobste.rs todo el contenido relacionado con el uso de LLM se clasifica ampliamente como vibecoding; no estoy de acuerdo, pero esa es la práctica. La pregunta central de este artículo también está estrechamente relacionada con la programación asistida por LLM.
      Dicho eso, estoy de acuerdo en que vibecoding no debería ser la única etiqueta. El artículo recién aborda explícitamente los LLM hacia el final, y el punto central ya era interesante y digno de discusión incluso antes de la era de los LLM. Por eso considero que hay fundamentos suficientes para restaurar la etiqueta practices, la restauré personalmente y recomiendo a otros hacer lo mismo.
    • El punto del artículo es relevante para todos los programadores, en especial para quienes quieren entender cómo trabajar con bases de código enormes y en qué se diferencia eso de los proyectos pequeños.
      Sigue siendo relevante incluso en un mundo donde el uso de LLM puede obstaculizar la comprensión completa de una base de código.
      Si la etiqueta significa “puede ser relevante para personas interesadas en el vibe coding”, entonces claramente es adecuada. En cambio, si significa “no es relevante salvo para personas interesadas en el vibe coding”, entonces no lo es. Todas las demás etiquetas se usan con el primer sentido, pero muchos usuarios de Lobsters usan solo esta etiqueta con el segundo, así que inevitablemente se perderán este artículo.
  • Otra característica del desarrollo moderno, distinta de 1985, es que aunque te conviertas en experto de una base de código de 300.000 líneas, la semana siguiente podrías estar trabajando en otra base de código de 300.000 líneas completamente distinta.
    Los programadores profesionales con experiencia están acostumbrados a que los manden en cualquier momento a territorio desconocido, y dependen de una combinación de modismos comunes (Google C++ Style Guide), herramientas automatizadas como compiladores que dan errores cuando se usa mal una API, y una intuición sobre “cómo debería estar organizado” un programa grande.
    También he visto discusiones del estilo “ahora que ya no hay nadie que entienda el comportamiento de toda la base de código, como en Bun reescrito con LLM, ¿cómo van a poder seguir desarrollándolo?”, pero eso se parece a preguntar cómo alguien que no es el autor puede entender una novela. Al final es código, así que basta con leerlo. Si no se entiende una función, se puede dividir en varias partes, escribir pruebas, dibujar el flujo de control en papel o probar cualquier otra cosa.
    Como uno termina moviéndose regularmente entre proyectos, tampoco hay motivo para seguir manteniendo la comprensión de una base de código antigua. En algún punto todo se mezcla de manera parecida. Aunque tengas que ayudar a que el código Scala de una startup adquirida por la empresa se comunique, mediante un protocolo RPC personalizado basado en Thrift, con un servicio Ruby basado en JSON que se está reescribiendo en Go, con una hora de búsquedas de Scala syntax reference y Thrift wire encoding puedes empezar.
    Tampoco hace falta volverse experto. Porque un mes después quizá estés depurando por qué un verificador de tipos basado en OCaml para JavaScript se cae dentro de una implementación en Go del kernel de Linux. Al final, todo es solo código.