Statische Typen und Schaufeln
(carefully.understood.systems)- Der Rückgang der Beliebtheit statischer Typen von den 2000er-Jahren bis in die frühen 2010er und ihr erneuter Anstieg in der zweiten Hälfte der 2010er lässt sich mit der verbesserten Qualität statischer Typsysteme erklären
- Dynamische Typsysteme werden damit verglichen, dass Menschen Zustand und Inhalt von Variablen und Feldern selbst beurteilen müssen – der Computer hilft dabei weder noch steht er im Weg, wie beim Graben mit bloßen Händen
- Frühere statische Typsysteme wie in frühem Java oder C++98 konnten nicht einmal bei der Unterscheidung zwischen nullable und non-nullable Zeigern helfen und werden mit einer Papierschaufel verglichen, die einen dazu zwingt, Typnamen immer wieder auszuschreiben
- Moderne Typsysteme wie TypeScript, Haskell, MyPy, Swift und Rust unterstützen durch Null-Behandlung, Sum Types bzw. Union Types und Type Inference deutlich besser beim Prüfen von Programmfehlern und beim Ausdrücken von Zuständen
- Mit der weiten Verbreitung von IDE-Funktionen wie der automatischen Vervollständigung von Methodennamen führen die in statische Typsysteme eingegebenen Informationen über die Fehlerprüfung hinaus auch zu Produktivitätsvorteilen
Kernaussage
- Die Popularität statischer Typen ist nicht bloß ein Trend, sondern steigt wieder, weil sich die Qualität von statischen Typsystemen, die breit einsetzbar sind, verbessert hat
- Die Metapher lautet: Mit einer guten Schaufel gräbt es sich besser als mit bloßen Händen, aber wenn es nur Schaufeln aus Papier gibt, sind bloße Hände die bessere Wahl
- Bei dynamischen Typsystemen müssen Menschen selbst darüber nachdenken, in welchem Zustand und mit welchem Inhalt sich Variablen und Felder eines Programms befinden; der Computer unterstützt diese Einschätzung nicht
- Ein schlechtes statisches Typsystem kann mehr Last als Hilfe sein; das wird mit dem Graben mit einer Papierschaufel verglichen
Grenzen früherer statischer Typsysteme
- Früh verbreitete statische Typsysteme aus den 1990er- und frühen 2000er-Jahren wie in frühem Java oder C++98 halfen nicht einmal bei der einfachen Unterscheidung zwischen nullable und non-nullable Zeigern
- Frühere statische Typsysteme werden als Strukturen beschrieben, die keine Sum Types, sondern nur Produkt-Typen besitzen
- Frühere statische Typsysteme zwangen dazu, Typnamen an vielen Stellen manuell auszuschreiben
- Code wie
BufferedReader bufferedReader = new BufferedReader(new FileReader(filename));wird als kleine Katastrophe beschrieben
Verbesserungen moderner Typsysteme
- Moderne Typsysteme wie TypeScript, Haskell, MyPy, Swift und Rust bieten Möglichkeiten, zwischen nullable und non-nullable Typen zu unterscheiden
- Als Beispiele werden Haskell mit
Maybe t, TypeScript mitT | null, Swift mitT?und Rust mitOptional<T>genannt; damit kann das Typsystem anzeigen, wo Null-Prüfungen nötig sind und wo sie fehlen - Es wird erklärt, dass man Null-Pointer-Fehler zur Laufzeit dadurch fast gar nicht mehr sieht
- Moderne Typsysteme bieten mindestens eines von beidem: Sum Types oder Union Types, und ermöglichen damit die Praxis "Make invalid states unrepresentable"
- Dadurch lassen sich Objekte, die Zustandsmaschinen darstellen, so ausdrücken, dass einzelne Felder nur in den jeweils passenden Zuständen vorhanden sind
- Moderne Typsysteme bieten Type Inference; wenn der Compiler
let x = 5;als Zahl erkennen kann, musslet x: number = 5;nicht ausgeschrieben werden
IDE-Funktionen und Fazit
- Durch die breite Verfügbarkeit von IDE-Funktionen wie der automatischen Vervollständigung von Methodennamen ist der Nutzen statischer Typsysteme weiter gestiegen
- In den 1990er-Jahren war Intellisense ein Kernmerkmal von Visual Studio, in den 2020er-Jahren bieten jedoch fast alle IDEs und Editoren ähnliche Funktionen
- Die Informationen, die in ein statisches Typsystem eingegeben werden, dienen nicht nur der Prüfung von Programmfehlern, sondern erzeugen zusätzliche Produktivitätsvorteile
- Gute dynamische Typsysteme sind besser als schlechte statische Typsysteme, aber heute stehen deutlich bessere statische Typsysteme zur Verfügung als früher
1 Kommentare
Lobste.rs-Kommentare
Der Artikel ist gut, aber ich stimme ihm nicht vollständig zu. Die statischen Typsysteme der frühen 2000er waren zwar nicht großartig, aber meiner Meinung nach immer noch viel besser als gar keine statischen Typen.
Geschlossene Summentypen gab es nicht, aber mit Subtyping ließ sich ein erheblicher Teil modellieren, und nicht-nullbare Typen gab es zwar ebenfalls nicht, doch C++-Referenzen und Nicht-Pointer-Typen sowie Javas primitive Typen deckten einen Teil davon ab. In Ruby oder JavaScript waren dagegen nicht nur alle Typen nullable, sie konnten außerdem wie Strings, wie Integers oder wie jeder andere Typ im Programm behandelt werden, was die Lage noch schlechter machte.
Ein großer Grund für den Stimmungswandel bei statischen Typen war meiner Ansicht nach, dass während des Web-2.0-Social-Network-Booms der First-Mover-Vorteil wichtiger war als alles andere. Selbst wenn man mit Ruby oder Python technische Schulden anhäufte, war schnelles Launchen und Iterieren besser, als wie Friendster oder Digg verdrängt zu werden, und wenn etwas langsam war, konnte man mit dem damals leicht verfügbaren Niedrigzinskapital einfach mehr Server kaufen.
Mit dem anschließenden Mobile-Boom lief Software dann auf eingeschränkten Benutzergeräten, die man nicht kontrollieren konnte, und langsame dynamisch typisierte Apps waren einfach tatsächlich langsam; außerdem ließen sich Typfehler nicht wie auf dem Server elegant über einen Top-Level-Response-Handler abfangen und beheben. In dieser Umgebung wirkten Sicherheit und Performance statischer Typen deutlich überzeugender.
Anfang der 2000er hätte ich dem noch zugestimmt, denn die damaligen Typsysteme erzwangen oft nur Eigenschaften, die ohnehin fast nie falsch waren, und legten dabei Einschränkungen auf, die beim Strukturieren von Code nicht halfen. Besonders die Kopplung von Subtyping und Implementierungsvererbung war unflexibel.
Mit moderneren Typsystemen habe ich meine Meinung geändert. In snmalloc erzwingt das C++-Typsystem eine Zustandsmaschine für Speicherbesitz, und in anderen Codebasen prüft es das korrekte Overflow-Verhalten von Ringpuffer-Zählern. Beides sind häufige Fehlerquellen, deren Debugging lästig ist, wenn sie falsch sind; und tatsächlich hat der Compiler bei Code, den ich für korrekt hielt, Fehler gemeldet und so verhindert, dass Bugs überhaupt in den Tree gelangen.
Wenn man im IDE
.drückt, ein Stück des Methodennamens eintippt und dann beim richtigen Vorschlag Enter drückt, spart man alle paar Sekunden 2 Sekunden; und wenn man nicht weiß, welche Methoden es gibt, spart man auch die 30 Sekunden, die sonst für das Nachschlagen der Klassendefinition draufgehen. Das Prinzip ist auch unter https://grugbrain.dev/#grug-on-type-systems gut beschrieben.Weil man Zeilen mit Methodenaufrufen viel häufiger schreibt als Typannotationen für Funktionsparameter, fällt dieser Trade-off überwältigend zu Ungunsten dynamischer Typisierung aus. Der wirklich wertvolle Teil war nie, zur Laufzeit scheiternden Unsinnscode zu erlauben, sondern lokale Variablentypen wegzulassen, und statisch typisierte Sprachen mussten das von vornherein gar nicht verbieten.
In den seltenen Codebasen, die Typsysteme ernsthaft nutzten, stapelten sich seitenweise Code ohne Aussagekraft, dazu kamen trotzdem Berge von Laufzeit-
if-Abfragen, und in Java wurde das Programm mit wachsender Typhierarchie in der Praxis auch noch langsamer. Die meisten Codebasen nutzten Typen nur lückenhaft und bauten viele Laufzeitprüfungen ein, sodass der nötige Testumfang gegenüber dynamischen Typsystemen nicht wesentlich kleiner war.Dynamisch typisierte Sprachen boten keine statischen Vorteile, waren dafür aber kompakt, leichter zu lesen und zu reviewen und auch leichter zu testen. Das galt besonders in Umgebungen wie den Dependency-Injection-Frameworks der späten 90er und frühen 2000er, in denen man für jeden neuen Service mehrere XML-Dateien anfassen musste. Und man konnte arbeiten, ohne ein IDE zu brauchen, das die Hälfte des RAM fraß.
Genau so sah mein früher Berufsalltag aus, deshalb stimme ich dem Artikel weitgehend zu. Das Verhältnis von Kosten zu Nutzen bei Java 1.4 bis Java 6 war so schlecht, dass ich statisch typisierte Sprachen fast ganz aufgegeben hätte. Erst als ich einige Jahre später hobbymäßig mit Haskell spielte, wurde mir klar, dass auch statische Typen ein vernünftiges Kosten-Nutzen-Verhältnis haben können und das Problem Java war. Der Essay „python is not java“ zeigt diese dunkle Zeit ebenfalls gut.
Ich bezweifle, dass wir nach dem Aufstieg statischer Typen zum Zeitgeist den Zuverlässigkeitsgewinn in unserer Software wirklich gesehen haben.
Ich dachte immer, der Vorteil statischer Typen liege viel stärker im unmittelbaren Feedback während der Entwicklung und in der Verringerung schwerwiegender Laufzeitfehler; theoretisch sind solche Fehler zwar immer möglich, aber in der Praxis schienen sie mir nie so häufig vorzukommen.
undefinedundnullaufzurufen, stark zurück.Junioren und einige Senioren waren anfangs skeptisch und meinten, überall würden
@ts-ignoreauftauchen, aber tatsächlich waren es nur etwa drei, einschließlich solcher, die durch kaputte Typen in Abhängigkeiten verursacht wurden. Früher ist mir etwa einmal pro Woche im Entwicklungs-Branch die App wegen Typverwechslungen abgestürzt und hat meine Arbeit blockiert; heute kann ich mich nicht einmal erinnern, wann das zuletzt passiert ist.Schon allein
tsczufriedenzustellen reduziert typbezogene Bugs, selbst wenn ich den Code nicht selbst geschrieben habe. Andererseits sind Linter heute übermäßig eifrig, und beim Versuch, Tools wie Sonar zufriedenzustellen, habe ich echte Refactoring-Schäden gesehen. 95 % der Warnungen waren falsch, 3 % waren Bugs im Tool, und selbst die nützlichen 2 % waren nicht die eigentliche Ursache realer Bugs. Ich habe eine Woche damit verbracht, die Codebasis anzupassen, einen Bug gefixt und dabei im Prozess zwei weitere eingebaut.Die Arbeit,
tsczufriedenzustellen, erzeugte grob geschätzt pro Tag zwei reine Bugfixes und eine Regression, aber die Regressionen waren meist weniger schwerwiegend als ein kompletter Crash und eher falsches Verhalten.Wenn man dazu noch Property-based Testing nimmt, dauerte das im Schnitt 2–4 Stunden und deckte immer mindestens einen Bug auf. Wenn der Code sich für Property-based Testing eignet, sollte man es tun.
Mit dem günstigen Modell DeepSeek V4 Flash haben wir den Testumfang ausgeweitet und dabei darauf geachtet, keinen Testmüll zu erzeugen; so haben wir pro Tag ungefähr 2–3 Logikfehler behoben, ohne Crashes. Das Testpaket ist allerdings nur gerade so wartbar.
Als wir Junioren mit Sonnet und Opus 4.5 bzw. 4.6 grob Tests erstellen ließen, erzeugten die Modelle nur Tests, die das „aktuelle Verhalten dokumentieren“, sodass die Wirkung gering war, und das Testpaket war so unwartbar, dass es verworfen werden musste.
Modellbasiertes Testen ist sehr gut darin, Bugs zu finden, aber die Einrichtung ist kompliziert, und es ist extrem mühsam, die Modelle dazu zu bringen, in alle Ecken vorzudringen, statt nur Zyklen auf oberflächliche Funktionen zu verschwenden. So etwas wie ein profilbasiertes modellbasiertes Fuzzer-Tool fände ich interessant.
Kurz gesagt: Typprüfer fangen schwere Fehler und viele Verwechslungen gut ab, und Property-based Testing ist hervorragend. Gewöhnliche Tests erfordern viel Disziplin, wenn man dauerhaft Nutzen daraus ziehen will.
Am wenigsten kann ich zustimmen, wenn TypeScript hier mit einem guten Typsystem gleichgesetzt wird.
awaithinweg verengt, hat mich mehrfach gebissen. Trotzdem hat es die Lage dramatisch verbessert.Ehrlich gesagt habe ich mich am strukturellen Typsystem am Ende auch gewöhnt, und ich denke, es wird sich positiv auf künftiges Sprachdesign auswirken.
Diese Behauptung ist wenig überzeugend. Ordentliche Programmiersprachen mit algebraischen Datentypen und Typinferenz gab es schon seit Mitte der 90er.
Die Typsysteme von Java und C++ waren sehr dürftig, aber SML, OCaml und Haskell gab es bereits, und sie fühlten sich dem heutigen Stand gar nicht so unähnlich an. Wenn die Leute diese Sprachen nicht benutzt haben, dann ist das eine Frage von Kultur, Akzeptanz und unausgesprochenen Anforderungen, nicht etwas, das sich allein damit erklären ließe, dass „benutzbare Typsysteme nicht gut genug waren“.
Oder falls die Behauptung lautet: „Die Typsysteme der damals populären Sprachen waren schlecht, und die Typsysteme der heute populären Sprachen sind besser, deshalb sind Typsysteme populärer geworden“, dann klingt das wie ein Zirkelschluss.
Auch beim Unterschied zwischen Sprachen, die zusammen mit einem Typsystem entworfen wurden, und Sprachen, die ursprünglich ohne Typen entworfen und später mit einem Typsystem versehen wurden, steckt viel Nuance drin.
Aus der Perspektive von jemandem, der ursprünglich dynamische Typisierung bevorzugt hat, finde ich den Beitrag ziemlich fair. Heute arbeite ich mit C#; als Hobby nutze ich Lisp, und früher habe ich auch Python verwendet.
Als ich Java 5 verwenden musste, habe ich ständig mit dem Typsystem gekämpft, meist wegen schlechter Entscheidungen von Bibliotheksentwicklern. Nach dem Wechsel zu C# um 2010 war das Typsystem nicht aktiv schädlich, aber überwiegend redundant und konnte auch die in Python häufigste Typverwechslung, nämlich Null-Pointer-Exceptions, nicht verhindern.
Wirklich hilfreich wurde das C#-Typsystem erst etwa 2020 mit den nicht-nullbaren Referenztypen. Dieses Jahr kommen auch native Union-Typen dazu, aber Bibliotheken für Union-Typen mit erzwungener Vollständigkeit waren mindestens seit 2016 möglich, und ich benutze sie seit 2020.
Ich denke, Mode spielt weiterhin eine Rolle, aber ein Teil davon ist nicht schlecht. Trendsprachen mit ausdrucksstärkeren Typsystemen haben auch Verbesserungen in die gewöhnlichen Sprachen gebracht, mit denen wir unser Geld verdienen.
Haskell und sein Typsystem gab es schon in den 2000ern. Es wurde damals nicht so breit genutzt wie heute, aber es existierte eindeutig, daher sollte diese Behauptung in diesem Punkt ergänzt werden.
Ich persönlich denke, dass TypeScript ein großer Faktor dabei war, Nutzer von Mainstream-Sprachen mit besseren Typsystemen vertraut zu machen. Neben der Qualität und der Unterstützung durch Microsoft hatte es den Vorteil, auf JavaScript angewendet zu werden, und JavaScript brauchte Typen dringender als Python. Wegen „Undefined is not a function.“ und „The good parts.“
„Real World Haskell“ erschien 2008 und hatte das Ziel, Haskell für Mainstream-Programmierer attraktiver wirken zu lassen. Ich weiß nicht, wie sehr es dabei geholfen hat, die gute Nachricht zu verbreiten.
In der Java-Welt brachte Scala 2004 schicke Typen, und für .NET erschien F# 2005. Scala hat vielleicht die meisten auffälligen Nutzer wie Twitter gewonnen, war aber nicht in der Position, einen großen Anteil der Nutzer seiner Plattform aufzusaugen wie TypeScript, und es war auch nicht attraktiv genug, um wie Rust oder Go in großem Stil Nutzer anderer Sprachen anzuziehen.
Im direkt folgenden Absatz erwähnt er Haskell als „modernes Typsystem“, aber der Anteil der Leute mit Haskell-Erfahrung in den späten 90ern und frühen 2000ern lag praktisch bei 0 %, selbst wenn man rein persönliches Herumprobieren mitzählt. Im Artikel geht es darum, wie die Mehrheit der Entwickler statisch typisierte Sprachen damals erlebt hat und warum diese Mehrheit statisch typisierte Sprachen kollektiv mied.
Um zum Beispiel in OCaml
dunezu benutzen, muss manopam-Dateien,dune-Dateien, dieocaml module-Syntax und dieocaml-Syntax verstehen. Die optionalen Compiler-Erweiterungen in Haskell wirken genauso einschüchternd.Im Gegensatz dazu muss man bei
cargonurtomlund Rust kennen.