10 Punkte von GN⁺ 2025-08-28 | 3 Kommentare | Auf WhatsApp teilen
  • Rust erhöht mit starken Sicherheitsgarantien die Produktivität und Wartbarkeit, weil sich selbst in großen Codebasen Refactorings mit Zuversicht durchführen lassen
  • Der Compiler erkennt Bugs im Zusammenhang mit asynchronem Scheduling im Voraus und stärkt die Stabilität, indem er undefiniertes Verhalten verhindert
  • Sprachen wie TypeScript haben aufgrund ihres lockeren Typsystems häufig asynchrone Bugs, die erst in der Produktionsumgebung entdeckt werden
  • Rusts Typsystem zeigt die Auswirkungen von Codeänderungen klar auf und erhöht so in komplexen Projekten Zuverlässigkeit und die Bereitschaft zu experimentieren
  • Zig ist im Gegensatz zu Rust bei der Fehlerbehandlung lockerer und kann deshalb Bugs durch Tippfehler übersehen, was die Zuverlässigkeit senkt

Zusammenfassung und Hintergrund

  • Das Backend von Lubeno ist zu 100 % in Rust geschrieben, und die Codebasis hat einen Umfang erreicht, bei dem sie sich nicht mehr vollständig im Kopf überblicken lässt
    • In großen Projekten ist es generell schwer, Nebenwirkungen von Änderungen zu erkennen, was normalerweise zu Produktivitätsverlusten führt
  • Rusts Sicherheitsgarantien machen bei Codeänderungen die Auswirkungen klar sichtbar und nehmen so die Angst vor Refactorings
    • Das trägt langfristig zu besserer Wartbarkeit und höherer Produktivität bei
  • Der Text beginnt mit einem Fall, in dem der Rust-Compiler einen asynchronen Bug erkannt hat, und untersucht daran Rusts Produktivitätsvorteile

Beispiel für Rusts Sicherheitsgarantien

  • Problemsituation: Eine Struktur wird für gleichzeitigen Zugriff in einen Mutex gepackt, nach dem Erwerb des Locks wird asynchrone Arbeit ausgeführt
    let lock = mutex.lock();  
    db.insert_commit(commit).await;  
    
  • Entdeckung des Problems: rust-analyzer zeigte keinen Fehler an, aber in der Datei mit der Router-Definition trat ein Compilerfehler auf
    .route("/api/git/post-receive", post(git::post_receive))  
                                         ^^^^^^^^^^^^^^^^^  
    error: future cannot be sent between threads safely  
    
  • Ursachenanalyse:
    • Das Web-Framework erstellt für jede HTTP-Verbindung einen asynchronen Task, und der Task-Scheduler verschiebt Tasks zwischen Threads
    • Ein Mutex erfordert, dass derselbe Thread den Lock wieder freigibt; wenn der Thread an einer .await-Stelle wechselt, kann undefiniertes Verhalten entstehen
    • Der Rust-Compiler verfolgt die Lebensdauer des Locks und erkennt, dass es möglicherweise auf einem anderen Thread freigegeben wird
  • Lösung: Den Lock vor .await freigeben
  • Bedeutung: Rust verhindert asynchrone Bugs bereits zur Compile-Zeit, selbst wenn sie in der Entwicklungsumgebung schwer zu reproduzieren sind

Vergleichsbeispiel mit TypeScript

  • Problemsituation: In TypeScript-Code tritt ein Bug bei einer asynchronen Weiterleitung auf
    if (redirect) {  
        window.location.href = redirect;  
    }  
    let content = await response.json();  
    if (content.onboardingDone) {  
        window.location.href = "/dashboard";  
    } else {  
        window.location.href = "/onboarding";  
    }  
    
  • Ursache des Problems:
    • window.location.href führt die Weiterleitung nicht sofort aus, sondern plant sie nur ein; der Code läuft weiter
    • Durch eine Race Condition kommt es zu einer unbeabsichtigten Weiterleitung
  • Lösung: Im if-Block ein return ergänzen
    if (redirect) {  
        window.location.href = redirect;  
        return;  
    }  
    
  • Einschränkung: TypeScript hat keine Nachverfolgung von Lebensdauern oder Borrowing-Regeln und kann solche Bugs daher nicht zur Compile-Zeit erkennen
    • Sie werden erst in Produktion entdeckt, und das Debugging kostet viel Zeit

Rusts Vorteile beim Refactoring

  • In der Webentwicklung bieten Python, Ruby und JavaScript/Node.js anfangs hohe Produktivität, doch mit wachsender Codebasis werden Änderungen durch lockere Kopplung schwieriger
    • Nach Änderungen treten unerwartete Fehler auf, was die Bereitschaft verringert, Code anzupassen
  • Rusts Typsystem zeigt die Auswirkungen von Änderungen klar auf und nimmt so die Angst vor Refactorings
    • Beispiel: Warnungen wie „Diese Änderung könnte andere Teile beeinflussen“ verhindern Probleme im Voraus
  • Selbst wenn die Codebasis wächst, steigt die Produktivität weiter, weil bestehender Code wiederverwendet werden kann und Änderungen stabil bleiben

Vergleich mit Tests

  • Tests sind bei Refactorings nützlich, um Regressionen zu vermeiden, aber da der Compiler sie nicht erzwingt, können sie ausgelassen werden
    • Das Schreiben von Tests bedeutet eine hohe mentale Belastung, weil entschieden werden muss: Abstraktionsebene, Verhalten vs. Implementierungsdetails und welche Fehler verhindert werden sollen
  • Rust blockiert mit dem Compiler typische Fehler im Voraus und reduziert so die Entscheidungsbelastung, die sonst bei Tests anfällt
    • Eigenschaften, die sich nicht über das Typsystem prüfen lassen, werden durch Tests ergänzt

Vergleich mit Zig

  • Zig ist wie Rust eine Systemprogrammiersprache, ist bei der Fehlerbehandlung jedoch lockerer
    • Beispiel für Fehlerbehandlungscode
      const FileError = error{ AccessDenied };  
      fn doSomethingThatFails() FileError!void {  
          return FileError.AccessDenied;  
      }  
      pub fn main() !void {  
          doSomethingThatFails() catch |err| {  
              if (err == error.AccessDenid) {  
                  std.debug.print("Access was denied!\n", .{});  
              }  
          };  
      }  
      
    • Durch den Tippfehler AccessDenid entsteht ein Bug, aber der Zig-Compiler behandelt dies als Zahl, sodass der Code trotzdem kompiliert
  • Bei Verwendung von switch wird ein Tippfehler erkannt, in einer if-Anweisung jedoch ignoriert, was Zuverlässigkeitsprobleme verursacht
  • Rust vermeidet solche Designlücken und prüft Tippfehler und logische Fehler strikt

Implikationen

  • Rust verbessert mit Sicherheitsgarantien und einem strengen Typsystem die Produktivität und Stabilität großer Projekte
  • Selbst komplexe Probleme wie asynchrone Bugs werden zur Compile-Zeit erkannt, was die Wartungskosten senkt
  • Die Beispiele mit TypeScript und Zig zeigen die Risiken lockerer Prüfungen und unterstreichen den Wert von Rusts strengem Compiler
  • Rust etabliert sich auch in der Webentwicklung als starkes Werkzeug nicht nur für anfängliche Produktivität, sondern auch für das langfristige Management von Codebasen

3 Kommentare

 
taptaps 2025-08-30

Immer wenn ich sehe, wie sehr behauptet wird, das sei das Beste und eine mächtige Sprache!!
frage ich mich: Gibt es vielleicht doch nicht so viele Rust-Entwickler, wie man denkt, und deshalb versucht man, Leute zu Rust zu überreden??

 
colus001 2025-08-29

Bin ich der Einzige, der bei empfohlenen Rust-Artikeln an den Gourmet aus "Probier's! Probier's!" denken muss?

 
GN⁺ 2025-08-28
Hacker-News-Kommentare
  • Ich habe letztes Jahr einen in Rust geschriebenen virtio-host-Netzwerktreiber portiert. Dabei habe ich das Backend, den Interrupt-Mechanismus und die Umstellung von einer Bibliothek auf einen eigenständigen Prozess geändert. Es war ein komplexes Programm, das Memory Mapping, VM-Interrupts, Netzwerk-Sockets und Multithreading abdeckte. Ich hatte fast keine Rust-Erfahrung und auch nur wenig Erfahrung mit virtio, aber als das Projekt schließlich kompilierte, funktionierte es praktisch perfekt. Abgesehen von einem Bug im Zusammenhang mit Drop, der leicht zu beheben war. Ich glaube, die Rust-Bibliotheken haben mir sehr geholfen, weil sie so aufgebaut sind, dass man sie kaum falsch verwenden kann

    • Ich entwickle schon lange in Rust, und meistens gilt: Wenn es kompiliert, funktioniert der Code auch. Gelegentlich gibt es Deadlocks oder Bugs in Bezug auf Reihenfolgen, aber grundsätzlich bedeutet ein erfolgreicher Build, dass ein großer Teil des Projekts korrekt läuft
  • Ich halte Rust für großartig. Aber ich stimme nicht der Meinung zu, dass der Bug bei der href-Zuweisung TypeScript anzulasten ist. Der Kern des Problems ist, dass das Setzen von href nicht sofort die Seitennavigation auslöst, sondern erst später verarbeitet wird. Genau dasselbe Problem könnte es auch in Rust geben. Wenn Rust eine Funktion set_href hätte und dieses Verhalten später ausgeführt würde, wäre so etwas wie der folgende Code möglich:

    set_href('/foo')

    if (some_condition) { set_href('/bar') }

    Ich denke nur nicht, dass man das in Rust so entwerfen würde. Dass ein Setter direkt Verhalten auslöst, ist kein gutes Library-Design, und dass beim Zuweisen von href nicht sofort eine Navigation erfolgt, ist seltsam. In der Rust-Standardbibliothek gäbe es so eine dumme Implementierung wohl nicht. Es ist also kein Problem Rust vs. TypeScript, sondern eher ein Unterschied zwischen Rusts Standardbibliothek und der Web-Platform-API. Ich stimme aber zu, dass Rust so eine User Experience vermutlich nicht bieten würde

    • Formal betrachtet ist es nicht wünschenswert, Setter so zu gestalten, dass sie sofort Aktionen auslösen. Auch die Benennung sollte eher etwas wie navigate_to(href) sein. In einer Browser-Umgebung werden alle JS-Codes ohnehin als Callbacks ausgeführt und vom Event Loop gesteuert, daher ist es auch natürlich, dass es nicht sofort passiert

    • Das Rust-Beispiel ist interessant, aber allein anhand des TypeScript-Beispiels lässt sich nicht beurteilen, ob TS für große Projekte geeignet ist. In Ruby muss ich zwar oft Bugs zur Laufzeit finden, was ein ungutes Gefühl ist, aber am Ende funktioniert es vor dem Commit gut, und der Code lässt sich leicht lesen und ändern, was zufriedenstellend ist. Das Problem mit der Navigation ist ein JavaScript-Problem, das TS geerbt hat. Es entsteht, weil JS beliebige Änderungen an Eigenschaften zulässt. Gleichzeitig verschwindet die Seite ja nicht sofort, also ist dieses Verhalten nachvollziehbar, wenn man es einmal kennt

    • Technisch könnte Rust durch die Rückgabe von () oder ! bei set_href die Bedeutung klarer andeuten. Bei bedingten Redirects wäre es aber weiterhin schwierig, falsche Verwendung zuverlässig zu verhindern

    • Ich wollte darauf hinaus, dass man mit Rusts Ownership-Modell eine API so gestalten könnte, dass window.set_href('/foo') die Ownership von window übernimmt und dadurch ein zweiter Aufruf unmöglich wird. TypeScript hat schon konzeptionell keine Lifetimes, daher ist das dort nicht möglich. Da die JS-API bereits existiert, gibt es auf TypeScript-Seite auch keinen Weg, ein Ownership-System einzuführen. Ich wollte es als Beispiel dafür nennen, wie mehrere Rust-Features zusammen stärkere Garantien liefern

    • Die Begründung, warum Rust besser sein soll, klingt für mich letztlich nach: „Rust-Programmierer sind einfach besser.“ Ich glaube nicht, dass Rust-Programmierer so zirkulär argumentieren würden

  • Code nach einer Zuweisung läuft weiter, solange man nicht explizit früh zurückkehrt. Ganz ehrlich, ich verstehe nicht, warum man erwarten sollte, dass eine Wertzuweisung die Skriptausführung stoppt. Dem TS-Beispiel fehlt vielleicht Kontext, aber als Beispiel für eine „Data Race“ wirkt es seltsam

    • Einer Zuweisung an window.location.href folgt als Side Effect, dass der Browser zu diesem Link navigiert. Dieses Verhalten ist überraschend, und weil eine simple Zuweisung eine neue Seite lädt, fühlt es sich fast ein wenig wie execve an, weshalb es nicht völlig unplausibel ist zu denken, dass die JS-Ausführung sofort stoppt. Beim Programmieren sollte man sich nicht auf so eine Annahme verlassen, aber das Verhalten ist tatsächlich seltsam genug, um Verwirrung zu stiften

    • Unabhängig davon, ob man so denkt oder nicht: Wenn einen jemand auf so einen Bug hinweist, ist die Korrektur eindeutig. Der eigentliche Punkt des Autors war, dass Bugs dieser Art, die TS nicht einfängt, in der Praxis schwer zu finden sein können und viel Zeit kosten

    • exit(), execve() usw. stoppen die Ausführung tatsächlich sofort, daher ist es nachvollziehbar, Redirects ähnlich einzuordnen

    • Es ist seltsam, jemandem seine Erfahrung zum Vorwurf zu machen

    • Diese Zuweisung hat den großen Side Effect, dass man die Seite verlässt. Es ist also nicht völlig abwegig, sie als asynchrone Aktion zu sehen, die sofort wirksam wird. Ich habe diese Annahme auch schon einmal gemacht

  • Es ist letztlich die Geschichte eines Entwicklers, der erkannt hat, dass statische Typsysteme nützlich sind. Solche Texte finde ich immer amüsant

    • Was ich in meinem Blog zeigen wollte, war, dass Rusts Lifetime-Tracking und Trait-System auch weit komplexere Probleme erfassen können als bloße Typinkompatibilitäten. TypeScript ist ebenfalls eine statisch typisierte Sprache, bietet aber nicht dieselben Garantien wie Rust
  • Kommen die meisten Vorteile am Ende nicht einfach daher, dass man eine statisch typisierte, also kompilierte Sprache verwendet? Das gilt genauso für Java, Go und C++. TypeScript hat ein paar Tricks: Es kompiliert zu JS und erbt dessen Probleme, ist aber trotzdem brauchbar. Rusts Typsystem ist strenger und ermöglicht zusätzliche Compile-Time-Checks, dafür ist es schwerer zu lernen und schwerer zu lesen

    • Teilweise stimme ich zu, aber Rust hat im Typsystem noch mehr Dimensionen: Ownership, geteilten/exklusiven Zugriff, Thread-Sicherheit, Sum Types usw. Das Ownership/Borrowing-System macht klar, ob ein Argument nur eine temporäre Sicht ist oder vollständig übergeben wird. Das ist bei großen Programmen oder externen Bibliotheken ein großer Vorteil. Zum Beispiel ist beim Slice-Typ in Go zur Laufzeit nicht immer klar, welche Operationen erlaubt sind, und es gibt keinen sauberen Weg, ihn read-only zu borgen. Rust kann Thread-Sicherheit auf Typsystem-Ebene garantieren und so Data Races schon zur Compile-Zeit verhindern, die andere Sprachen oft nur schwer zur Laufzeit finden

    • Wenn man alle statisch typisierten Sprachen als eine Gruppe betrachtet, hat man die wahre Stärke von Union-(Sum-)Types und Pattern Matching wohl noch nicht erfahren. Wenn man sich einmal an Union Types gewöhnt hat, wirken andere traditionelle statisch typisierte Sprachen oft unbefriedigend

    • Ein großer Vorteil sind traits bzw. impl traits. In Rust kann man einem Typ auch nachträglich Traits hinzufügen, ähnlich wie bei Extension Methods in C#. In den meisten Sprachen ist ein Typ bei seiner Definition in einer Bibliothek weitgehend festgelegt, während man in Rust auf einfache Typen schrittweise weiter Funktionalität aufbauen kann. Diese late-bound-Eigenschaft bringt Dynamik in das Typsystem. Etwas zugespitzt gesagt ist Rusts eigentliche Superkraft nicht der Borrow Checker, sondern die Offenheit und Flexibilität seines Typsystems. Man muss nicht alles von Anfang an vollständig entwerfen, sondern kann schrittweise erweitern

    • Nicht alle statisch typisierten Sprachen haben denselben Effekt. Java verlässt sich letztlich auf Object und Runtime-Casts. Go hat keine Enums. C++ hat zwar variant, aber um es sicher zu verwenden, braucht man manuelle Konstrukte ähnlich wie try/except, was strukturell unhandlich ist

    • Es heißt immer, Rust sei schwer zu lernen, aber wenn man es einmal wirklich gelernt hat, ist es eigentlich nicht schwierig. Gerade am Anfang des Programmierens ist es wichtig, einfach etwas zusammenzuschreiben, bis es irgendwie läuft, und dafür ist Rust nicht besonders freundlich. Als Einstiegssprache würde ich es nicht empfehlen, aber schwer zu lesen ist es nicht

  • Rusts starke Sicherheitsgarantien erhöhen mein Vertrauen, wenn ich an einer Codebasis arbeite. Mit diesem Vertrauen scheut man auch Refactorings im Kern nicht mehr, und dadurch steigen Produktivität und Wartbarkeit deutlich. Aber genau dafür gibt es doch Tests. Ohne Tests hilft ein strenger Compiler sicher sehr, aber mit guten Tests kann man in jeder Sprache selbstbewusst refaktorieren

    • Es ist besser, wenn der Compiler das, was möglich ist, statisch beweist. Tests sollte man optimalerweise nur dort einsetzen, wo statische Garantien schwer zu erreichen sind. Das ideale Endgame wäre formale Verifikation, was in der Praxis sehr schwierig ist, also keine allgemeine Lösung, aber als Prinzip stimmt es

    • Gute Tests und ein gut genutztes Typsystem sind beide effektiv beim Finden von Bugs. Beim Schreiben von Tests muss ich allerdings immer an den xkcd-Comic „Standards“ denken. Man behebt Probleme, indem man noch mehr Standardisierung schafft — hier also, indem man noch mehr Code schreibt, um Bugs in Code zu finden. Immerhin wird das Typsystem von den Sprachdesignern gepflegt und muss nicht pro Projekt separat verwaltet werden

    • Bei jedem Refactoring auch noch die Tests refaktorieren zu müssen, verdoppelt die Arbeit

  • Ich finde, dass die Typsysteme von Rust und F# besonders beim Refactoring glänzen. Der Ausdruck „fearless refactoring“ trifft es genau

    • Ein Nachteil ist, dass Rust unvollständigen Code nicht toleriert, sodass man während eines Refactorings keinen „teilweise funktionierenden Code“ haben kann. Man muss es entweder vollständig fertig machen oder gar nicht erst anfangen, was experimentelles Schreiben etwas unbequem macht. Aber genau diese Strenge trägt am Ende auch zu besserem Code bei
  • Das Zig-Beispiel ist schockierend. Es wirkt so instabil, dass ich nicht nachvollziehen kann, wie man so ein Design gut finden kann

    • Ich denke, das ist vermutlich ein Bug. Aber in einer Creator-zentrierten Sprache wie Zig ist es wichtig, dass der Creator den Fehler auch als Bug anerkennt, damit er behoben werden kann. Wenn er es für Absicht hält, könnte das Design genau so weitergeführt werden

    • Jede Sprache hat ein paar instabile Designentscheidungen. In Go oder Zig muss man zum Beispiel mutex.unlock() immer explizit aufrufen; beim Verlassen des Scopes wird nicht automatisch entsperrt. Andererseits hatte ich mit Rusts as-Operator schon den gegenteiligen Fall, wo implizite numerische Umwandlungen zwischen Zahlentypen zu Bugs geführt haben, nach denen ich den ganzen Tag gesucht habe

    • Zuerst hatte ich diesen Fehler gar nicht bemerkt, erst durch diesen Kommentar ist es mir aufgefallen

    • Ich frage mich, ob ein Linter nicht Warnungen ausgeben könnte, etwa für Verweise auf Fehler, die im System nicht existieren, und Empfehlungen zur Verwendung von switch

    • Ich hatte angenommen, dass Error Sets auf Basis von Funktionssignaturen erzeugt werden. Schon etwas ungewöhnlich

  • Mir gefällt, dass ein starkes und solides statisches Typsystem so viele Funktionen bieten kann. Ich habe selbst erlebt, wie einfach große Refactorings in einer Haskell-Codebasis mit 1 Million SLOC waren. Auch ohne besonders fortgeschrittene Features war das allein durch das Typsystem möglich

  • Rust hat zwar korrekt erkannt, dass über eine await-Grenze hinweg ein Lock gehalten wird, aber ob es tatsächlich sicher ist, dieses Lock vor await freizugeben, hängt von weiterem Kontext ab. Meiner Meinung nach sollte das Lock gehalten werden, bis der Transaction Commit erstellt ist; wenn man es vorher freigibt, könnten Concurrency-Probleme entstehen. Ich kenne mich mit Rust async nicht gut aus, aber nach dem Commit müsste man das doch eher mit join oder select absichern, oder?

    • Wenn man ein Lock über await hinweg halten muss, kann man einen async-aware Mutex verwenden. Die Crates futures oder tokio implementieren so etwas. Solche Locks verwendet man typischerweise, wenn sie lange gehalten werden oder zwischen await-Punkten bestehen bleiben müssen. Sie sind teurer als normale Locks

    • Wenn ein Lock auch über await-Grenzen hinweg gehalten werden muss, kann man Tokios async-aware Mutex verwenden. Siehe die Dokumentation zu tokio/sync/struct.Mutex