4 Punkte von GN⁺ 2025-08-20 | 1 Kommentare | Auf WhatsApp teilen
  • Beim Programmieren von links nach rechts bleibt das Programm in dem Moment, in dem Code eingegeben wird, stets in einem gültigen Zustand, wodurch Tool-Unterstützung wie die Autovervollständigung des Editors maximiert wird
  • Die List Comprehension in Python behindert die Autovervollständigung, weil nicht deklarierte Variablen und fehlende Typinferenz im Weg stehen
  • Rust und JavaScript lassen sich natürlich von links nach rechts aufbauen, wodurch die Verwendung von Variablen und das Erkunden von Methoden intuitiver werden
  • Der funktionale Stil in C und Python beeinträchtigt durch die schwache Auffindbarkeit von Funktionsnamen und Strukturen eine effiziente Coding-Erfahrung
  • Bei Logik mit hoher Komplexität ist Code, der sich von links nach rechts entfaltet, leichter zu lesen und besser für Wartbarkeit und Erweiterbarkeit geeignet

Von links nach rechts programmieren

Code muss in dem Moment, in dem er eingegeben wird, gültig sein


Die Grenzen von Python-List-Comprehensions

  • Die Python-Syntax für List Comprehensions words_on_lines = [line.split() for line in text.splitlines()] verursacht das Problem, dass der Editor Autovervollständigung und Typinferenz nicht richtig bereitstellen kann, weil auf eine nicht deklarierte Variable (line) zugegriffen werden muss
  • Während der schrittweisen Eingabe des Codes
    • wenn man etwa words_on_lines = [line.sp eingibt, kennt der Editor den Typ von line nicht und kann daher keine Methoden vorschlagen
    • auch potenzielle Fehler wie Tippfehler im Variablennamen (lime usw.) lassen sich schwer erkennen
  • Um korrekte Vorschläge zu erhalten, muss man unvollständigen Code schreiben, was unintuitiv und unbequem ist

Aufbau von links nach rechts in Rust

  • Im Rust-Beispiel (let words_on_lines = text.lines().map(|line| line.split_whitespace());)
    • gilt eine Variable (line) in dem Moment, in dem sie zusammen mit der anonymen Funktion erstmals erscheint, als deklariert, sodass sofort Autovervollständigung und Methodenvorschläge möglich sind
    • auch die Methode split_whitespace ließ sich dadurch leicht finden, weil sie automatisch vorgeschlagen wurde
  • Dieser Ansatz hält das Programm immer in einem zumindest teilweise gültigen Zustand, sodass IDEs und Editoren das Coding in Echtzeit unterstützen können

Progressive Disclosure und API-Usability

  • Progressive Disclosure ist ein Gestaltungsprinzip, bei dem Nutzer nur so viel Komplexität sehen, wie sie gerade benötigen; das lässt sich auch auf das Programmieren anwenden
    • Beispiel: ähnlich wie bei der UX einer Textverarbeitung, in der relevante Optionen erst erscheinen, wenn ein Bild eingefügt wird
  • In C fehlt diese Unterstützung weitgehend
    • da sich nicht alle zu FILE *file gehörenden Funktionen über file. erkunden lassen, muss man Namensmuster von Funktionen (fread, fclose usw.) auswendig lernen, und Funktionen sind schwer zu entdecken
    • in einer idealen Sprache könnten passende Funktionen dagegen über Methodenvorschläge nach file. leicht schrittweise entdeckt werden

Unterschied bei der Auffindbarkeit von Funktionen und Methoden

  • Vergleich der Beispiele map(len, text.split()) in Python und text.split(" ").map(word => word.length) in JavaScript
    • in Python muss man oft mehrere Versuche unternehmen, weil Funktionsnamen wie len, length oder size nicht vorhersehbar sind
    • in JavaScript reicht es, nach word. nur .l einzugeben, und der Editor schlägt Methoden wie length vor, wodurch die Auffindbarkeit hoch ist
    • auch Higher-Order Functions wie map machen den tatsächlichen Rückgabewert und den Datentyp unmittelbar klar erkennbar

Vorteile strukturierten Schreibens bei komplexerer Logik

  • Bei Logik mit hoher Komplexität (langer Python-Code mit verschachteltem filter und lambda)
    • muss man wiederholt Anfang und Ende des Codes prüfen, und Bedingungen oder Klammerzuordnungen verschlechtern Lesbarkeit und Verständlichkeit
  • In der JavaScript-Version derselben Logik kann man den Code dagegen von oben nach unten und von links nach rechts sequenziell lesen und verstehen

Kernprinzip

Code muss in jedem Moment der Eingabe gültig sein

  • Schon die alleinige Eingabe von text hält das Programm in einem gültigen Zustand
  • Auch wenn man bis text.split(" ") schreibt und anschließend .map(word => word.length) ergänzt, bleibt der Gesamtzustand durchgehend gültig
  • Dieses Coding-Muster erhöht die Möglichkeit Echtzeit-Unterstützung durch den Editor und erlaubt es in einer REPL-Umgebung, Ergebnisse sofort zu prüfen

Fazit

  • API- und Sprachdesign sollten so gestaltet sein, dass Code auf natürliche Weise von links nach rechts eingegeben werden kann und in jedem Zwischenschritt ein gültiges Programm entsteht
  • Gutes API-Design ist der Schlüssel zur Verbesserung dieser Coding-Erfahrung

1 Kommentare

 
GN⁺ 2025-08-20
Hacker-News-Kommentare
  • Einer der Nachteile von SQL ist, dass Abfragen mit SELECT statt mit FROM beginnen. Dadurch ist nicht sofort klar, mit welcher Entität bzw. Tabelle gearbeitet wird, und es erschwert auch Smart-Editoren, beim Schreiben von Queries effizienter zu helfen. Die Reihenfolge FROM -> SELECT -> WHERE wäre natürlicher, insbesondere weil man im SELECT-Teil Spaltennamen festlegt und diese dann in WHERE referenziert. Eigentlich, so finde ich, müsste man statt SELECT * FROM table auch einfach nur FROM table schreiben können und die SELECT-Klausel wäre implizit. Vielleicht klinge ich mit diesem Ärger wie ein nörgelnder alter Mann, aber es ist einfach meine persönliche Sehnsucht.
    • PSQL und PRQL verwenden tatsächlich eine Query-Reihenfolge, bei der FROM zuerst kommt. BigQuery hat inzwischen auch eine Pipe-/Pfeilsyntax hinzugefügt, und es gibt Community-Extensions für DuckDB, die ich empfehlen kann: DuckDB - PSQL, DuckDB - PRQL
    • SQL wird so geschrieben, weil in den Grundlagen der relationalen Algebra die Projektion immer zuerst notiert wird. Deshalb kann man laut Standard in WHERE keine Spalten-Aliasse verwenden, weil die Selektion (WHERE) vor der Projektion (SELECT) stattfindet. Zur Info: In MySQL 8 gibt es auch die Syntax TABLE <table>.
    • Tatsächlich ist die interne Verarbeitungsreihenfolge der meisten SQL-Engines FROM -> WHERE -> SELECT. Deshalb können in SELECT definierte Spalten-Aliasse in GROUP BY, HAVING und ORDER BY verwendet werden, aber nicht in WHERE.
    • In C# ist auch die zu SQL kompilierte DSL LINQ-to-SQL so aufgebaut, dass FROM zuerst kommt. Ich halte diese Struktur für gut, weil man beim Schreiben anderer Klauseln in der IDE dank Autovervollständigung sofort Feldvorschläge bekommt.
    • Kusto, die Datenanalyse-Abfragesprache von Azure, hat ebenfalls eine ähnliche Form mit Pipes: Einführung in Kusto-Abfragen. Der LINQ-Stil in .NET ist genauso. Ehrlich gesagt sollte SQL Varianten, die mit FROM beginnen, deutlich aktiver einführen. Ich halte das auch nicht für schwierig; es fehlt einfach an Versuchen, die Nutzbarkeit zu verbessern.
  • Ich finde es schwer nachzuvollziehen, warum Python so geliebt wird. Sobald mehr als zwei Personen daran arbeiten, wird die Sprache endlos schmerzhaft. Was der Autor anspricht, ist nur die Spitze des Eisbergs.
    • Ich glaube, das ist ähnlich wie bei der Frage, warum nicht alle zu Lisp-artigen Sprachen strömen. Mathematische Strenge bedeutet nicht automatisch Lesbarkeit. Die List-/Dict-/Set-Comprehensions in Python sind im Grunde wie for-Schleifen mit festgelegtem Typ. Alle sorgen sich darum, dass Python-Typen locker sind, aber ausgerechnet die einzige Syntax, die den Rückgabetyp eindeutig festlegt, nämlich die Listen-Comprehension, wird zum Ziel. In den meisten anderen Sprachen, einschließlich Rust, ist die Reihenfolge ebenfalls nicht "from iter as var". Interessant ist auch, die Funktionsaufrufsyntax verschiedener Sprachen zu vergleichen, zumal es in Python ja auch functools.map gibt.
    • Nur weil man etwas nicht versteht, wird es noch lange nicht zu einer Tugend. Es muss einen Grund geben, warum Python geliebt wird. Natürlich hat es klare Nachteile, aber für sich genommen sagt das wenig. Man muss die Vor- und Nachteile insgesamt gegeneinander abwägen, und zwar auch im Vergleich zu anderen Sprachen.
    • Ich mag Python ebenfalls, allerdings für kleine Teams sowie kurze und kurzlebige Programme. Ohne statische Typen kann man schnell implementieren, aber dank des starken Typsystems bricht trotzdem nicht alles völlig zusammen. Deshalb ist es meiner Ansicht nach in der Data Science so beliebt: Für Exploration ist es sehr bequem. Andererseits hat es bei langfristiger Wartung durch mehrere Teams oder bei großen Programmen eindeutig Schwächen. Am Ende gibt es keine Allzwecksprache; man braucht mindestens sowohl eine "weiche" Sprache, mit der man Dinge schnell ausprobieren und vorantreiben kann, als auch eine "harte" Sprache, die sich gut über lange Zeit pflegen lässt.
    • Früher habe ich dieser Meinung vollständig zugestimmt, aber durch Type Annotations und Type Checking ist die Zusammenarbeit an Python-Code, den andere geschrieben haben, viel einfacher geworden. Für wirklich große Projekte halte ich es zwar noch immer nicht für ideal, aber mit Typen ist Python für mich zur liebsten Skriptsprache geworden.
    • In gemeinsam genutzten Codebasen vermeide ich Dinge wie Listen-Comprehensions und halte mich an einen sehr einfachen Python-Stil. Python gilt zwar als Sprache, in der es "nur einen Weg" geben soll, in Wirklichkeit existieren aber zu viele Wege nebeneinander. Listen-Comprehensions machen mir persönlich Spaß und fühlen sich befriedigend an, aber wenn wirklich alle einem einzigen Weg folgen sollen, dürfte es diese Syntax meiner Meinung nach nicht geben.
  • Ich kann der Behauptung zustimmen, dass "ein Programm in dem Moment gültig sein sollte, in dem man es tippt", aber in der Praxis schreibt man Code nicht immer streng von links nach rechts und Zeile für Zeile. Oft schreibt man erst andere Teile oder deklariert Variablen erst später. Man verwendet zum Beispiel eine Variable und deklariert sie erst deutlich danach.
    • Da Code einmal geschrieben, aber dutzend- oder hundertfach gelesen wird, finde ich Code, den man sequentiell lesen kann, deutlich leichter lesbar als Code, bei dem man springen muss.
    • Eigentlich weicht diese Diskussion etwas vom Kern des Artikels ab, aber sie ist ein interessanter Blickwinkel.
    • Stimme vollkommen zu. Nur wenn ich eine neue Datei anlege, schreibe ich Code von Anfang an in Reihenfolge. Wenn ich ein Feld hinzufüge, gehe ich nicht erst zur Klassendefinition, sondern schreibe direkt den Code, der dieses Feld verwendet. Auch beim Verbessern von Bedingungen landet man oft vorübergehend in einem ungültigen Zustand, der Fehler erzeugt.
    • Dem stimme ich ebenfalls zu, aber ein damit verbundenes wichtiges Prinzip ist: Eine Struktur, die einem sagt "du bist mit dem Coden noch nicht fertig, also darfst du gar nicht kompilieren", ist zu extrem. Fehler sollten nicht blockierend sein, aber manche Sprachen blockieren unvollständigen Code vollständig, etwa bei unbenutzten Variablen oder fehlendem return.
    • Ich empfinde es manchmal als etwas lästig, dass die IDE nicht gut erkennt, in welcher Reihenfolge ich Code tatsächlich schreibe.
  • Einige IDEs haben Code-Template-Funktionen, bei denen man eine Abkürzung eintippt, die dann zu einer Codestruktur expandiert, und man füllt mit Tab die einzelnen Platzhalter aus. Die Reihenfolge der Tab-Sprünge muss dabei nicht zwingend von links nach rechts sein; es wäre zum Beispiel auch eine Reihenfolge wie {3} for {2} in {1} möglich. Solche Werkzeuge schaffen einen Kompromiss zwischen "gut lesbarer Syntax" und "leicht zu tippender Syntax". Ich tendiere dazu, Lesbarkeit der Syntax zu priorisieren, selbst wenn man dafür Tooling ausnutzt. Ich finde nicht, dass man unbedingt an einer for-in-Struktur festhalten muss.
  • Auf Hacker News scheint man sich derzeit einig zu sein, dass Python den Pipe-Operator verpasst hat. Als ich von Mathematica zu R gewechselt bin, habe ich den Wert von Pipes schnell erkannt. Beim Schreiben schrittweiser Datentransformationen in der Data Science ist das wirklich intuitiv und gut lesbar. Python wird zwar in vielen Bereichen eingesetzt, aber ich frage mich, ob Pipes auch außerhalb der Datenanalyse in anderen Kontexten Vorteile hätten. Ich versuche zu verstehen, warum Python sie nicht eingeführt hat.
    • Noch einen Schritt weiter als der Pipe-Operator ginge Reverse Assignment. Statt das Ergebnis wie in let foo = ... einer Variablen zuzuweisen, würde ich gern einmal eine Form wie ... =: foo ausprobieren.
    • Der Pipe-Operator in R, insbesondere im tidyverse-R, ist für mich die wichtigste "Killer-App". Für Datenarbeit gibt es meiner Meinung nach keine Sprache, die so leicht und angenehm ist. Statt ein Keksrezept verschachtelt als bake(divide(add(knead(mix(flour, water, sugar, butter)),eggs),12),450,12) zu schreiben, ist es mit Pipes als mix(flour, water, sugar, butter) %>% knead() %>% add(eggs) %>% divide(12) %>% bake(temp=450, minutes=12) viel einfacher und übersichtlicher.
    • Mit der Pipe-Syntax in Python pandas sieht das so aus:
      result = (df
       .pipe(fun1, arg1=1)
       .pipe(fun2, arg2=2)
      )
      
      In R hingegen:
      result <- df |>
       fun1(., arg1=1) |>
       fun2(., arg2=2)
      
      Beides lässt sich gut lesen, aber R hat den Vorteil, dass Pipes auch außerhalb von DataFrames besser funktionieren.
  • Dieser Streit ist fast schon ein Glaubenskrieg, ähnlich wie die Debatte FP (funktional) vs. OOP (objektorientiert) oder vim gegen emacs. In vim kommt der Operator zuerst, in emacs zuerst die Auswahlreihenfolge. Sprachen, die sich "wie Englisch lesen" lassen, haben in der Regel eine Struktur mit dem Verb zuerst, wie Lisp/Scheme. Sprachen wie Deutsch oder Tamil, in denen das Verb am Ende steht, passen eher zum OOP-Stil mit dem Nomen zuerst. Im Tamilischen sagt man zum Beispiel "water drink", im Englischen "drink water". Deshalb kann sich vim für manche Menschen natürlicher anfühlen. Nicht unbedingt, weil der eine Stil besser wäre als der andere, sondern weil Werkzeuge auch zu menschlichen Vorlieben gebaut werden; und heute ist mit Sprachmodellen ohnehin fast alles möglich.
    • Zur Frage "Wenn man es wie Englisch lesbar macht, kommt dann das Verb zuerst?": Bei imperativen Sprachen ja, aber in deklarativen Sprachen stünde bei einer englischartigen Lesart eher das Subjekt zuerst.
    • Zur Frage "Steht im Deutschen das Verb immer am Ende?": In einfachen Sätzen steht es tatsächlich an zweiter Stelle ("I drink water" → "Ich trinke Wasser"), also nicht vollständig am Satzende.
    • Zu der Aussage, dass in vim der Operator zuerst kommt: Kakoune arbeitet tatsächlich genau umgekehrt, und ich halte diesen Ansatz für viel logischer: Warum Kakoune
  • Andererseits ist Pythons Syntax from some_library import child_module sehr intuitiv. In JS wirkt eine Struktur wie import { asYetUnknownModule } from SomeLibrary deutlich weniger intuitiv.
    • In JS kann man mit Namespace-Import so schreiben:
      import * as someLibrary from "some-library"
      someLibrary.someFunction()
      
      Dann funktioniert die IDE-Autovervollständigung meiner Meinung nach tatsächlich gut: MDN-Erklärung zu Namespace-Imports
    • Ich finde die Fixierung auf das Schlüsselwort "from" etwas seltsam. Man könnte doch einfach so schreiben:
      import SomeLibrary {
        asYetUnknownModule
      }
      
  • ReScript hat genau aus diesem Grund seine APIs von data-last auf data-first umgestellt. Dank der hervorragenden Typinferenz bekommt man fast immer korrekte und typgerechte Autovervollständigung, was die Developer Experience sehr gut macht. Natürlich gibt es immer noch Probleme, wenn man Funktionen ohne Referenz deklariert, weil der Typ dann nicht bekannt ist, aber das lässt sich durch zusätzliche Typen oder einen vorangestellten Aufruf lösen. Dazu passt auch dieser Blogbeitrag: Vergleich von Data-first und data-last
  • Ich vertrete diese Sicht schon länger, und sie hängt auch damit zusammen, dass Ruby sich für mich viel einfacher angefühlt hat. Ich habe weder Python noch Ruby je tiefgehend auf Production-Niveau eingesetzt, aber ich verstehe nicht so recht, warum Python so breit installiert und genutzt wurde. Auch Ruby ist nicht ohne Schwächen, aber nur wenige Menschen scheinen beim Skripten überhaupt mit den komplexen Änderungen konfrontiert zu sein, die Python hatte. Zumindest hat Ruby in den letzten zehn Jahren keinen großen Versionskonflikt ausgelöst.
  • Insgesamt stimme ich den im Artikel angesprochenen Punkten voll zu. Ich denke, eine Struktur, bei der der Kontext zuerst kommt und die sich von links nach rechts lesen lässt, passt besser zu LLMs und Autovervollständigung. Allerdings würde ich den Beispielcode eher nicht als len(list(filter(lambda line: all([abs(x) >= 1 and abs(x) <= 3 for x in line]) and (all([x > 0 for x in line]) or all([x < 0 for x in line])), diffs))) schreiben, sondern lieber mit einem NumPy-Array arbeiten; so muss man keine neuen Listen im Speicher anlegen und kann die gesamte Zeile viel besser auf einmal verarbeiten, zum Beispiel:
    sum(1 for line in diffs
      if ((np.abs(line) >= 1) & (np.abs(line) <= 3)).all()
        and ((line > 0).all() or (line < 0).all()))
    
    Das spiegelt "von links nach rechts" deutlich besser wider.
    • Auch die NumPy-Version ist noch etwas kryptisch ("line > 0" ist okay, aber die Broadcasting-Regeln können kompliziert werden), doch die vom Autor genannten Beispiele in JavaScript oder die Collection-APIs aus streng typisierten Sprachen wie C#, Java und Scala sind sauberer. Mein Favorit ist Kotlin, weil man es so schreiben kann:
      diffs.countIf { line -> 
        line.all { abs(it) in 1..3 } and ( 
          line.all { it > 0} or
          line.all { it < 0}
        )
      }