- 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
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“ garantiertWenn 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“
newtypebasierendes „parse, don’t validate“ ist in der Praxis sehr nützlichWenn 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..100einschränken oder mit[T] is [_, ..]nicht-leere Slices ausdrückenAllerdings 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ässtTypen wie
NonZeroU32sind einfach, aber die eigentliche Stärke liegt darin, die gesamte Domänenlogik als Typen zu entwerfen, sodass der Compiler die Rolle des Gatekeepers übernimmtDadurch verlagert sich die Debugging-Last von der Laufzeit in die Entwurfsphase
Als Beispiele sind "Domain Modeling Made Functional" und dieses Video empfehlenswert
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 istDie Typen
Vect n aundFin nin Idris sind dafür BeispieleBeispiel: anodized (Einführungsvideo)
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
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
Externe Eingaben müssen am Ende trotzdem geparst werden, daher ersetzt das den anderen Ansatz nicht vollständig
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>oderNumber<char>werfen dabei Ausnahmen, wenn der Wertebereich überschritten wirdDas Beispiel
try_rootsim Artikel ist eigentlich ein GegenbeispielDie Bedingung
b^2 - 4ac >= 0als Typ auszudrücken, wird in Rust sehr komplexIn solchen Fällen ist es vernünftiger, einfach
Optionzurückzugeben und innerhalb der Funktion zu validierenDie meisten Validierungen betreffen das Zusammenspiel mehrerer Werte und lassen sich daher nur unhandlich als „Parsing“ modellieren
fn(abc: ValidABC)zusammenführenDieses 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
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
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 lassenWenn C# künftig native DUs bekommt, wird das deutlich eleganter