Der Kern von Rust
(jyn.dev)- Rust ist eine Sprache, in der verschiedene Konzepte eng miteinander verflochten sind; selbst zum Verständnis eines einfachen Programms muss man viele Elemente gleichzeitig lernen
- Funktionen, Generics, Enums, Pattern Matching, Traits, Referenzen, Ownership,
Send/Sync,Iteratorusw. sind allesamt zentrale Elemente, die auf ihr Zusammenspiel hin entworfen wurden - Im Vergleich zu JavaScript kann man in JS bereits mit dem Verständnis einiger weniger Konzepte Code schreiben, während man in Rust erst mit dem Verständnis des Gesamtkontexts der Sprache sinnvoll Code verfassen kann
- Diese Komplexität von Rust erhöht zwar die Lernhürde, bietet zugleich aber Sicherheit und Konsistenz und beeinflusst maßgeblich die Art, wie Code entworfen wird
- Diese sprachliche Verzahnung macht Rust besonders, und die Vision eines „kleineren Rust“ lenkt den Blick erneut auf eine präzise verzahnte Sprachphilosophie
Die Schwierigkeit, Rust zu lernen
- Obwohl Rust eine hohe Einstiegshürde hat, haben viele Menschen zu Dokumentation, APIs und verbesserten Diagnosen beigetragen
- Zu den grundlegenden Konzepten gehören Funktionen als First-Class-Objekte, Enums, Pattern Matching, Generics, Traits, Referenzen, Borrow Checker, Concurrency-Sicherheit und Iteratoren
- Diese Konzepte hängen voneinander ab und sind miteinander verflochten, weshalb sie sich schwer einzeln lernen lassen; auch die Standardbibliothek nutzt die meisten dieser Funktionen
- Selbst um rund 20 Zeilen Rust-Code zu verstehen, muss man zugleich mehrere Elemente erfassen: funktionale Paradigmen,
Resultund Fehlerbehandlung, generische Typen, Enums, Iteratoren usw.
Vergleich von Rust und JavaScript
- Wenn man dasselbe Programm zur Erkennung von Dateiänderungen in Rust und JS schreibt, sind in Rust zahlreiche Sprachkonzepte miteinander verflochten
- In JS reicht es grundsätzlich aus, Funktionen und den Umgang mit null zu verstehen, um funktionsfähigen Code zu schreiben
- Das bedeutet nicht einfach, dass Rust schwieriger ist, sondern zeigt, dass Rust eine Sprache ist, deren Design ein strukturelles Verständnis des Ganzen erfordert
Rusts miteinander verzahntes Design
- Der Kern von Rust ist die Verbindung organisch entworfener Funktionen
- Enums sind ohne Pattern Matching unhandlich, und Pattern Matching ist ohne Enums ebenfalls eingeschränkt
ResultundIteratorlassen sich ohne Generics nicht implementieren- Die Konzepte
Send/Syncund die Einschränkungen vonprintlnlassen sich nur mit Traits sicher ausdrücken - Der Borrow Checker gewährleistet
Send/Sync-Sicherheit durch die Analyse dessen, was Closures erfassen
- Diese gegenseitige Verzahnung macht Rust nicht bloß zu einer Sammlung von Features, sondern zu einem integrierten Sprachsystem
Die Vision eines kleineren Rust
- 2019 erwähnte without.boats „Smaller Rust“ und diskutierte die Möglichkeit eines kleinen, verfeinerten Rust
- Heute ist Rust deutlich größer geworden, doch das Konzept eines kleineren Rust erinnert weiterhin an das Wesen eines präzise ineinandergreifenden Sprachdesigns
- Rusts Reiz liegt darin, dass sprachliche Elemente zwar unabhängig voneinander sind, in ihrer Kombination jedoch starke Ausdruckskraft und Sicherheit bieten
Fazit
- Rust ist schwer zu lernen, aber die Konsistenz und Integration seiner miteinander verflochtenen Konzepte sind eine große Stärke
- Dank dieser Struktur bringt Rust Entwickler dazu, Code nicht einfach nur zu schreiben, sondern eine Denkweise zu entwickeln, die Sicherheit und Performance zugleich berücksichtigt
- Das Wesen von Rust liegt in einer „kleinen, präzise gestalteten Kernsprache“, und diese Philosophie bleibt auch im heutigen, erweiterten Rust wichtig
1 Kommentare
Hacker-News-Kommentare
fs.watch-Dokumentation steht ausdrücklich, dass man im Callback unbedingt prüfen muss, obfilenamenullsein kann. In Rust würde diese Tatsache im Typsystem abgebildet und zur Behandlung zwingen, während man in JS leicht schlampigen Code schreiben kann. Zugehörige Dokunull-Prüfung erzwungen. Ich finde, das ist ein gutes Beispiel dafür, dass TS in JS eine relativ reibungsarme Stufe darstellt, die der Korrektheit von Rust etwas näher kommtfor path in pathsmüsstefor (const path of paths)heißen. JS wirft ohne Klammern zwar sofort einen Fehler, aber der Unterschied zwischeninundofist, dass über Indizes des Iterables statt über die Werte iteriert wird, sodass am Ende tatsächlich der Index als String in das erste Argument vonfs.watchgelangt. Selbst TypeScript erkennt diesen Fehler womöglich nichtkindkommt. Inconsole.log("${kind} ${filename}")müsste es eigentlicheventType(ein String) stattkindseinprintlnin Rust kann nur Typen ausgeben, die dasDisplay- oderDebug-Trait implementieren. Deshalb kannPathnicht direkt ausgegeben werden. Nicht jedes OS speichert Pfade in UTF-8, und Rusts String-Typen sind alle UTF-8. Das heißt, die Ausgabe einesPathkann verlustbehaftet sein.Pathliefert über die Methodedisplayeinen Typ zurück, derDisplayimplementiert. Rust hat das im Typsystem verankert, während sich in JS/TS kaum klar ausdrücken lässt, dass interne Strings UTF-16 sind, und man für nicht-Unicode-Pfade eigentlich direktTextEncoder/Decoderverwenden muss, um korrekt damit umzugehen. Aus früherer Erfahrung weiß ich: Wenn ein Server Text in Shift_JIS liefert und man ihn mitresponse.text()liest, bekommt man zur Laufzeit nur einen leeren String zurück. Wenn man mit Encoding-Problemen nicht vertraut ist, kann man in so einer Situation beim Debuggen locker mehrere Tage verlieren. Außerdem enthält das JS-Beispiel Bugs und Syntaxfehler, die im Rust-Code nicht vorhanden sind (in der Schleife braucht manfor-ofstattfor-in). Man kann bei diesem Beispiel auch nicht wirklich davon sprechen, dass nur „First-Class Functions“ verwendet werden; man braucht wie in Rust auch ein Verständnis für Iteratoren, und es wird CommonJS verwendet. Außerdem muss manasync/await, Promises und Top-Level-awaitneu lernen, und Top-Level-awaitwird erst seit Kurzem in einigen Laufzeiten einschließlich Node unterstützt. In manchen JS-Engines (z. B. Hermes von React Native) wird es immer noch nicht unterstütztGenau deshalb benutze ich weiterhin Rust. Das Beispiel ist nur eines von vielen, aber solche kleinen Probleme und Fallstricke liegen in anderen Sprachen überall herum. Sie treten vielleicht nicht einzeln auf, aber über den gesamten Lebenszyklus eines Programms hinweg summieren sie sich, und dann springen einem ständig seltsame Bugs entgegen, die man immer weiter suchen muss. In Rust passiert das nicht. Das Typsystem blockiert im Vorfeld absurd viele Fälle. In der Praxis ist es so: Wenn man Software in Rust fertig implementiert und veröffentlicht hat, fügt man höchstens gelegentlich Features hinzu, und der typische Aufwand für allgemeine Bugfixes verschwindet fast völlig. Natürlich kann es überall logische Fehler geben, aber weil Probleme aus dummen Typ-/Struktur-Mismatches von vornherein ausgeschlossen werden, ist Produktivität und Wartbarkeit eine völlig andere Erfahrung als in anderen Sprachen
Ich habe persönlich den Eindruck, dass es in JS/TS nicht viele Entwickler gibt, die Thenables/Promises und
async/awaitwirklich richtig verstehen. Ich habe auch so etwas gesehen:Da wird ein Wrapper in Callback-Form einfach noch einmal in ein Promise gepackt und dann in einer
async-Funktion erneut verwendet. Jedes Mal tut mir dabei das Herz weh. Solchen Code sehe ich tatsächlich überall. Und wenn man dann noch Modul-Imports, asynchroneimport(), Transpiling, Code-Splitting usw. dazunimmt, wird es wirklich komplexrustfmt,rust-analyzer, Bugfixes inrustcund besseres Error Reporting in Cargo. Ich selbst schreibe jeden Tag Reproduktionsskripte für Issues mit Cargo Script-Zscriptzu suchen und dazu zu recherchieren. Das läuft seit 2023, und es gibt Open Issues, die schon fast fertig wirken. Im ZomboDB-Repository habe ich ebenfalls gesehen, dass die Build-Pipeline mit Rust abgewickelt wird, aber den vollständigen Kontext habe ich nicht ganz verstanden. Ich möchte noch erwähnen, wie nützlich Cargo-Frontmatter für die Portabilität von Skripten ist. Man muss nur eine Datei teilen und kann dann, anders als bei Python oder Node.js, Abhängigkeiten sofort holen und verwenden, ohne zusätzliche Installation oder Initialisierung#!/some/pathbeginnt, wird von der Shell einfach mit dem angegebenen Kommando ausgeführt, wobei der gesamte Dateiinhalt überstdinübergeben wirdasyncundconst. Wenn das gemeint ist, hätte man meiner Meinung nach einfach direkt sagen sollen: „Rust vorasyncundconstwar kleiner und sauberer.“ Im Haupttext wird das leider nicht so unmittelbar ausgesprochenCopy-Trait, Reborrowing, Deref-Coercion, automatischesinto_iterin Schleifen, automatischedrop-Aufrufe am Ende eines Scopes (man könnte das auch explizit aufrufen oder den Compiler Fehler werfen lassen), das implizite:Sizedbei Trait Bounds, Lifetime-Elision, Match-Ergonomics und allerlei weitere Automatisierungen/Bequemlichkeiten entfernen, und dann bekäme man ein wirklich mechanisch einfacheres Rust. Allerdings wäre eine solche Sprache im Alltag sehr unbequem zu benutzen. Ironischerweise wurden diese Dinge in Wahrheit für Anfänger entworfenasyncundconstkleiner und sauberer war. Dass ich es nicht direkter formuliert habe, liegt daran, dass ich mit Leuten befreundet bin, die an diesen Features gearbeitet haben. Matklad hat das auf lobste.rs sehr gut ausgedrückt: Rust von 2015 war vollständiger und konsistenter, aber Rusts Vision war nie vollkommene Konsistenz (coherence), sondern eine Sprache zu werden, die in der Industrie nützlich ist.into()und dasFrom-Trait behandeln Typkonvertierungen zu implizit. Auch in der Standardbibliothek gibt es viele dieser „Bequemlichkeits“-Funktionen. Am Ende wird der Typ eines Objekts undeutlich, und es wird schwierig, Funktionsaufrufe mit ihren Implementierungen zu verknüpfen (auch wenn eine IDE dabei etwas hilft)intstehen“, und das Debugging ist deutlich schneller erledigt. Man verbringt nicht den ganzen Tag mit Compilerfehlern, sondern spart sich eine Woche Leid mit Laufzeitfehlernconstin Rust könnten sogar helfen, sich später das Verlernen schlechter Gewohnheiten aus traditionellen Sprachen zu ersparennoneist“ fühlen sich viel schwieriger an als ein Laufzeitabsturz in einem Testfall, dessen Stelle man sofort findet. Wenn man selbst Zeile für Zeile Werte ausgibt und so Fehler sucht, lässt sich vieles schnell lösen; an kryptischen Compilerfehlern kann man dagegen wirklich lange hängenbleibenmem-Moduls; wenn man die Struktur der Interfaces wirklich verstehen will, ist std::mem ein guter Startpunkt