11 Punkte von GN⁺ 10 시간 전 | 2 Kommentare | Auf WhatsApp teilen
  • Bei großen Systemen mit zig Millionen Zeilen kann niemand das Ganze im Kopf behalten; Engineers müssen daher auch mit einem teilweise korrekten Verständnis effektiv arbeiten können
  • Peter Naurs Programming as Theory Building argumentiert, dass man ein Programm besser verwerfen und neu schreiben sollte, wenn das Verständnis des ursprünglichen Teams verloren geht. Große Systeme, in denen Nutzer und zahlreiche Ausnahmen miteinander verflochten sind, lassen sich jedoch nur schwer von Grund auf neu aufbauen
  • Auch eine Codebase, deren Verantwortliche alle gegangen sind, kann wiederbelebt werden, wenn man zunächst einen Ablauf vollständig nachvollzieht und den Änderungsradius dann vorsichtig erweitert. In großen Organisationen findet diese Rekonstruktion von Verständnis immer wieder statt
  • LLMs behindern einerseits den Aufbau detaillierter mentaler Modelle, helfen andererseits aber dabei, schnell ein partielles Verständnis aufzubauen und zu nutzen. Auch Zusammenarbeit, rechtliche Anforderungen, Sicherheitsupdates und das Einführen von Abhängigkeiten verlangen Kompromisse zwischen Codeverständnis und anderen Werten
  • Ein korrektes mentales Modell hilft bei angenehmer und stabiler Entwicklung, ist aber kein absoluter Maßstab. In der Praxis muss man für Geschwindigkeit, rechtliche Compliance und organisatorische Anforderungen mitunter auf vollständiges Verständnis verzichten

Vollständiges und partielles Verständnis

  • Bei kleinen Codebases mit wenig Fluktuation liegt die Annahme nahe, dass man das Ganze verstehen muss, um gute Arbeit leisten zu können
  • In großen Codebases mit häufiger Fluktuation, etwa dem Backend der Google-Websuche oder GitHub, kann niemand das Ganze verstehen; daher arbeitet jede Person daran, ihren jeweiligen lokalen Bereich so gut wie möglich zu erfassen
  • Diese beiden Umgebungen unterscheiden sich stark in Entwicklungsmethoden, Praktiken und Kultur, doch in Online-Diskussionen über Software Engineering ist die erste Kultur, die vollständiges Verständnis betont, überrepräsentiert
    • Open-Source-Engineers haben einen starken Anreiz, ihre Arbeit schriftlich zu teilen, und reine Engineering-Arbeit lässt sich dort leichter sichtbar machen als in großen proprietären Systemen
    • Proprietäre Systeme lassen sich aus rechtlichen Gründen schwer veröffentlichen, und selbst wenn das möglich wäre, bräuchte es für die Erklärung großer Codebases übermäßig viel konkreten Kontext
  • In vielen Softwareumgebungen ist partielles Verständnis kein fehlerhafter Zustand, sondern bei großen Systemen realistisch gesehen das Beste, was erreichbar ist
  • Dieser Unterschied führt zu dem Konflikt zwischen einer Kultur des vollständigen Verständnisses und einer Kultur des partiellen Verständnisses, wie er in Pure and impure software engineering beschrieben wird

Die These von Programming as Theory Building

  • Peter Naurs Aufsatz Programming as Theory Building betrachtet nicht Code als das wichtigste Ergebnis der Arbeit von Programmierern, sondern eine Theorie über das Programm
    • Diese Theorie besteht aus einem intuitiven Verständnis davon, was warum passiert
    • Code und Dokumentation können dieses Verständnis nur teilweise festhalten
    • Selbst wenn der Code verloren geht, kann ein Team mit der Theorie das Programm erneut schreiben; wird das Team jedoch vollständig ausgetauscht und geht das Verständnis verloren, wird es schwierig, den bestehenden Code zu begreifen
  • Nach Naur lässt sich die bestehende Theorie nicht allein aus Dokumentation oder Code rekonstruieren; daher sollte man das ursprüngliche Programm verwerfen und ein neues Team das Problem von Grund auf neu lösen lassen
  • Gilbert Ryles The Concept of Mind, das Naur zitiert, fasst den Bereich des Theoriebildens weiter
    • Beim tatsächlichen Ausführen einer Tätigkeit können Theorie oder Know-how auf natürliche Weise entstehen
    • Daher ist auch der Prozess, eine bestehende Codebase durch Untersuchung des Codes selbst zu verstehen, mit diesem Ansatz vereinbar

Warum sich große Systeme nicht von Grund auf neu schreiben lassen

  • In hinreichend großen Systemen mit Nutzern sammeln sich Tausende von Ausnahmefällen und eigentümlichen Verhaltensweisen an, die schwer erneut zu implementieren sind
  • Selbst ein Team, das das System gut kennt, kann nicht alle Details gleichzeitig berücksichtigen, sodass es schwierig ist, das Ganze auf einmal neu zu schreiben
  • Erfolgreiche Neuschreibungen teilen die bestehende Codebase in kleine, isolierte Teile und ersetzen jeweils einen Teil nach dem anderen
    • Am Ende ist auch eine Neuschreibung eine Serie von Änderungen am bestehenden System
    • Wenn man das bestehende System nicht ändern kann, ist es umso schwieriger, es vollständig durch ein neues System zu ersetzen

Wie man eine verlassene Codebase wiederherstellt

  • In Tech-Unternehmen mit Hunderten Millionen Zeilen Code und Tausenden Engineers ist es nicht ungewöhnlich, dass niemand mehr übrig ist, der eine bestimmte Codebase kennt
    • Es kann schon reichen, dass einige Verantwortliche zu einem ungünstigen Zeitpunkt gehen oder eine Codebase ein Jahr lang nicht gewartet wird
  • Naur hielt es für unwahrscheinlich, dass neue Programmierer damit beauftragt werden, ein vollständig totes Programm ohne jemanden wiederherzustellen, der auch nur ein wenig von der bestehenden Theorie kennt; in großen Organisationen passiert genau das jedoch tatsächlich
  • Auch bei einer verlassenen Codebase kann man mit Zeit ein neues Verständnis aufbauen und sie wieder in einen Zustand versetzen, in dem effektiv daran gearbeitet werden kann
    • Zunächst versteht man einen einzelnen Verarbeitungsablauf von Anfang bis Ende
    • Durch vorsichtige Änderungen erweitert man den Verständnishorizont von diesem Ablauf aus auf angrenzende Bereiche

Alle arbeiten mit unvollständigen Theorien

  • Moderne große Software ist so umfangreich, dass weder Einzelpersonen noch ganze Teams alle Verhaltensweisen im Kopf behalten können
  • In einer hinreichend großen Codebase arbeitet jeder mit einer bis zu einem gewissen Grad ungenauen Theorie über das Programm
  • Effektive Engineers warten nicht darauf, dass jemand mit perfektem Verständnis die Antwort liefert, sondern treffen auf Basis der aktuellen Informationen die bestbegründete Entscheidung und reagieren anschließend auf die Ergebnisse
  • Diese Arbeit erfordert die Fähigkeit, auch unter Unsicherheit Position zu beziehen, sowie Selbstvertrauen
  • Die Realität, dass niemand das Gesamtverhalten eines großen Produkts vollständig kennt, entspricht der Situation, die in Nobody knows how software products work behandelt wird

Codeumfang zu Naurs Zeit und heute

  • Im Jahr 1985, als Naur seinen Aufsatz schrieb, waren durchschnittliche Programme vermutlich um mehrere Größenordnungen kleiner als heute
  • Das erste große Programmbeispiel, das Naur nennt, war ein industrielles Monitoring-Programm mit 200.000 Zeilen; das zweite Beispiel war ein Compiler
  • Die erste Version von GCC umfasste 1987 etwa 100.000 Zeilen, wuchs bis 2015 aber auf über 14 Millionen Zeilen an
  • Wenn vorhandene Tests wiederverwendet werden können, lassen sich Programme mit 100.000 bis 200.000 Zeilen vergleichsweise leicht neu schreiben; auf Systeme mit über einer oder zwei Millionen Zeilen lässt sich dieselbe Einschätzung jedoch schwer übertragen

Die zwei Seiten von LLMs und Theoriebildung

  • LLMs werden oft als schlechte Werkzeuge bewertet, weil sie den üblichen Prozess der Theoriebildung behindern
  • Wie andere Softwarewerkzeuge haben aber auch LLMs zwei Seiten
    • Es kann schwieriger werden, ein detailliertes mentales Modell der Software aufzubauen
    • Partielle Theorien lassen sich schnell erstellen
    • Sie können dabei unterstützen, auch auf Grundlage unvollständigen Verständnisses effektiver zu arbeiten
  • Die Beziehung zwischen LLMs und Codeverständnis lässt sich nicht einfach in Vor- und Nachteile aufteilen; sie bleibt ein komplexer Trade-off, der weiterhin Urteilskraft erfordert

Entscheidungen, die nötig sind, obwohl sie Codeverständnis erschweren

  • Neben LLMs gibt es viele Faktoren, die es erschweren, eine korrekte Theorie der Codebase zu bewahren
    • Anderen Personen zu erlauben, Code in dieselbe Codebase zu schreiben
    • Gesetzlich vorgeschriebene Funktionen umzusetzen, etwa für Barrierefreiheit oder Datenschutz
    • Kolleginnen und Kollegen zu erlauben, das Unternehmen zu verlassen oder in ein anderes Team zu wechseln
    • Softwareversionen wegen Sicherheitspatches zu aktualisieren
    • Bibliotheken oder andere Abhängigkeiten einzuführen
  • Ein Werkzeug oder eine Praxis lässt sich nicht allein deshalb als schlecht beurteilen, weil sie Theoriebildung behindert
  • Codebase-Verständnis ist, ebenso wie Lesbarkeit, Wartbarkeit und Korrektheit, einer von mehreren Engineering-Werten
    • Je nach Situation kann man korrektes Verständnis priorisieren und dafür andere Werte opfern
    • Umgekehrt gibt man Codeverständnis auch zugunsten von Geschwindigkeit, rechtlicher Compliance oder organisationspolitischen Gründen nach
  • Das Gegenargument, Codeverständnis sei der Schlüssel zur Verwirklichung aller anderen Werte, lässt sich genauso auf Lesbarkeit, Wartbarkeit und Korrektheit anwenden; in der Praxis werden auch diese Kernwerte ständig gegeneinander abgewogen

Persönliche Vorlieben und berufliche Verantwortung

  • Besonders reine Engineers bevorzugen es, mit einem korrekten mentalen Modell zu arbeiten
    • Die Entwicklung macht mehr Spaß und ist weniger stressig
    • Es fühlt sich näher an dem an, was sie selbst als echtes Engineering betrachten
  • Viele Engineers bauen in ihrer Freizeit allein kleine Open-Source-Projekte, weil sie dabei eine korrekte Naur’sche Theorie über die Codebase aufrechterhalten können
  • Im Beruf muss man sich an der Menge von Werten orientieren, für die die Organisation bezahlt und die sie einfordert, nicht an den persönlichen Engineering-Werten
    • Auch wenn man Performance wichtig findet, kann es nötig sein, langsamen Code zu schreiben, um einen Termin einzuhalten oder schwierige Anforderungen zu erfüllen
    • Auch vollständiges Verständnis der Codebase ist kein absoluter Maßstab, der immer gewahrt werden muss, sondern eine Option, die je nach Arbeitsziel gegen andere Werte eingetauscht werden kann

2 Kommentare

 
ndrgrd 49 분 전

Deshalb klammern wir uns so sehr an Abstraktion.

 
Meinungen auf Lobste.rs
  • Der Titel des Originalartikels ist leider ziemlich clickbaitig; tatsächlich geht es eher darum, auch mit nur teilweisem Verständnis einer großen Codebasis Fortschritte machen zu können.
    Auch die Perspektive des Theory Building widerspricht dem Kern des Textes nicht, wenn man annimmt, dass eine Theorie sowohl Breite als auch Tiefe hat. Breite bedeutet, welchen Bereich des Systems man ausreichend versteht, um Fragen zu beantworten und Änderungen souverän vorzunehmen; Tiefe bedeutet, wie komplexe Fragen man zu einem bestimmten Teil beantworten und wie komplexe Änderungen man unter Wahrung der Integrität durchführen kann.
    Wenn man Code modulweise neu schreibt und dafür eine Übergangsphase einplant, baut man im Grunde eine schmale, aber tiefe Theorie zu diesem Modul auf und arbeitet entsprechend damit. Bei kleinen Funktionen ist es in Ordnung, einen Parameter oder Rückgabewert hinzuzufügen; bei größerem Umfang ist es sinnvoller, zumindest bei einem bestimmten Modul auf sauberer Basis neu anzufangen und die anderen Teile so zu verdrahten, dass sie die neue Implementierung aufrufen. Grundsätzlich funktioniert es nicht gut, die Korrektheit einer von Grund auf fehlerhaften Implementierung schrittweise verbessern zu wollen; das gilt häufig auch für Eigenschaften wie Performance.
    Allerdings ist diskutabel, ob Naur, als er sagte, man solle „den bestehenden Programmtext verwerfen und ein neues Programmiererteam das gegebene Problem von Grund auf lösen lassen, statt das Programm wiederzubeleben“, meinte, man solle den alten Code vor Beginn der Neuentwicklung löschen, oder ob man ihn erst verwerfen dürfe, nachdem sich das neue Programm als angemessener Ersatz erwiesen hat.
    Außerdem ist in der Praxis das gegebene Problem selbst gar nicht gegeben. Naur setzt voraus, dass die ursprüngliche Problemdefinition noch existiert, doch bei großen Codebasen ist das oft nicht der Fall. Am nächsten kommt dem meist die Test-Suite; daher kann man Komponenten austauschen, während die Tests weiterhin bestehen.
    Damals gab es auch noch kein Continuous Deployment. Heute muss ein Dienst weiterlaufen, während man Bereitschaftsdienste übernimmt, Fragen beantwortet und dringende Bugs behebt; dadurch verändert sich auch die Kosten-Nutzen-Rechnung beim Neuschreiben.

  • Diese Gegenrede hat einige gut formulierte Stellen, wirkt aber größtenteils offensichtlich. In hinreichend großen Systemen ist teilweises Verständnis der einzige Zustand, doch Organisationen sollten stets nach tieferem Verständnis streben.
    Unabhängig von der Logik „man wird dafür bezahlt, Arbeit zu erledigen“ erfüllt große Software häufig weder Nutzerbedürfnisse noch Geschäftsziele. Auch Windows und OS X kann man als durch akkumulierte Komplexität verrottend betrachten.
    Auch das Management würde Komplexität magisch beseitigen, wenn es könnte. Das ist jedoch ein noch ungelöstes Problem der Softwaretechnik; es ist eher so, dass das Management unter den gegebenen Umständen solche Kompromisse akzeptiert, als dass es schlechten Code ausliefern möchte.
    Man könnte sagen: „Wenn man ein Monopol hat, reicht das, also darf man miserable, fehlerhafte Software ausliefern“, aber das ist übertrieben nihilistisch. Es gibt viele Softwarefirmen, die kein Monopol haben oder ihre Nutzer nicht festhalten können; dort ist Qualität tatsächlich wichtig.

    • Meiner Erfahrung nach misstraut das Management guten Praktiken der Softwareentwicklung und fordert oft, sie vorsorglich aufzugeben, um schneller voranzukommen. Meist geschieht das auf Basis bloßer Annahmen, ohne Überprüfung.
    • Das Ziel „man sollte immer mehr verstehen“ kann so missbraucht werden, dass man in einen Zustand gerät, in dem gar nichts mehr geht. Wenn nicht ausreichendes, sondern perfektes Verständnis verlangt wird, kann man nichts mehr tun und gibt am Ende auf oder verzweifelt.
      Unter dieser Lesart wirkt der Begriff des Verstehens fast wie eine Form von Gatekeeping.
  • Der im Artikel beschriebene Ansatz hängt eng mit der Fähigkeit zusammen, über Teile des Codes lokal zu argumentieren.
    Lokales Reasoning, also einen Teil einer großen Codebasis zu verstehen, ohne das gesamte Umfeld zu verstehen, ist ein Kernkonzept, das die Informatik von Anfang an verfolgt hat.
    Ein großer Vorteil der strukturierten Programmierung war, dass sie lokales Reasoning ermöglicht; auch die Empfehlung, globale Variablen zu vermeiden, hatte im Wesentlichen dasselbe Ziel. Funktionale Programmierung ermöglicht lokales Reasoning, indem sie Seiteneffekte eliminiert, und ein Prinzip der objektorientierten Programmierung — Datenstrukturen mit dem Code zu koppeln, der sie bearbeitet — bietet ein weiteres Mittel, lokal zu argumentieren.
    Die wahre Stärke dieser Grundkonzepte liegt darin, dass sie es erlauben, über kleine Teile einer Codebasis nachzudenken und daran zu arbeiten, ohne die gesamte Codebasis zu verstehen, auch wenn das keine ideale Situation ist.

  • Ich habe den Text als interessante Gegenrede zu dem Rat gelesen, man müsse „jede Zeile der Codebasis verstehen“, und weil die zentrale These des Artikels mit Entwicklungspraktiken zu tun hat, habe ich das Tag #practices hinzugefügt.
    Ich weiß, dass diese Community LLMs nicht mag, aber ich bezweifle, dass dieser Artikel wirklich unter #vibecoding fällt.

    • Vielleicht weißt du es schon, aber auf lobste.rs wird alles, was mit der Nutzung von LLMs zusammenhängt, breit als vibecoding klassifiziert; ich stimme dem zwar nicht zu, aber das ist dort die Praxis. Auch die Kernfrage dieses Artikels hängt eng mit LLM-unterstütztem Coding zusammen.
      Ich stimme allerdings zu, dass vibecoding nicht das einzige Tag sein sollte. Der Artikel behandelt LLMs erst ganz am Ende ausdrücklich, und sein Hauptargument war schon vor der LLM-Ära interessant und diskussionswürdig. Daher sehe ich genügend Grundlage, das Tag practices wiederherzustellen; ich habe es selbst wiederhergestellt und empfehle anderen, dasselbe zu tun.
    • Der Punkt des Artikels ist für alle Programmierer relevant, insbesondere für Menschen, die verstehen wollen, wie man mit riesigen Codebasen umgeht und wie sich das von kleinen Projekten unterscheidet.
      Er bleibt auch in einer Welt relevant, in der die Nutzung von LLMs ein vollständiges Verständnis der Codebasis behindern kann.
      Wenn das Tag bedeutet: „könnte für Menschen relevant sein, die sich für Vibe Coding interessieren“, dann ist es eindeutig passend. Wenn es dagegen bedeutet: „irrelevant für alle außer Menschen, die sich für Vibe Coding interessieren“, dann ist es nicht passend. Alle anderen Tags werden im ersteren Sinn verwendet, aber viele Lobsters-Nutzer verwenden gerade dieses Tag im letzteren Sinn; damit werden sie diesen Artikel letztlich zwangsläufig verpassen.
  • Ein weiteres Merkmal moderner Entwicklung im Unterschied zu 1985 ist, dass man zwar Experte für eine Codebasis mit 300.000 Zeilen werden kann, aber schon nächste Woche an einer völlig anderen Codebasis mit 300.000 Zeilen arbeitet.
    Erfahrene professionelle Programmierer sind daran gewöhnt, jederzeit in unbekanntes Terrain geworfen zu werden, und stützen sich auf eine Kombination aus gemeinsamen Idiomen (Google C++ Style Guide), automatisierten Werkzeugen wie Compilern, die Fehler melden, wenn man APIs falsch benutzt, und einem Bauchgefühl dafür, wie große Programme „aufgebaut sein sollten“.
    Ich habe auch Diskussionen gesehen wie bei Bun, das mit einem LLM neu geschrieben wurde: „Wie sollen wir weiterentwickeln, wenn jetzt niemand mehr das Verhalten der gesamten Codebasis versteht?“ Das ähnelt der Frage, wie man einen Roman verstehen könne, wenn man nicht der Autor ist. Am Ende ist es Code, also kann man ihn lesen. Wenn man eine Funktion nicht versteht, kann man sie in mehrere Stücke zerlegen, Tests schreiben, den Kontrollfluss auf Papier notieren oder was auch immer versuchen.
    Da man regelmäßig zwischen Projekten wechselt, gibt es auch keinen Grund, das Verständnis einer alten Codebasis dauerhaft aufrechtzuerhalten. Irgendwann vermischt sich alles. Selbst wenn man helfen soll, dass Scala-Code eines übernommenen Startups über ein maßgeschneidertes, Thrift-basiertes RPC-Protokoll mit einem JSON-basierten Ruby-Service kommuniziert, der gerade in Go neu geschrieben wird, kann man nach etwa einer Stunde Suche nach Scala syntax reference und Thrift wire encoding loslegen.
    Man muss auch kein Experte werden. Denn einen Monat später debuggt man vielleicht, warum in einer Go-Implementierung des Linux-Kernels ein OCaml-basierter Typechecker für JavaScript abstürzt. Am Ende ist alles nur Code.