`if` nach oben ziehen, `for` nach unten drücken
(matklad.github.io)- 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
ifnach 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
enumerzeugt und direkt danach wieder permatchdarauf verzweigt, wiederholt dieselbe Bedingung als Verzweigung, Datenstruktur und erneute Verzweigung, sodass sich das oft zu Aufrufen wiefoo(x)undbar(y)vereinfachen lässt - Wenn man
fornach 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
Walrusentgegennimmt, als bessere Form als eine Funktion, dieOption<Walrus>erhält und beiNoneeinfach 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“
- Das Beispiel sieht eine Funktion, die direkt
- 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
iferhöhen die Komplexität und sind oft eine Quelle von Bugs- Wenn man
ifnach 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
- Wenn man
- Das Refactoring „dissolving enum“ beseitigt ein Muster, bei dem Verzweigungen erst in eine Datenstruktur umgewandelt und danach erneut verzweigt werden
- Wenn
f()je nach BedingungE::Foo(x)oderE::Bar(y)erzeugt undg(e)anschließend permatchwiederfoo(x)oderbar(y)aufruft, wird dieselbe Bedingung in mehreren Formen wiederholt - Zieht man die Bedingung in
main()nach oben, vereinfacht sich das zuif condition { foo(x) } else { bar(y) }
- Wenn
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 alsfor 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
ifundforlassen sich gemeinsam anwenden- Eine gute Form ist eine Struktur, in der außerhalb von
if conditionje nach Zweig jeweilsfor walrus in walrusesausgeführt wird - Eine schlechte Form ist eine Struktur, in der innerhalb der
for-Schleife jedes Malif conditionausgewertet wird - Dadurch lässt sich vermeiden, dass Bedingungen erneut ausgewertet werden, Verzweigungen im Hot Loop verschwinden, und Vektorisierung wird möglich
- Eine gute Form ist eine Struktur, in der außerhalb von
- 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
forist zwar Performance, aber ein auf Collections ausgerichteter Stil wie bei jQuery kann auch die Ausdruckskraft verbessern
1 Kommentare
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“.
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 einenjoinin 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.
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.
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/forundfor/ifunterscheiden 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.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
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)undinsertToDb/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
ifundforstehen oder obmapundfilterverwendet 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 werdenWenn 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
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
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
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 Managervalidated_xydafür sorgen, dass Funktionen nur in einem validierten Bereich aufgerufen werdenZur 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
Daher sind die Vorbedingungen weiterhin direkt in der Funktionsdefinition sichtbar, nur eben nicht als
if-Statement, sondern als Teil der TypsignaturDas 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 nichtZum Beispiel nimmt man
do_something(position: ValidatedPosition)entgegen, validiert eine normalePositionund übergibt sie danach alsValidatedPositionweiterIn der Praxis würde man die Validierung vermutlich im Konstruktor von
ValidatedPositionkapseln, aber der Kern ist, dass mypy meldet, wenn man versucht, einePositionunverändert zu übergeben, weil dann der falsche Typ übergeben wirdPythons 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
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
fn frobnicate(walrus: Walrus)kompiliert das Programm nicht, wenn man etwas übergibt, das kein eigenerWalrusistSelbst 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
publicundprivatezumindest teilweise darin bestand, Code passend zu einem bestimmten Kontext zu taggenOder vielleicht braucht es eine spezifischere Semantik, die quer zu den Bereichen liegt, die
public,privateund im .NET-ÖkosystemprotectedabdeckenDas erste Beispiel ist nicht wegen
ifundforschlecht, 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.
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
iferzeugt 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.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
forempfinde 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.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.
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
pdarf nicht NULL sein“, dann kann man das prüfen. Wenn die Vorbedingung aber lautet: „pmuss ein gültiger Zeiger sein“, dann viel Glück dabei, eine passende Assertion zu finden.Ich arbeite derzeit viel mit T-SQL, und dort kann man Parameter oder Variablen nicht als
NOT NULLdeklarieren.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 NULLdeklarieren, 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.
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:
walrusje nach Bedingung aufteilen undfrobnicate_batch(fwalrus)sowietransmogrify_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
forundifinstinktiv 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
ifniemals verschachteln, und das war ziemlich lächerlich.Wie im Beispiel des Artikels wurde Code mit
if conditioninnerhalb vonforals „schlecht“ markiert, aber in den meisten tatsächlich so geschriebenen Codes hängtconditionvonwalrusab, sodass man dasifnicht 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.
match (condition_a, condition_b)zur Aufteilung in vier Fälle nicht besser gewesen wäre als verschachtelteifs.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
ifanstelle 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
walrusabhä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 conditionfrobnicate_batch(walruses)odertransmogrify_batch(walruses)aufgerufen wird.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.
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.
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.
Einige dieser Koans sollten einander widersprechen.