3 Punkte von GN⁺ 2026-02-23 | 1 Kommentare | Auf WhatsApp teilen
  • Beschreibt einen Rust-Designansatz, der mithilfe des Typsystems Invarianten bereits zur Compile-Zeit statt durch Runtime-Validierung garantiert
  • Definiert neue Typen (newtypes) wie NonZeroF32 und NonEmptyVec, um ungültige Zustände (0, leerer Vektor usw.) unrepräsentierbar zu machen
  • Statt Fehler mit Option oder Result zurückzugeben, werden Einschränkungen bei Funktionsparametern verschärft, um Fehler im Voraus zu verhindern
  • Zeigt Beispiele wie String::from_utf8 oder serde_json::from_str, bei denen durch Parsen in bedeutungsvolle Typen umgewandelt wird
  • Das Designprinzip, illegale Zustände unrepräsentierbar zu machen und Validierung möglichst früh vorzuziehen, erhöht Stabilität und Lesbarkeit des Codes

1. Einschränkungen mit Typen statt Runtime-Validierung ausdrücken

  • In der Funktion divide(a, b) führt eine Division durch 0 zu einer Runtime-Panic
    • Man kann das Scheitern durch Rückgabe von Option ausdrücken, doch das schwächt den Rückgabetyp
  • Durch Definition des Typs NonZeroF32 lässt sich sicherstellen, dass nur von 0 verschiedene Werte erzeugt werden können
    • Der Konstruktor hat die Form fn new(n: f32) -> Option<NonZeroF32> und gibt bei Fehlschlag None zurück
    • Wird divide_floats(a: f32, b: NonZeroF32) so definiert, ist keine Runtime-Validierung mehr nötig
  • Die Verantwortung für die Validierung wird vom Funktionsinneren auf die Aufruferseite verlagert, sodass Fehler vorab beseitigt werden

2. Doppelte Validierung entfernen und Code vereinfachen

  • Wenn in der Funktion roots(a, b, c) die Prüfung a == 0 über Option behandelt wird, entsteht doppelte Validierung sowohl beim Aufrufer als auch in der Funktion
  • Mit NonZeroF32 wird die Validierung nur einmal durchgeführt, und die nachfolgende Logik wird einfacher
  • Nach demselben Prinzip lässt sich NonEmptyVec<T> definieren, um leere Vektoren auszuschließen
    • Wenn get_cfg_dirs() ein NonEmptyVec<PathBuf> zurückgibt, ist in main() keine zusätzliche Validierung mehr erforderlich

3. Praxisbeispiele: String und serde_json

  • String ist intern ein newtype um Vec<u8>, und String::from_utf8 führt die Gültigkeitsprüfung aus
    • Danach kann der String sicher als UTF-8-garantierter Text verwendet werden
  • serde_json mit from_str::<Sample> parst JSON in eine Struktur und garantiert die Existenz von Feldern sowie Typkonsistenz zur Compile-Zeit
    • Das Vorhandensein der Felder foo und bar, Typübereinstimmung, Array-Länge usw. werden alle auf Typebene geprüft

4. Zwei Prinzipien des typgesteuerten Designs

  • Illegale Zustände unrepräsentierbar machen
    • NonZeroF32 kann 0 nicht ausdrücken, NonEmptyVec keinen leeren Zustand
    • Eine einfache Prüffunktion wie is_nonzero kann weiterhin ungültige Zustände darstellen und ist daher unvollständig
  • Validierung so früh wie möglich durchführen
    • Wenn Validierung wie bei „Shotgun Parsing“ über den gesamten Code verstreut ist, kann das zu Sicherheitslücken führen (CVE-2016-0752 usw.)
    • Werden alle Einschränkungen schon im Parsing-Schritt geprüft, kann die nachfolgende Logik sicher ausgeführt werden

5. Typbasierte Beweise und Anwendungen in Rust

  • Nach der Curry-Howard-Korrespondenz können Typen als logische Aussagen und Werte als deren Beweise verstanden werden
    • Mit dem Crate typenum lassen sich mathematische Beziehungen (3 + 4 = 8) zur Compile-Zeit überprüfen
  • Mit dem Typsystem kann die Korrektheit eines Programms bereits beim Kompilieren bewiesen werden

6. Hinweise für den Praxiseinsatz

  • Selbst wenn eine externe API einfache Typen wie bool oder i32 verlangt, sollte intern mit bedeutungsvollen Enums oder newtypes gearbeitet werden
    • Beispiel: LightBulbState { On, Off } definieren und From<LightBulbState> for bool implementieren
  • Gibt es einfache Prüffunktionen wie verify() oder do_something_fallible(), sollte eine strukturierte Typumwandlung durch Parsen in Betracht gezogen werden
  • Bei Funktionen ohne Seiteneffekte lässt sich mit Result<Infallible, MyError> ein absichtlich unmöglicher Zustand im Typ ausdrücken

7. Fazit

  • Wenn Rusts Typsystem als Validierungswerkzeug genutzt wird, verbessern sich Klarheit und Stabilität des Codes
  • Werkzeuge des Rust-Ökosystems wie Vec, sqlx und bon nutzen bereits typbasiertes Design
  • Nicht jedes Problem lässt sich mit Typen lösen, aber der Ansatz, Validierungslogik auf die Typebene zu heben, verbessert Wartbarkeit und Sicherheit
  • Es wird empfohlen, Rusts starkes Typsystem maximal zu nutzen und Code zu schreiben, bei dem der Compiler Fehler auffängt

1 Kommentare

 
GN⁺ 2026-02-23
Hacker-News-Kommentare
  • Das in diesem Artikel verwendete Beispiel der Division durch null eignet sich nicht besonders gut, um das Prinzip „Parse, Don’t Validate“ zu erklären
    Der Kern dieses Prinzips liegt in Funktionen, die nicht vertrauenswürdige Daten in einen strukturell korrekten Typ umwandeln
    Auch im Artikel "Names are not type safety" von Alexis King wird darauf hingewiesen, dass das newtype-Muster keine vollständige „correct by construction“ garantiert
    Wenn das Typsystem Invarianten nicht direkt ausdrücken kann, ist die Verwendung abstrakter Typen mit Smart Constructors als Parser-Nachbildung ein realistischer Ansatz
    Das zweite Beispiel, ein non-empty vec, ist ein deutlich besserer Fall, weil es im Typsystem garantiert, dass „immer mindestens ein Element vorhanden ist“

    • Auch ein auf newtype basierendes „parse, don’t validate“ ist in der Praxis sehr nützlich
      Wenn unklar ist, woher ein String stammt, erhöht ein gekapselter Wert die Verlässlichkeit erheblich
      Für vollständige correctness-by-construction wäre ein abhängiges Typsystem nötig, aber es gibt leichtgewichtigere Alternativen wie die pattern types von Rust
      Man kann damit zum Beispiel Bereiche wie i8 is 0..100 einschränken oder mit [T] is [_, ..] nicht-leere Slices ausdrücken
      Allerdings zeigt eine non-empty list in der Form (T, Vec<T>) den Konflikt zwischen praktischer Nutzbarkeit und theoretischer Reinheit, weil sie sich nur eingeschränkt wie ein Vektor behandeln lässt
    • „correct by construction“ ist das eigentliche Ziel
      Typen wie NonZeroU32 sind einfach, aber die eigentliche Stärke liegt darin, die gesamte Domänenlogik als Typen zu entwerfen, sodass der Compiler die Rolle des Gatekeepers übernimmt
      Dadurch verlagert sich die Debugging-Last von der Laufzeit in die Entwurfsphase
    • Unter dem Stichwort „make invalid states impossible/unrepresentable“ findet man ebenfalls passendes Material
      Als Beispiele sind "Domain Modeling Made Functional" und dieses Video empfehlenswert
    • Das Beispiel mit der Division durch null ist ein Fall von falsch verstandener Trennung der Zuständigkeiten
      Statt auf dieser Ebene wrappen zu wollen, wäre es klarer, das Verhalten arithmetischer Funktionen wie bei Überläufen zu kapseln
  • Ich habe Links zu neueren Diskussionen zum Thema zusammengestellt
    Parse, Don't Validate (2019) (Februar 2026, 172 Kommentare)
    Parse, Don’t Validate – Some C Safety Tips (Juli 2025, 73 Kommentare)
    Parse, Don't Validate (2019) (Juli 2024, 102 Kommentare) usw.
    Nur zur Referenz geteilt

  • Der Ansatz Parsing statt Validierung hat Grenzen, wenn sich nicht alle realen Fälle kennen lassen
    Bei Dateiformaten ist es gut, so früh wie möglich zu scheitern, aber bei Business-Logik oder der Modellierung von Zustandsübergängen sollte man vorsichtig sein
    Wenn sich Anforderungen in der Realität ändern, kann das System sie womöglich nicht mehr aufnehmen, und am Ende weichen Nutzer darauf aus

  • In anderen Sprachen kann man mit abhängigen Typen (dependent typing) noch weiter gehen
    Zum Beispiel kann get_elem_at_index(array, index) die Gültigkeit des Indexbereichs schon zur Compile-Zeit garantieren, auch wenn die Array-Länge nicht im Voraus bekannt ist
    Die Typen Vect n a und Fin n in Idris sind dafür Beispiele

    • Auch in Rust gibt es Bibliotheken, die abhängige Typen makrobasiert nachbilden
      Beispiel: anodized (Einführungsvideo)
    • Wenn die Array-Länge aus stdin gelesen wird, ist sie zur Compile-Zeit nicht bekannt, daher ist diese Art der Verifikation auf Fälle mit statischer Information beschränkt
    • Es wäre wünschenswert, wenn solche Funktionen weiter verbreitet würden
  • Es gibt auch den Ansatz, mehrere Funktionen zu einem einzigen Typ zu haben
    Wie in Clojure werden dabei alle Daten als eine einzige Map dargestellt, und die gesamte Standardbibliothek kann sie manipulieren

    • Zwischen Perlis’ Aussage „100 Funktionen für eine Datenstruktur“ und „Parse, Don’t Validate“ besteht eine gewisse Spannung
      Wichtige Invarianten kann man im Typ unterbringen oder einfach durch Funktionen ausdrücken
      Auch in dynamisch typisierten Sprachen gibt es Designgewohnheiten, die einen ähnlichen Effekt erzielen
    • Das ist weniger eine reine Alternative als ein Trade-off
      Externe Eingaben müssen am Ende trotzdem geparst werden, daher ersetzt das den anderen Ansatz nicht vollständig
    • Es klingt ähnlich wie die Kritik an einer „stringly typed language“, ist in Wirklichkeit aber ein Prozess der schrittweisen Verfeinerung von Datenformen
    • Balance ist wichtig
      In strukturellen Typsystemen kann man mit branding nominale Typen nachbilden und umgekehrt ebenso, aber ergonomisch ist das nicht
      Realistisch ist letztlich eine passende Mischung aus beiden Ansätzen
  • Diese Diskussion erinnert an die concepts-Funktion von C++
    In Bjarne Stroustrups Concept-based Generic Programming wird ein Beispiel gezeigt, in dem Integer-Konvertierungen automatisch validiert werden
    Typen wie Number<unsigned int> oder Number<char> werfen dabei Ausnahmen, wenn der Wertebereich überschritten wird

  • Das Beispiel try_roots im Artikel ist eigentlich ein Gegenbeispiel
    Die Bedingung b^2 - 4ac >= 0 als Typ auszudrücken, wird in Rust sehr komplex
    In solchen Fällen ist es vernünftiger, einfach Option zurückzugeben und innerhalb der Funktion zu validieren
    Die meisten Validierungen betreffen das Zusammenspiel mehrerer Werte und lassen sich daher nur unhandlich als „Parsing“ modellieren

    • Wenn die Gültigkeit der Eingabe von Beziehungen zwischen mehreren Argumenten abhängt, muss man sie letztlich in etwas wie fn(abc: ValidABC) zusammenführen
  • Dieses Muster passt auch gut zur API-Entwicklung
    Statt JSON-Requests zu validieren, kann man sie von Anfang an in typgesicherte Structs parsen, sodass in der weiteren Logik keine doppelte Validierung mehr nötig ist
    Mit Rusts Kombination aus serde + custom deserializer lässt sich das leicht umsetzen
    Ich habe tatsächlich Fälle gesehen, in denen sich der Code für Fehlerbehandlung um 60 % reduziert hat

    • In Go wird das ebenfalls versucht, aber durch die übermäßige Verwendung von Pointern und das Fehlen algebraischer Typen wird es etwas umständlich
  • Dieselbe Philosophie lässt sich auch auf UI-Design-Systeme anwenden
    Statt CSS nachträglich zu prüfen, definiert man Typen, die nur Platzierungen in Rastereinheiten erlauben, sodass beliebige Margins wie 13px zu Compile-Fehlern werden
    So bleibt das Design deterministisch

    • Jemand fragte, welches Tooling dafür verwendet wird
  • Records + Pattern Matching in C# kommen diesem Ansatz recht nahe
    Discriminated Unions in F# sind noch mächtiger, weil sich mit Result<'T,'Error> ungültige Zustände als nicht darstellbar modellieren lassen
    Wenn C# künftig native DUs bekommt, wird das deutlich eleganter