- Nach dem Wechsel von JavaScript zu Rust, um sich auf WebAssembly zu konzentrieren, bewertet der Autor über drei Jahre hinweg den praktischen Wert von Rust – mit Wick, Produktions-Deployments, einem ebook und rund 100 crates.io-Paketen
- Borrow Checker, reichhaltiges Typsystem, funktionale Muster und das Fehlen von
nullverhindern viele Fehler bereits zur Compile-Zeit und ermöglichen es, große Codebasen mit weniger Tests zu pflegen - Clippy und Cargo workspaces sind mächtig, doch Lücken bei Tools und Ökosystem, etwa globale lint-Konfiguration und workspace-Veröffentlichung, führen zu Betriebskosten
- async, Refactoring sowie der Umgang mit generics, lifetimes und trait constraints bleiben Bereiche mit mehr Reibung als in JavaScript oder Go
- Rust ist robust und vielseitig, bringt aber hohe Kosten bei Recruiting, Lernen, schneller Iteration und Fehlersuche mit sich; es eignet sich daher eher, wenn der Umfang klar ist oder die Anfangskosten tragbar sind
WebAssembly führte zur Wahl von Rust
- Vor einigen Jahren gab der Autor seine bisherige Arbeit auf, um sich zu 100 % auf WebAssembly zu konzentrieren; damals hatte Rust die beste Unterstützung für die Kompilierung nach WebAssembly
- Auch funktionsreiche WebAssembly-Runtimes basierten auf Rust, sodass Rust unter den Optionen die realistischste Wahl war
- Danach entstand Wick, ein Application Framework und eine Runtime, die WebAssembly als zentrales Modulsystem nutzt
- Über drei Jahre sammelte der Autor Rust-Erfahrung durch mehrere Produktions-Deployments, ein ebook und die Veröffentlichung von rund 100 Paketen auf crates.io
Mehr Code mit weniger Tests pflegen
- In Rust schrieb der Autor zunächst Tests wie in anderen Sprachen, stellte dann aber fest, dass er Tests für Dinge schrieb, die nicht fehlschlagen konnten, sobald der Code kompiliert
- Wenn man
unsafe {}-Blöcke und panic-anfällige Methoden wie.unwrap()vermeidet, werden viele Probleme grundsätzlich umgangen - Der Borrow Checker, das reichhaltige Typsystem, funktionale Muster und Bibliotheken sowie das Fehlen von
null-Werten reduzieren den Testaufwand - Im Wick-Projekt wurden mehr als 70.000 Zeilen Code mit deutlich weniger Tests gepflegt, als in anderen Sprachen nötig gewesen wären
- Wenn Tests erforderlich sind, lassen sie sich dank Rusts integriertem Test-Harness leicht direkt neben dem Code ergänzen
Rust verändert auch Coding-Gewohnheiten in anderen Sprachen
- Der Rust-Compiler beanstandet immer wieder Code, der in anderen Sprachen als normal gegolten hätte; durch diesen Prozess verändern sich die eigenen Coding-Gewohnheiten
- Inzwischen fühlt es sich auch in anderen Sprachen unangenehm an, wenn die Reihenfolge von Codezeilen seltsam ist oder Rückgabewerte nicht geprüft werden
- Auch Runtime-Fehler lösen nun deutlich stärkere Ablehnung aus als früher
- Rusts Strenge ist unbequem, aber wenn man sich daran gewöhnt hat, vom Compiler geschützt zu werden, fällt die Rückkehr zu anderen Sprachen schwer
Clippy ist mehr als nur ein Linter
- Clippy ist der Linter für Rust, fühlt sich aber eher wie ein hilfreicher Assistent an, der Alternativcode vorschlägt, als wie ein simples Prüfwerkzeug
- Die Rust-Standardbibliothek ist sehr groß; ihre Funktionen verteilen sich auf viele Typen, traits, Makros und Funktionen, was das Finden der passenden API erschwert
- Mehrere Regeln erkennen gängige Muster, die sich besser durch Methoden oder Typen aus der Standardbibliothek ersetzen lassen
- Beispiel: manual_is_ascii_check
- Hunderte Regeln behandeln Performance, Lesbarkeit und unnötige Indirektion und liefern, wenn möglich, auch Ersatzcode
- Projektweite lint-Konfiguration schien über ein Cargo-Issue möglich zu werden; bis dahin musste Wick jedoch die inline-lint-Konfigurationen von Dutzenden crates per Skript automatisch aktualisieren
Im Ökosystem gibt es Lücken, die man hinnehmen muss
- Das Problem der globalen Clippy-Konfiguration ist ein Beispiel für Ökosystemlücken, auf die man bei Rust-Tools und -Bibliotheken häufig stößt
- Die zugehörigen Issues sind inzwischen geschlossen, waren aber über mehrere Jahre offen, und die Lösung dauerte lange
- Rust zieht neue Nutzer an, nicht zuletzt weil es lange als „beliebteste Sprache“ galt, doch dieser Zulauf führte nicht unmittelbar zu dramatischen Verbesserungen bei Bibliotheken und Tools
- Häufig entstanden einmalige forks für bestimmte Use Cases; auch bei Wick gab es ähnliche Erfahrungen beim Versuch, PRs einzureichen
- Mögliche Gründe sind der Druck, stabile APIs zu erhalten, und das stark ausdifferenzierte Typsystem
- Für Library-Maintainer kann schon eine kleine Änderung zu einem Major-Version-Change führen und ist daher schwer anzunehmen
- Auch die Belastung, Rust-Code zu schreiben, der die Anforderungen aller erfüllt, ist hoch
Reibung bei Cargo, crates.io und workspace-Veröffentlichung
- Die Repository-Struktur von Wick orientierte sich an populären Projekten und wirkte anfangs vernünftig, doch in der Veröffentlichungsphase zeigten sich Probleme
- Modulgroße crates mit Cargo zu bauen, zu testen und zu verwenden, ist einfach; die Veröffentlichung auf crates.io ist jedoch eine andere Sache
- Ein Paket kann nur veröffentlicht werden, wenn alle referenzierten crates einzeln auf crates.io veröffentlicht sind
- Es ist sinnvoll, die Veröffentlichung eines crates zu verhindern, das von einem Paket abhängt, das nur im lokalen Dateisystem existiert
- In der natürlichen Struktur großer Projekte, die in kleine interne Module aufgeteilt sind, kann man jedoch keine sub-crates veröffentlichen, die nur innerhalb des Parent-crates existieren
- Es gab die Korrektur, dass crates mit lokalen dev dependencies veröffentlicht werden können, wenn in
Cargo.tomlkeineversionenthalten ist - Die Cargo-workspace-Unterstützung selbst ist hervorragend, und die Verwaltung großer Projekte ist besser als in den meisten Sprachen
- Workspaces lösen jedoch das Veröffentlichungsproblem nicht; selbst bei verschiedenen Konfigurationsmöglichkeiten ist keine „richtige“ Lösung leicht zu finden, die Veröffentlichungen einfach macht
- Schon die Existenz vieler Utility-crates für cargo workspace publish zeigt das Problem
- Beim Veröffentlichen von Wick dauerte es häufig mehr als eine Stunde, weil man manuelle Wiederholungsarbeit mit nur teilweise funktionierenden Tools kombinieren musste
async ist eine der größten Reibungsquellen
- Rusts async fühlt sich an wie ein Feature, das nach der ursprünglichen Entwicklung der Sprache hinzugefügt wurde, und steht auch in der Praxis oft im Weg wie etwas nachträglich Angeflanschtes
- Fehler sind schwer zu verstehen und zu beheben, und bei der Suche nach Lösungen muss man nach verschiedenen Runtimes und deren jeweiligem async-Ansatz filtern
- Manche async-Bibliotheken können außerhalb einer bestimmten async-Runtime möglicherweise nicht genutzt werden
- Aus der Perspektive von jemandem mit 20 Jahren JavaScript-Erfahrung und Go-Erfahrung ist Rust async die größte Quelle von Frustration und Reibung
- Es ist kein unüberwindbares Problem, aber man muss stets darauf vorbereitet sein, dass async-Probleme jederzeit auftreten können
- In anderen Sprachen funktioniert async so natürlich, dass es fast unsichtbar ist
Refactoring kann harte Arbeit sein
- Rusts reichhaltiges Typsystem ist zugleich Stärke und Schwäche
- In Rust-Typen zu denken ist gut; Rust-Typen zu verwalten kann ein Albtraum sein
- Daten und Funktionssignaturen können generic types, generic lifetimes und trait constraints enthalten
- Constraints selbst können wiederum generic types und lifetimes haben, sodass es Fälle gibt, in denen es mehr type constraints als eigentlichen Code gibt
- Beispiel: rxRust observable.rs
- Für jedes
implmüssen generics definiert werden, was schon beim ersten Schreiben umständlich ist; beim Refactoring kann eine kleine Änderung zu einer Kette von Anpassungen anwachsen- Beispiel: Wasmtime Cranelift iter.rs
- Wenn dieselben constraints oder generic-Listen an mehreren Stellen wiederholt werden müssen, gibt es auf Sprach- oder Tool-Ebene keine Möglichkeit, sie zu aliasieren oder auf eine zentrale Definition zu verweisen; die Duplizierungslast bleibt
Fazit: mächtig, aber teuer
- Rust ist vielseitig genug, um systemnahen Code, CLI-Apps, Webserver und Webclients in derselben Sprache zu schreiben
- Mit WebAssembly lässt sich dasselbe Binary nutzen, um ein LLM im Browser und auf der Kommandozeile auszuführen
- Rust-Programme können sehr robust sein, und wenn man erlebt hat, welche Probleme Rust verhindert, fällt die Rückkehr zu anderen Sprachen schwer
- Als der Autor kurz zu Go zurückkehrte, war die Entwicklungsgeschwindigkeit wieder attraktiv; nach einem Runtime-panic geriet dieser Vorteil jedoch ins Wanken
- Rust hat klare Nachteile
- Recruiting ist schwierig
- Lernen geht langsam
- Für schnelle Iteration ist Rust zu starr
- Besonders in async-Code ist es schwierig, Speicher- und Performanceprobleme nachzuverfolgen
- Nicht alle Bibliotheken sind für sicheren Code ausreichend gut
- Die Entwicklungstools haben noch deutliches Verbesserungspotenzial
- Mit einem kleinen Team wurde Erstaunliches erreicht, aber es gab auch große Hindernisse; da es außerdem technische Gründe gab, warum Rust besser zu Wick passte, ist es noch zu früh zu beurteilen, ob Rust für Wick den Aufwand wert war
- Wenn schnelle Iteration nötig ist, ist Rust wahrscheinlich nicht die richtige Wahl
- Wenn der Umfang bekannt ist oder höhere Anfangskosten tragbar sind, lohnt es sich, Rust ernsthaft in Betracht zu ziehen
- Da die WebAssembly-Perspektive von Monat zu Monat stärker wird, rückt die Möglichkeit näher, einmal geschriebene robuste Software an vielen Orten wiederzuverwenden
1 Kommentare
Meinungen auf Hacker News
Ich habe viel Rust geschrieben, aber selbst nach Jahren fühlt es sich immer noch wenig produktiv an.
In letzter Zeit nutze ich viel Zig; dort kann ich mich einfach auf den Code konzentrieren, den ich schreiben will, und muss mir keine Gedanken darüber machen, welche Tools oder Libraries ich verwenden sollte. Dadurch fühle ich mich etwa 10-mal produktiver.
Ich weiß, dass Rust Speichersicherheit bietet und dass das wichtig ist, aber die Benutzbarkeit ist wirklich schlecht. Jedes Mal, wenn ich Rust verwende, fühle ich mich eingeschränkt, und weil ich ständig Libraries suchen oder nachschlagen muss, wie man etwas macht, kann ich nicht einfach „Code tippen“.
Auch das Typsystem kann völlig ausufern, und oft ist schwer zu erkennen, welche Methoden man auf einer bestimmten Struct tatsächlich aufrufen kann. Rust ist ein hervorragendes Werkzeug und löst viele Probleme, aber ich halte es nicht für eine gute Allzwecksprache.
Obwohl ich seit fast 20 Jahren Python verwende, arbeite ich inzwischen auch in Rust genauso schnell wie in Python.
Rust hat für mich den richtigen Kompromiss völlig verfehlt. Für High-Level-Anwendungen ist es bei Low-Level-Details zu pingelig, und für Embedded- oder Betriebssystemcode ist es zu komplex.
Für Ersteres würde ich C++, Java, Haskell, OCaml oder sogar Go mit etwas C wählen; für Letzteres ist C, wie Macro Assembly verwendet, deutlich besser geeignet.
Ich habe immer noch das Gefühl, dass Graydon Hoares ursprüngliche Vorstellung – eher OCaml/SML mit linearen Typen, Garbage Collection, Stack Allocation, Green Threads und CPS – eine viel bessere Sprache ergeben hätte.
In C führt ein kleiner Fehler oft zu undefiniertem Verhalten und Problemen; in Rust gibt es das nicht, und das hat das Spiel völlig verändert.
Meinst du, dass du in Zig keine Libraries suchen oder herausfinden musst, wie etwas gemacht werden sollte?
Rust zwingt einen, im Voraus festzulegen, wohin jedes Bit und Byte geht, in welchem Thread es verwendet wird und auf welche Art es verändert wird. Wenn es nicht gerade um Parser oder Mikrocontroller geht, fühlt sich dieser Prozess mühsam an.
Ich mag es, etwas erst einmal zum Laufen zu bringen und danach die beste API-Struktur festzulegen, aber Rust steht diesem Prozess im Weg.
Auch wenn Rusts Typsystem mächtiger ist, bekommt man mit Swift 90 % der Performance und es fühlt sich viel natürlicher an.
Dass crates.io keine Namespaces hat, könnte der größte Kritikpunkt sein.
Jeder kann globale, generische Paketnamen blockieren, und sofern man das crates.io-Repository nicht meidet, muss man das meist hinnehmen. Einige dieser mit generischen Namen belegten Pakete sind aber nicht einmal die besten Pakete für den tatsächlichen Einsatz.
Vielleicht war es eine Gegenreaktion auf die ausführliche und lästige umgekehrte DNS-Notation aus Java, aber ein Modell wie bei GitHub, bei dem Benutzer- oder Gruppennamespaces dem Paketnamen vorangestellt werden, wäre wohl ein guter Mittelweg gewesen.
Ich habe diese Analyse an das crates.io-Team geschickt und auch darauf hingewiesen, dass es eine Richtlinie gegen Automatisierung gibt, aber als Antwort kam, dass dies kein ausreichender Beweis für Namensbesetzung sei.
Das Problem bei crates.io ist, dass selbst klare Richtlinien nicht durchgesetzt werden. Deshalb sind kurze, leicht zu merkende Crate-Namen schon alle vergeben, und es gibt keine Möglichkeit, sie zurückzubekommen.
Bei NPM, PyPI, RubyGems, Hex von Elixir, Cabal von Haskell usw. fällt mir aus der Zeit um 2014–2015, als Rust aufkam, kaum ein nicht-Java-Paketmanager ein, der keinen einzigen globalen Namespace hatte.
Einige haben später versucht, das zu korrigieren, aber damals funktionierten Paketmanager einfach so.
Die schlechteren Dependency-Management-Systeme, die später in anderen Sprachen entstanden, haben kaum aus diesem früheren Beispiel gelernt.
Es hat auch den Vorteil, dass auf Sprachebene keine globale Paketdatenbank nötig ist. Wenn man ein Paket auf example.com/your-thing stellt, ist es damit praktisch sofort veröffentlicht.
Natürlich kann man bei Bedarf separat Caches und Suchmaschinen bereitstellen.
Es ist dann so etwas wie: http-server ist schlecht, also sollte man MuffinTop verwenden – und das muss man einfach wissen.
Die Idee offizieller Paketnamen ist interessant, aber wenn sich der Code hinter einem Alias mit der Zeit ändert, kann das in der Praxis leicht verwirrend werden.
Am Ende bleibt es wohl in jedem Ökosystem Teil des Weges, Domain-Experte zu werden.
Wenn man im Workspace-Root eine
.cargo/config.tomlerstellt, gilt sie für alle Crates, sodass man globale Clippy-Lints setzen kann.In die Datei schreibt man unter
[build]etwarustflags = ["-Wclippy::lint_name_to_warn", "-Dclippy::lint_name_to_deny"].Allerdings werden
rustflagsnicht additiv ergänzt, sondern überschrieben; wenn es also andere Quellen wie die UmgebungsvariableRUSTFLAGSgibt, überschreiben sie diese Einstellung.lib.rs- odermain.rs-Dateien hinzufügt. Simpel.Ich lerne Rust, weil ziemlich klar zu sein scheint, dass es beruflich wichtig werden wird. Ich möchte es wirklich mögen und sehe auch die Vorteile, aber es gehört für mich bisher zu den unangenehmsten Sprachen, die ich je benutzt habe.
Ich hatte immer gehofft, dass meine Abneigung mit zunehmender Erfahrung verschwindet, aber je weiter ich die Lernkurve hinaufkomme, desto weniger werde ich damit warm.
Ist schon okay. Es wird nicht die einzige Sprache sein, die ich beherrsche und trotzdem nicht mag. Nur sagen so viele Leute, dass sie Rust lieben, dass ich dachte, ich würde auch Spaß daran haben.
Auch der Rust-Compiler scheint besser darin geworden zu sein, einen breiteren Bereich gültiger Fälle zu akzeptieren.
Der Schlüssel zum Verständnis des Borrow Checkers war, das zugrunde liegende Speichermodell zu verstehen. Rusts Speichermodell ist, abgesehen von Erweiterungen für Abstraktionen wie Generics, dasselbe wie in C.
Die Regeln des Borrow Checkers wirken zunächst willkürlich, sind aber eng mit diesem Speichermodell verbunden. Der eigentliche Wert entsteht, wenn man unbeabsichtigt am Borrow Checker hängen bleibt, denn dann handelt es sich um einen Bug, der aus Unaufmerksamkeit entstanden ist.
Das Beängstigende ist, dass Sprachen wie C oder C++ solchen Code einfach akzeptieren und weitermachen würden.
Rusts strenges Typsystem und der Borrow Checker schieben einen sanft dazu, Code richtig zu strukturieren, und ich bin überzeugt, dass das mein Code-Design in allen Sprachen verbessert hat, die ich verwende.
Das ist eine sehr grundlegende Usability-Funktion für Programmierer, die die meisten populären Sprachen gemeinsam haben, und selbst C++ hat seit Langem Default-Argumente.
Ich bin ein ehemaliger C/C++-Programmierer, der zehn Jahre lang fast jede Woche pthread aufgerufen hat, und inzwischen nutze ich asynchrones Rust überall.
Ich verstehe nicht, warum Async so viel Hass abbekommt. Meiner Meinung nach sollten alle für alles Async verwenden, sogar für „einfache“ Aufgaben, die oberflächlich betrachtet Single-Threaded sind.
Wenn der Anwendungsfall des Autors Wasm gewesen wäre, hätte sich sicher eine andere Perspektive ergeben.
Aufgaben, bei denen man große Buffer verwendet oder wiederverwendet, um Allokationskosten zu vermeiden, profitieren oft auch von klassischen Thread Pools. Mit Bump Allocators oder Shard Allocators lässt sich das bis zu einem gewissen Grad lösen, aber wenn man in engen, vektorisierbaren Schleifen CPU-gebunden ist, funktionieren Thread Pools besser.
Async ist ein gutes Werkzeug, aber es ist nicht in jedem Kontext optimal.
In Go, C# und TypeScript gibt es diese Hürde nicht.
Zum Beispiel scheint man ohne den Decorator aus Tokio nicht einmal innerhalb der Funktion
mainawaitverwenden zu können.EnginenichtSendsind, ich sie aber innerhalb einer asynchronen Closure verwenden will.GPT-4 hat vorgeschlagen, in einem Thread eine
tokio Runtimezu erstellen undblock_on()zu verwenden. Das werde ich morgen ausprobieren. Das ist mein erstes ernsthaftes Rust-Projekt.Mich interessiert auch, ob „ich nutze jetzt überall asynchrones Rust“ bedeutet: „ich nutze überall Tokio“.
Rust-Programmierung ist wirklich keine missbräuchliche Beziehung. Der Compiler versucht, einem so gut wie möglich zu helfen, und insbesondere die Fehlermeldungen von rustc sind Weltklasse.
Wenn überhaupt, ist nicht der Rust-Compiler, sondern eher das Betriebssystem näher an einer missbräuchlichen Beziehung. Für Hardware gilt dasselbe: Sie muss Assembly korrekt ausführen, also könnte man es aus dieser Sicht ebenfalls so nennen.
Rusts Fehlermeldungen sind einzigartig, und kein anderer Compiler kommt ihnen nahe.
Nebenbei: Ich habe kürzlich LaTeX verwendet, und die Fehlermeldungen waren grauenhaft. Sich durch die Fehler zu wühlen, um herauszufinden, was schiefgelaufen ist, war ein Albtraum.
Futurenicht mehrSendundSyncist.Die ganze Konsole ist mit Fehlern überzogen, und der eigentliche Syntaxfehler ist irgendwo dazwischen vergraben.
Meine Hauptbeschwerde ist, dass ich Lifetimes immer noch nicht vollständig verstehe und der Compiler auch nicht jedes Mal helfen kann. Ich verstehe den Grund: Der Compiler urteilt konservativ.
In C++-Tests sagt man häufig: „Wenn es kompiliert, ist es meistens richtig.“
Wenn man meint, Rust nehme einem so viele Fehler ab, dass übliche Testfälle bedeutungslos würden, dann ist das aus meiner Sicht ein Zeichen dafür, dass man in anderen Sprachen nicht das Richtige getestet hat.
Testen sollte man nicht die Probleme der Sprache selbst, sondern die Business-Logik.
Wenn man beim Blick auf den Testcode denkt: „In JavaScript würde ich das testen, in Rust muss ich das nicht“, dann kann man diesen Test einfach löschen.
Eine Trennung in „Business-Logik versus Sprachproblem“ gibt es nicht. Denn die Sprache ist das Fundament, auf dem die Business-Logik steht.
Wenn man Fehlermodi nicht testet, weiß ich nicht, welchen Sinn die Tests haben sollen.
Anders als die meisten vertrauten Sprachen hat C++ IFNDR, scherzhaft auch als False Positive auf die Frage „Ist das ein C++-Programm?“ bezeichnet.
Ein standardkonformer C++-Compiler darf in manchen Fällen nicht darauf hinweisen, dass der geschriebene Code vermutlich unsinnig ist, sondern muss einfach weitermachen und irgendetwas ausgeben.
Das kann eine funktionierende ausführbare Datei sein, oder eine, die jeden Freitag eine Katastrophe auslöst. Man kann es nicht wissen.
Der ISO-Standard identifiziert solche Fälle zwar, aber so vage, dass schwer genau zu fassen ist, was darunterfällt. Meine Vermutung ist, dass heutzutage die meiste nichttriviale C++-Software tatsächlich IFNDR ist. Besser, man lehnt die ganze Sprache einfach ab.
Rust scheint endlich mit der Vorstellung gebrochen zu haben, dass Programmierer alles, was der Compiler tut, vollständig kontrollieren und bewusst im Blick haben müssen.
Tatsächlich war das schon seit Jahrzehnten nicht mehr so; Compiler waren fast schon Magie. Rust hat diese Entwicklung durch Borrowing ein gutes Stück zurückgedreht und Menschen damit vertrauter gemacht, dass der Compiler es besser weiß als sie selbst.
Ich hoffe, dass man sich damit noch wohler fühlt. Wenn es algorithmisch nicht nötig ist, sollte man Collections nicht explizit von vorne nach hinten durchlaufen müssen. Viele Operationen sollten implizit parallelisiert werden. Ich hätte gern ein Bash im Stil von Rust.
Rust ist ziemlich transparent darin, was es tut, und sehr konservativ bei Compiler-Magie. Die Sprache allokiert nicht auf dem Heap, betreibt kein Reference Counting und hat keine impliziten numerischen Typumwandlungen.
Typen, die nicht ausdrücklich als implizit kopierbar deklariert sind, werden nicht kopiert, und selbst das ist nur bei Typen legal, die sich durch ein einfaches flaches
memcpykopieren lassen.Rust verwendet überall Zero-Cost-Abstraktionen, daher ist vorhersehbar, in welchen Code etwas kompiliert wird, und meistens ist es simpel. Auch das grundlegende Layout der Standardtypen ist gut bekannt, sodass man weiß, dass eine
Vec-Iteration zu einer Schleife kompiliert wird, die einen Pointer erhöht; implizite Parallelität gibt es nicht.Borrowing als „der Compiler weiß es besser als der Programmierer“ zu beschreiben, ist seltsam. Borrowing ähnelt Typprüfung. Wenn man einen Typ als temporär deklariert und ihn dann so verwendet, als würde er länger leben, gibt es einen Fehler.
Das ist so, als würde eine Funktion, die deklariert, eine
Foo-Struktur zurückzugeben, stattdessenBarzurückgeben: Dann gibt es einen Fehler. Der Compiler „weiß es besser“, weil man schlicht einen Bug geschrieben hat.Auch Borrowing wird ohne Garbage Collection zu direkter Pointer-Nutzung kompiliert, und bei C-ABI-Strukturen und -Funktionen ist garantiert, dass es buchstäblich C-Pointern entspricht. Wenn man glaubt, es besser zu wissen als der Compiler, kann man Lebenszeiten mit
unsafeauch umgehen.iter()-Aufrufs ändern.Ich stimme allerdings nicht zu, dass das implizit sein sollte.
In mancher Hinsicht gibt Rust Programmierern mehr Kontrolle als C. Zum Beispiel unterstützt Rust Inline-Assembly standardmäßig, während Inline-Assembly in C von anbieterspezifischen Erweiterungen abhängt.
Die bequemen Defaults sind allerdings sehr unterschiedlich. Unsichere Typecasts verlangen in Rust viel Prozedere und Sorgfalt und müssen mehr Regeln einhalten als in C.
Insbesondere verhalten sich Rust-Referenzen praktisch alle wie
restrict, und beimunsafe-Casting von Raw Pointern in sichere Referenzen kann man das sehr leicht kaputtmachen. Deshalb entsteht ein starker Anreiz, solchen Code nicht zu schreiben, wenn man eine Wahl hat.Ich will im Gegenteil mehr explizite Kontrolle und möchte diese Last durch ein ausdrucksstärkeres Typsystem verringern. Idealerweise würde Rusts Typsystem zu einer Art Prolog-Variante werden.
Zu lernen, wann man in Type Constraints investiert und wann nicht, ist eine wichtige Lektion.
Das ist kein reines Rust-Problem, kann sich dort aber etwas anders äußern.
Ich habe mit übermäßig typisiertem C++ und überabstrahiertem, typisiertem Java gearbeitet; beide haben dieselbe Art von Refactoring-Problem.
Umgekehrt habe ich auch viel Go gesehen, dem es an Typen und Dokumentation mangelte, sodass bestimmte Werte überall verstreut lagen und zu Laufzeit-Tretminen wurden; Refactoring kann dadurch wirklich massiv erschwert werden.
Das anfängliche Gefühl von Fortschritt stellt sich schneller ein, aber meist liefert man am Ende Bugs an die Nutzer aus.
Für diesen Kompromiss gibt es keine magische richtige Antwort. Rust bietet auf dieser Achse ziemlich viele Wahlmöglichkeiten.
Die Aussage „Rust schreit einen den ganzen Tag, jeden Tag, wegen Dingen an, die man im früheren Leben für völlig normal gehalten hätte“ trifft in ähnlicher Weise auch auf gute C-Compiler zu, wenn man alle Flags aktiviert
Ich mag Sprachen und Compiler, bei denen man das Geschrei optional abschalten und absichtlich schlechten Code schreiben kann. Funktionierender schlechter Code, den man schnell schreiben kann, ist oft besser als perfekter Code, der ewig dauert
Man kann erst einen schlechten, aber funktionierenden Proof of Concept bauen und ihn danach weniger schlecht machen
Dass „neue Nutzer gut angezogen werden, Bibliotheken oder Tools aber nicht dramatisch besser werden und nur einmalige Forks für bestimmte Use Cases entstehen“, hat nichts mit dem Alter zu tun
Kernentwickler anzuziehen ist schwierig, und es braucht viel Aufwand, das attraktiv zu machen. Außerdem werden kulturelle Gepflogenheiten von den Early Adopters festgelegt, und fehlende Gepflogenheiten können oft genauso schädlich sein wie schlechte
Bei Python sieht man, dass der lockere Umgang mit Entwicklungs- und Laufzeitumgebungen dazu geführt hat, dass es ungefähr 50 konkurrierende Arten gibt, Python-Programme zu entwickeln oder auszuführen
Das meistgenutzte Paket-Repository PyPI war jahrelang ein Durcheinander; nur wenige bauen auf bestehenden Paketen auf, die Namen wirken wie aus einem Zufallswortgenerator, das Ökosystem enthält viel Schadcode, und man kann Pakete nicht einmal über die Kommandozeile suchen
Das ist nicht die Schuld der Sprache, sondern die einer Community und eines Kernteams, die wie Zuschauer danebenstanden. Kultur ist wichtiger als die Technologie in ihrem Zentrum
Ich will nicht nur Python herausgreifen und kritisieren; ich kenne diese Probleme dort einfach besser. C gibt es seit einem halben Jahrhundert, aber auch diese Community hat nicht einmal die Hälfte der Lösungen ordentlich ausgearbeitet, die modernere Sprachen bereitgestellt haben
Der Compiler schreit einen an, man solle Dinge korrigieren — aber manchmal will man einfach erst eine Idee ausprobieren, bevor man alles perfekt ausarbeitet