1 Punkte von GN⁺ 2023-11-16 | 1 Kommentare | Auf WhatsApp teilen
  • Im Kern stehen zwei Regeln zur Strukturierung von Code: bedingte Verzweigungen zur Aufruferseite hochziehen und die Wiederholungslogik in Batch-Operationen nach unten schieben
  • Wenn man if nach oben zieht, lassen sich Vorbedingungen leichter per Typen oder Assertions erzwingen, und komplexe Kontrollflüsse werden an einer Stelle gebündelt, sodass doppelte Bedingungen und tote Zweige leichter auffallen
  • Eine Struktur, in der man erst ein enum erzeugt und direkt danach wieder per match darauf verzweigt, wiederholt dieselbe Bedingung als Verzweigung, Datenstruktur und erneute Verzweigung, sodass sich das oft zu Aufrufen wie foo(x) und bar(y) vereinfachen lässt
  • Wenn man for nach unten drückt, werden statt Iterationen über einzelne Objekte Batch-Operationen zur Grundeinheit, wodurch sich Anlaufkosten amortisieren, Verarbeitungsreihenfolgen ändern und Vektorisierung sowie Struct-of-Array-Optimierungen ermöglichen lassen
  • Wenn Bedingungen aus der Schleife herausgezogen werden, lassen sich Verzweigungen in Hot Loops reduzieren; das führt auch zu Architekturen wie bei TigerBeetle, wo die Entscheidungskosten der Control Plane durch Batch-Verarbeitung in der Data Plane amortisiert werden

if nach oben ziehen

  • if-Bedingungen in einer Funktion sollte man zuerst darauf prüfen, ob sie zur Aufruferseite verschoben werden können
    • Das Beispiel sieht eine Funktion, die direkt Walrus entgegennimmt, als bessere Form als eine Funktion, die Option<Walrus> erhält und bei None einfach zurückkehrt
    • Es ist klarer, wenn der Aufrufer die Vorbedingung prüft und ihre Gültigkeit per Typ oder Assertion erzwingt, statt sie innerhalb der Funktion zu prüfen und dann „nichts zu tun“
  • Dieses Refactoring, bei dem Vorbedingungsprüfungen nach oben gezogen werden, kann sich entlang der Aufrufkette ausbreiten und dadurch insgesamt die Anzahl der Prüfungen verringern
  • Kontrollfluss und if erhöhen die Komplexität und sind oft eine Quelle von Bugs
    • Wenn man if nach oben zieht, sammelt sich die komplexe Verzweigungslogik in einer Funktion, während die eigentliche Arbeit in geradlinige Unterroutinen nach unten wandert
    • Komplexer Kontrollfluss lässt sich leichter auf doppelte und tote Bedingungen prüfen, wenn er in einer Funktion auf einen Bildschirm passt, statt über die ganze Datei verstreut zu sein
  • Das Refactoring „dissolving enum“ beseitigt ein Muster, bei dem Verzweigungen erst in eine Datenstruktur umgewandelt und danach erneut verzweigt werden
    • Wenn f() je nach Bedingung E::Foo(x) oder E::Bar(y) erzeugt und g(e) anschließend per match wieder foo(x) oder bar(y) aufruft, wird dieselbe Bedingung in mehreren Formen wiederholt
    • Zieht man die Bedingung in main() nach oben, vereinfacht sich das zu if condition { foo(x) } else { bar(y) }

for nach unten drücken

  • Aus datenorientierter Sicht verarbeiten Programme und Hot Paths meist viele Objekte; deshalb führt man Batches von Objekten ein und macht Batch-Operationen zum Standardfall
    • Die skalare Variante für einzelne Objekte wird dann zum Spezialfall der Batch-Operation
    • frobnicate_batch(walruses) gilt als bessere Form als for walrus in walruses { frobnicate(walrus) }
  • Der wichtigste Vorteil ist die Performance
    • Wenn ein ganzer Batch auf einmal verarbeitet wird, lassen sich Anlaufkosten amortisieren
    • Man kann die Verarbeitungsreihenfolge flexibel ändern und auch Vektorisierung oder Struct-of-Array-Ansätze nutzen, bei denen zunächst dasselbe Feld aller Entitäten verarbeitet wird
    • Als extremes Beispiel wird der Vectorized Interpreters Talk verlinkt
  • Als interessantes Beispiel taucht die FFT-basierte Polynom-Multiplikation auf
    • Die gleichzeitige Auswertung eines Polynoms an vielen Punkten kann schneller sein, als die Auswertung an einzelnen Punkten mehrfach auszuführen
  • Die Regeln für if und for lassen sich gemeinsam anwenden
    • Eine gute Form ist eine Struktur, in der außerhalb von if condition je nach Zweig jeweils for walrus in walruses ausgeführt wird
    • Eine schlechte Form ist eine Struktur, in der innerhalb der for-Schleife jedes Mal if condition ausgewertet wird
    • Dadurch lässt sich vermeiden, dass Bedingungen erneut ausgewertet werden, Verzweigungen im Hot Loop verschwinden, und Vektorisierung wird möglich
  • Dieses Muster gilt sowohl auf Mikro- als auch auf Makroebene
    • Die Architektur von TigerBeetle verarbeitet in der Data Plane gleichzeitig Objekt-Batches und amortisiert so die Entscheidungskosten der Control Plane
    • Die Hauptmotivation hinter dem Rat zu for ist zwar Performance, aber ein auf Collections ausgerichteter Stil wie bei jQuery kann auch die Ausdruckskraft verbessern

1 Kommentare

 
GN⁺ 2023-11-16
Meinungen auf Hacker News
  • Anfangs war ich überrascht, dass dieser Beitrag so viel Gegenwind bekommt, aber als mir klar wurde, dass es hier um Ratschläge zu datenorientiertem Design geht, ergab es Sinn.
    Wie viele in diesem Forum baue auch ich meistens Web-Apps fürs Business-Umfeld, und in diesem Kontext kann dieser Rat wie Unsinn wirken.
    Wenn man sich im Arbeitsalltag nicht um den Instruction Cache kümmern muss, scheint mir die Heuristik richtig, diesen Rat weitgehend zu ignorieren.
    Wenn man ein Gefühl dafür bekommen möchte, wann dieser Rat wichtig wird, sollte man nach Mike Actons „Typical C++ Bullshit“ suchen; dieser Beitrag wirkt wie eine leichter verständliche Aufbereitung davon.
    Ich teile Casey Muratoris Problembewusstsein durchaus, aber die meiste Business-Software sollte eher auf Änderbarkeit und Korrektheit optimieren als auf Performance, also auf „Programmieren über die Zeit hinweg“.

    • Ich denke, dieser Rat lässt sich auch auf objektorientierte Programmierung immer noch recht gut anwenden.
      Entwickler versuchen, komplexe Business-Logik DRY zu machen, indem sie sie in kleine private Methoden innerhalb von Klassen zerlegen; „if nach oben ziehen“ verhindert, dass Verzweigungslogik über mehrere Methoden verstreut wird.
      Auch „for nach unten schieben“ ist wichtig. Viele Aufrufpfade enden schließlich in teuren Datenbankabfragen, und oft erzeugt eine Schleife auf höherer Ebene unten mehrere DB-Aufrufe.
      Diese Schleife lässt sich häufig durch eine where-Klausel oder einen join in SQL ersetzen, und auch Aggregation oder Filterung sollte man eher nahe an das DAO schieben, statt massenhaft Objekte zu laden und darüber zu iterieren; so lässt sich näher an der DB besser optimieren.
      Wie bei allen Designprinzipien sollte man es allerdings nicht dogmatisch anwenden, sondern bewusst abwägen.
    • Datenorientiertes Design scheint Menschen immer zu reizen. Vermutlich, weil es impliziert, dass viele Teile des bisher dominierenden objektorientierten Ansatzes falsch sind.
      Es ist schade, Änderbarkeit und Performance als Gegensätze zu betrachten. Überzeugende Belege dafür, dass die beiden tatsächlich kollidieren, habe ich bisher nicht gesehen.
    • Änderbarkeit, Korrektheit und Performance schließen sich nicht gegenseitig aus; im Gegenteil, sie gehen oft Hand in Hand.
      Einer der wichtigsten Aspekte von Performance ist, Dinge klein zu machen. Kleiner Code, kleine Datenstrukturen und eine geringe Zahl ausgeführter Instruktionen sind zentral, und „an den Instruction Cache denken“ bedeutet im Kern, kleinen Code zu schreiben.
      Je kleiner der Code ist, desto weniger Platz gibt es für Bugs, desto leichter lässt er sich auf einmal verstehen und desto einfacher ist es, Testabdeckung zu erreichen, was der Korrektheit zugutekommt. Auch Änderbarkeit profitiert, denn je kleiner der Code ist, desto kleiner fallen Änderungen aus.
      Natürlich gibt es Optimierungen wie Parallelisierung, Caching oder Low-Level-Optimierung, die Code komplexer machen, aber das ist nur ein Teil der Performance-Optimierung; ein Programmierer, der Performance ernst nimmt, macht so etwas nicht ohne Profiling und Analyse.
      if/for und for/if unterscheiden sich funktional vielleicht kaum, aber eines davon kann schneller sein; wenn man es ohne Kosten richtig machen kann, gibt es keinen Grund, absichtlich die langsamere Variante zu wählen.
    • Wenn man John Ousterhout zuhört, kollidieren Änderbarkeit und Korrektheit in der Praxis nicht so sehr mit Performance, wie man zunächst denkt.
      Einfache Programme haben kurze Call Stacks und vermeiden komplexe, teure Abstraktionen, daher verbrauchen sie tendenziell weniger Speicher und laufen schneller.
      Wenn man Performance auf höchstem Niveau anstrebt, kann ein Programm komplexer werden, aber echte Einfachheit bringt meist vernünftige Performance mit sich.
      „for nach unten schieben“ ist vor allem ein datenorientierter Ratschlag, aber „if nach oben ziehen“ geht eher in Richtung, Programme einfacher zu machen. Genauer gesagt geht es darum, die Lokalität im Quellcode zu erhöhen; der Kern ist, Verzweigungslogik an einer Stelle zu bündeln: https://loup-vaillant.fr/articles/source-of-readability
    • Ich denke, beide Ratschläge lassen sich auch auf Business-Apps recht gut anwenden.
      Besonders „Schleifen nach unten schieben“ ist in CRUD-Apps stark. Wenn man Erzeugung und Aktualisierung so weit wie möglich batcht, spart man oft viel mehr Zeit, als jede CPU-Engstelle kosten würde.
      Der Unterschied zwischen items.map(insertToDb/postToServer) und insertToDb/postToServer(items) beträgt fast immer mehrere Größenordnungen.
      Ich habe gesehen, wie solche Optimierungen Laufzeiten von Sekunden oder Minuten auf Millisekunden senkten; oft wird dabei auch die API sauberer und die Logs werden leichter lesbar.
  • Mit wachsender Erfahrung habe ich das Gefühl, dass zu viele Programmierer zwar auf kleine Einheiten „schönen Code“ achten, aber nicht genug auf das Design der gesamten Codebasis
    Knapp formulierte, gut ausgearbeitete Funktionen sind gut, aber solche Texte können in PRs oder Diskussionen in unproduktive Bikeshedding-Debatten ausarten
    Ob eine Funktion etwas unordentlich ist, wo if und for stehen oder ob map und filter verwendet werden, ist mir nicht besonders wichtig, solange der Funktionsname passt, Interfaces und Typen aussagekräftig sind, der Zweck klar ist, Dokumentation vorhanden ist und Seiteneffekte nicht übermäßig eingesetzt werden

    • „if nach oben ziehen“ ist kein Bikeshedding, sondern eher eine Architekturentscheidung, die man treffen muss, genau wie gesagt
      Wenn man darüber nachdenkt, wo Eingaben validiert werden sollen, dann sollten sie nach dieser Heuristik ganz oben, dort wo sie entgegengenommen werden, validiert werden
      Das hilft auch beim Verstehen des Codes und ist aus Sicht von Beweisen technisch nötig, weil Vorbedingungen nach oben propagiert werden müssen
      Der erste Ratschlag ist definitiv keine Kleinigkeit, der zweite ist etwas weniger eindeutig
    • In meiner Arbeit im Security-Bereich hatte ich viel mit Tech Leads und Architekten auf etwa FAANG-Level L7 bis L9 zu tun, und die Menschen, die am meisten auf „korrektes Design“ fixiert waren, hatten meist 5 bis 10 Jahre Berufserfahrung
      Sie wollen verhindern, dass Juniors sich selbst ins Knie schießen, haben aber oft noch kein ausreichendes Gefühl für die Kosten von Komplexität und dafür, wie sich Code langfristig verändert
      Umgekehrt bewertet man nach über 20 Jahren Erfahrung Einfachheit höher als die meisten technischen Aspekte
      Fragen wie „wie früh verzweige ich?“ oder „was soll dieser Code eigentlich tun?“ liefern langfristig meist die wertvollsten Antworten
      Fragen zu Abstraktion und Kapselung führen dagegen leicht zu den oben genannten Debatten, und dass in Code von Leuten, die nur das „große Ganze“ betrachten, die meisten Security-Probleme auftauchen, liegt auch daran, dass sie nicht wirklich verstehen, was diese angeblich gut designte Codebasis tatsächlich tut
    • Wenn solche scheinbar kleinen Prinzipien vorab festgelegt sind, können PRs und Diskussionen weniger ermüdende Debatten derselben Art enthalten
      In performancekritischer Software, zu der datenorientiertes Design gut passt, muss man auf solche Dinge im Kleinen stärker achten. Denn Compiler-Optimierungen funktionieren auf diese Weise
      Dann ändern sich die Spielregeln. Die Bedeutung von Statements wird wichtiger, selbsterklärender Code gewinnt an Bedeutung, Code-Kommentare bleiben nur noch zum Begründen von Schlussfolgerungen übrig, und „Dokumentation“ nähert sich eher Spezifikationen und Benutzerhandbüchern an
    • Ich habe auch oft das Gegenteil gesehen. Viele entwerfen gern überdesignte Kathedralen, denken aber nicht über die Low-Level-Effizienz von Algorithmen nach
    • Ein Grund ist, dass Programmiersprachen kaum Möglichkeiten bieten, Komponenten, die deutlich größer als Funktionen sind oder auf andere Weise als Funktionen miteinander verbunden werden, sinnvoll auszudrücken
      Im besten Fall kann man Funktionen gruppieren, danach ist man auf sich gestellt
  • „if nach oben ziehen“ hat den Nachteil, dass Vorbedingungen und Nachbedingungen innerhalb der Funktionsdefinition nicht direkt sichtbar sind und an jedem Aufrufort geprüft werden müssen
    In großen Projekten mit mehreren Beteiligten können solche Funktionen außerhalb des beabsichtigten Kontexts wiederverwendet werden und zu Bugs führen
    Mit einem Contract-Framework lässt sich das lösen, aber dann schreibt man Bedingungen doppelt, im Contract und im Code, und bei Dependent Types ist es ähnlich
    Interessant ist ein Ansatz, bei dem Codebereiche, die zu einem bestimmten Kontext gehören, getaggt werden und Funktionen definiert werden, die nur innerhalb dieses Kontexts aufgerufen werden können
    In Python könnte man mit einem Decorator wie @requires_context("VALIDATED_XY") und einem Context Manager validated_xy dafür sorgen, dass Funktionen nur in einem validierten Bereich aufgerufen werden
    Zur Laufzeit kennt das System die Bedeutung dieses Kontexts nicht, aber mit passenden Tools und Tests kann man es so gestalten, dass der gewünschte Kontext nur aufgebaut wird, wenn bestimmte Bedingungen erfüllt sind
    In Sprachen wie Haskell könnte man das mit etwas wie der Identitätsmonade auf Typebene erzwingen, und selbst ohne Erzwingung auf Typebene könnte es eine interessante Methode sein, „unsafe“-Codebereiche abzusichern

    • Ich glaube, du hast den zweiten Teil der Argumentation des Autors übersehen. Es geht darum, „Vorbedingungsprüfungen an den Aufrufer zu verlagern und sie per Typ zu erzwingen
      Daher sind die Vorbedingungen weiterhin direkt in der Funktionsdefinition sichtbar, nur eben nicht als if-Statement, sondern als Teil der Typsignatur
      Das ist ein in Rust häufiges Pattern, wie es im Artikel verwendet wird, und anders als eine if-Prüfung ist es eine strikte Vorbedingung, die zur Compile-Zeit statt zur Laufzeit geprüft wird; wenn sie nicht erfüllt ist, kompiliert das Programm nicht
    • Auch in Python kann man das mit Typen ausdrücken
      Zum Beispiel nimmt man do_something(position: ValidatedPosition) entgegen, validiert eine normale Position und übergibt sie danach als ValidatedPosition weiter
      In der Praxis würde man die Validierung vermutlich im Konstruktor von ValidatedPosition kapseln, aber der Kern ist, dass mypy meldet, wenn man versucht, eine Position unverändert zu übergeben, weil dann der falsche Typ übergeben wird
      Pythons Type Checking ist nicht so umfassend wie das von Rust, wird aber immer nützlicher, wenn man sicherstellen möchte, dass weitergereichte Daten angemessen verarbeitet wurden
    • Der Ratschlag „if nach oben ziehen“ betrifft nicht Vorbedingungsprüfungen, sondern die Auswahl des richtigen Codepfads
      Wenn eine Funktion Vorbedingungen hat, kann man sie natürlich am Anfang der Funktion per Assertion prüfen. Da das Java-Typsystem zum Beispiel null zulässt, muss eine Funktion, die ein Objekt benötigt, eine Exception werfen, wenn null hereinkommt
      Jeder Aufrufort ist dafür verantwortlich, die Funktion nur dann aufzurufen, wenn die Vorbedingungen wahr sind. Das ergibt sich bereits aus der Definition von Vorbedingungen
      Eine Funktion unter Verletzung ihrer Vorbedingungen aufzurufen, ist ein Bug des Aufrufers. Innerhalb der Funktion kann Code nötig sein, der das prüft, um undefiniertes Verhalten zu verhindern, aber solche Assertions und der tatsächliche Kontrollfluss des Programms müssen getrennt werden; der Artikel handelt von Letzterem
    • In diesem Beispiel mit fn frobnicate(walrus: Walrus) kompiliert das Programm nicht, wenn man etwas übergibt, das kein eigener Walrus ist
      Selbst bei Generics muss das Argument die Trait Bounds erfüllen, und je nachdem, wie das Argument innerhalb der Funktion verwendet wird, verlangt der Compiler die für die Funktionsdefinition nötigen Bounds
    • Ich vermute, dass die Intention von public und private zumindest teilweise darin bestand, Code passend zu einem bestimmten Kontext zu taggen
      Oder vielleicht braucht es eine spezifischere Semantik, die quer zu den Bereichen liegt, die public, private und im .NET-Ökosystem protected abdecken
  • Das erste Beispiel ist nicht wegen if und for schlecht, sondern aus anderen Gründen.
    Wenn man im Allgemeinen einen „Container“ für etwas hat, ist es besser, Funktionen nicht auf dem Container zu schreiben, sondern auf dem domänenspezifischen „Objekt“ darin.
    Wenn man in Clojure einen Agent verwendet, schreibt man keine Funktionen für den Agent, sondern Funktionen für das Objekt, das der Agent enthalten kann.
    Auch in Elixir arbeiten die zentralen Domänenfunktionen nicht auf einer PID, sondern auf der internen Domänendatenstruktur, und GenServer-Aufrufe werden bei Bedarf dorthin delegiert.
    Dadurch wird es flexibler, und man kann die Kerndomäne „einen Walrus frobnicate“ sauberer von der Anwendungsangelegenheit „es kann einen Walrus geben oder auch nicht, und falls ja, ihn frobnicate“ trennen.

    • Der gegebene Rat scheint die Validierungslogik möglicherweise zu weit nach oben zu ziehen.
      Früh zu validieren ist gut, aber es ist auch wichtig, statt dass eine Funktion plötzlich mit einem merkwürdigen Fehler platzt, klar einen Validierungsfehler auszugeben.
      Haskell löst dieses Problem mit newtype, also durch einen „transparenten Container, der bescheinigt, dass die passende Validierung bereits erfolgt ist“.
      Der Rat, den ich den Leuten wirklich ans Herz legen möchte, ist, „traurige ifs“ zu bevorzugen. Statt den Normalpfad mehrfach zu verschachteln, ist Code fast immer lesbarer und wartbarer, wenn er falsche Bedingungen oben nacheinander prüft und sofort korrigiert oder abbricht.
      Menschen haben die natürliche Tendenz, sich auf den erwarteten Fall zu konzentrieren, aber Code möchte umgekehrt, dass man sich zuerst auf die Ausnahmefälle konzentriert. Jedes if erzeugt mentale Last, und wenn man Informationen aus externen Systemen holen oder früh mit einem Fehler beenden muss, kann man diese Last sofort ablegen, wenn Erkennung und Behandlung beieinanderliegen.
    • Wenn damit gemeint ist, dass das erste Beispiel als walrus.frobnicate() besser wäre, dann scheint mir das eher eine Frage des Syntaxgeschmacks zu sein als der Kernpunkt, den der Autor vermitteln will.
  • Ich würde nicht so weit gehen, das als schlechten Rat zu bezeichnen, aber es ist auch nicht zwangsläufig guter Rat.
    Dass die gewählte Sprache Rust ist, sagt ziemlich viel aus. Ein starkes Typsystem verhindert vieles an defensiver Programmierung, die in anderen Sprachen nötig wäre.
    Wenn ein C-Programmierer die Gültigkeit eines an eine Funktion übergebenen Zeigers nicht prüft und dadurch eine NULL-Dereferenzierung verursacht, möchte ich mit so jemandem nicht im Team arbeiten.
    Daher sollten zumindest einige ifs eindeutig weiter unten stehen, und es wäre gut, wenn Fehler sauber nach oben propagiert werden.
    Bei for empfinde ich das weniger stark, aber weil Array-Argumente in C zu Pointern decayen, denke ich, dass auch Iteration eher oben als unten sein sollte. In der Funktion, in der ein Array erstellt wurde, kann man der Länge vertrauen, in einer Funktion, an die es als Argument übergeben wurde, nicht.

    • C-Interfaces sollten Erwartungen sorgfältig dokumentieren und genau so viel prüfen.
      Die Dokumentation sollte ein starkes Typsystem ersetzen, nicht Runtime-Prüfungen.
      Code voller NULL-Prüfungen und anderer defensiver Maßnahmen ist deutlich schwerer zu lesen.
      An Bibliotheksgrenzen kann man sagen, dass defensivere Prüfungen nötig sind, und genau solche Prüfungen will der Artikel nach oben verschieben.
      Sicherheitskritischer Code mag anders sein, aber in den meisten Fällen reicht es, wenn versehentliche NULL-Dereferenzierungen durch Tests, Sanitizer und Fuzzing gefunden werden.
    • In C ist NULL oft ein gültiger, aber nicht dereferenzierbarer Zeigerwert.
      Um ungültige Zeigerwerte zu prüfen, müsste man die ganze Menge der vielen möglichen falschen Werte prüfen; wenn man nur NULL prüft, prüft man faktisch nicht auf ungültige Werte.
      Wenn die Vorbedingung einer Funktion lautet: „Der Parameter p darf nicht NULL sein“, dann kann man das prüfen. Wenn die Vorbedingung aber lautet: „p muss ein gültiger Zeiger sein“, dann viel Glück dabei, eine passende Assertion zu finden.
    • Meine Heuristik lautet: Wenn das Typsystem falsche Werte nicht verhindern kann, ist die Runtime dafür verantwortlich, sie zu verhindern.
      Ich arbeite derzeit viel mit T-SQL, und dort kann man Parameter oder Variablen nicht als NOT NULL deklarieren.
      Daher ist es gut, so früh wie vernünftigerweise möglich auf NULL zu prüfen, normalerweise ganz oben in der Stored Procedure.
      Andernfalls kann sich NULL unerwartet tief durch die Aufrufhierarchie propagieren und weniger klare Probleme verursachen.
      Zum Glück kann man Tabellendaten als NOT NULL deklarieren, sodass solche Bugs normalerweise keine Daten ruinieren, aber es ist deutlich einfacher, sie früh abzufangen.
      Wenn es jedoch je nach Wert eines Parameters Logik gibt, die in die DB schreibt, und dieser Wert unerwartet NULL ist, kann ein falscher Wert geschrieben werden oder ein erforderlicher Wert überhaupt nicht, was die Daten faktisch beschädigen kann.
      Also ist defensive Programmierung die Antwort.
  • Ohne passenden Kontext wirkt dieser Rat ziemlich seltsam und vielleicht sogar schlecht.
    Schleifen und if-Anweisungen sind beide Kontrollfluss-Operationen, daher ergibt ein Teil der Argumentation im Artikel wenig Sinn.
    Das stärkste Argument scheint Performance zu sein, aber Performance sollte normalerweise das sein, worum man sich zuletzt kümmert, und besonders bei heuristischen Ratschlägen gilt das aus meiner Sicht umso mehr.
    Leider hat der Autor daraus einen Catchphrase gemacht, und ich hoffe, dass er sich nicht verbreitet.

    • Performance kann ein Argument sein, aber kein starkes.
      Wenn man, wie im Beispiel des Autors, den verbesserten Code schreiben kann, ist die Bedingung während der Schleifenausführung konstant. Solange nicht jedes Mal eine teure Bedingung ausgewertet wird, erledigt die Sprungvorhersage das meiste.
      Wenn die Bedingung ein boolescher Ausdruck aus const-Werten ist, kann der Compiler sie möglicherweise ebenfalls erkennen.
  • Ich dachte, der ganze Artikel würde auf ein Beispiel dieser Art hinauslaufen: walrus je nach Bedingung aufteilen und frobnicate_batch(fwalrus) sowie transmogrify_batch(twalrus) aufrufen.
    Tatsächlich ging es dann aber zu einem Beispiel, bei dem eine einzige Bedingung die gesamte Schleife in zwei Zweige einhüllt.

  • Es überrascht mich, dass Programmierer mit Software-Engineering-Hintergrund das so oft falsch machen.
    Ich habe in der Wissenschaft mit dem Programmieren angefangen, und dort ist es absolut notwendig, über solche Dinge nachzudenken.
    Allein die falsche Reihenfolge von Schleifen kann dazu führen, dass eine Simulation eine Woche statt einer Stunde dauert.
    Durch diesen Hintergrund ordne ich for und if instinktiv passend an, um kleine Optimierungen mitzunehmen, und Code, der das nicht tut, sieht für mich einfach falsch aus.

  • Ich bin nicht sicher, ob solche allgemeinen Regeln wirklich auf echten Code anwendbar sind.
    Solche Regeln wirken oft wie fehlplatzierte Dogmen, und auch wenn ein Blogpost mit dem Hinweis beginnt, es handle sich um Heuristiken, nehmen junge Programmierer das nicht immer so auf.
    Vor ein paar Wochen hat YouTube mir ständig ein Video namens „I'm a never-nester“ empfohlen; es schien zu behaupten, man solle if niemals verschachteln, und das war ziemlich lächerlich.
    Wie im Beispiel des Artikels wurde Code mit if condition innerhalb von for als „schlecht“ markiert, aber in den meisten tatsächlich so geschriebenen Codes hängt condition von walrus ab, sodass man das if nicht nach oben ziehen kann.
    Wenn man es nach oben ziehen könnte, wäre es so offensichtlich, dass dieselbe Bedingung in jeder Schleifeniteration erneut ausgewertet wird, dass Programmierer das ganz natürlich zu vermeiden versuchen.
    Aber wenn ein Junior oder Student einen Rat liest, der wie ein Dogma klingt, kann er beim strikten Befolgen schlechteren Code produzieren.

    • Was „niemals verschachteln“ angeht, bin ich nicht besonders dogmatisch, aber unter der Voraussetzung, dass die Syntax es zulässt, habe ich aus Erfahrung noch keinen Fall gesehen, in dem match (condition_a, condition_b) zur Aufteilung in vier Fälle nicht besser gewesen wäre als verschachtelte ifs.
    • Ich sehe das eher als gutes Beispiel dafür, dass man das if „nach oben ziehen“ sollte.
      Der Zweck des Codes ist, je nach Bedingung eine bestimmte Operation auf walrus auszuführen, aber tatsächlich wird das if anstelle von Polymorphie und Typsystem verwendet.
      Warum sollte walrus zwei Funktionen haben, die in unterschiedlichen Szenarien aufgerufen werden müssen? Warum nicht eine Funktion und zwei Typen haben und einfach den richtigen Typ übergeben?
      Auch in der aktuellen Struktur kann man oben je nach Bedingung die Verarbeitungsfunktion auswählen und die untere Schleife dann jedes Mal die ausgewählte Funktion aufrufen lassen.
      Wenn man Entscheidungen so früh wie möglich trifft, muss man sie nicht im ganzen Code verstreuen. Das Innere des Codes führt ohne Verzweigungen jedes Mal dieselbe Arbeit aus, und die Ausgabe unterscheidet sich nur über den Konfigurationsgraphen.
      Natürlich ist das kein neuer Gedanke; schon vor 15 Jahren war es eine alte Idee: https://www.youtube.com/watch?v=4F72VULWFvc
    • Das „GOOD“-Refactoring funktioniert nur, wenn die Bedingung nicht von walrus abhängt, und hilft dabei, diese Tatsache explizit sichtbar zu machen.
      Wendet man hier „for nach unten verschieben“ erneut an, landet man bei einer Form, in der innerhalb von if condition frobnicate_batch(walruses) oder transmogrify_batch(walruses) aufgerufen wird.
    • Ich finde es gut, dass es solche Artikel gibt.
      Jemand hat ein Problem in Worte gefasst, dem ich mehrfach begegnet bin, das ich aber nicht richtig ausdrücken konnte, und es wirkt wie ein gutes Modell, das man im Kopf behalten kann.
      Gleichzeitig ist auch diese Kritik wertvoll. Ich hoffe, dass dogmatische Junior-Entwickler beides lesen und etwas nuancierter urteilen.
    • Der Kern, den man aus allen „Heuristiken“ und „Best Practices“ lernen sollte, ist das Warum dahinter.
      Programmierer sollten solche Dinge insbesondere nicht kopieren und mechanisch anwenden.
      Heuristiken und Best Practices blind anzuwenden, ist möglicherweise keine gute Idee, und am Ende gilt immer: „Es kommt darauf an.“
  • In solchen Heuristiken steckt meist ein Körnchen Weisheit, aber im Großen und Ganzen entsteht damit oft etwas, das man neuen Programmierern wieder entdogmatisieren muss.
    Es gibt immer viele Fälle, in denen es schlechter wird, wenn man sie zu strikt befolgt, und „wisse, wann du diesen Rat nicht befolgen solltest“ ist im Grunde die eigentliche Schwierigkeit.

    • Entdogmatisierung ist ohnehin nötig.
      Ich halte solche Regeln für nützlich, um mit ihnen herumzuspielen. Man kann sie annehmen, bis zum Ende durchziehen, sie nach einem Tag oder einem Jahr auch mal umdrehen und sehen, wie weit man damit kommt.
      Auch die Grenzen zu lernen, ist ein weiterer Stoff, der sich auf dem immer wieder überschriebenen Pergament ablagert.
    • Dieser Artikel kann als Koan innerhalb einer größeren Sammlung nützlich sein.
      Einige dieser Koans sollten einander widersprechen.