Parsen, nicht validieren
Das Wesen des typgesteuerten Designs
- Ein einfacher Slogan zur Erklärung von type-driven design: Parsen, nicht validieren
- Dieser Slogan beschreibt, wie sich mit dem Typsystem Sicherheit und Korrektheit von Code erhöhen lassen
Der Bereich des Möglichen
- Ein statisches Typsystem macht es leicht zu beurteilen, ob eine bestimmte Funktion überhaupt implementierbar ist
- Beispiel:
foo :: Integer -> Void ist nicht implementierbar (der Typ Void kann keinen Wert enthalten)
- Beispiel: Die Funktion
head :: [a] -> a ist für den Fall einer leeren Liste nicht definiert
Partielle Funktionen in totale Funktionen verwandeln
Erwartungsmanagement
- Die Funktion
head kann bei einer leeren Liste keinen Wert zurückgeben, daher lässt sich mit dem Typ Maybe die Rückgabe von Nothing ermöglichen
- Das kann die Verwendung jedoch umständlicher machen
Erwartungen weitergeben
- Mit dem Typ
NonEmpty lässt sich eine nicht leere Liste ausdrücken, sodass garantiert ist, dass die Funktion head immer einen Wert zurückgibt
- Durch die Verwendung von
NonEmpty lassen sich unnötige Prüfungen entfernen, und Fehler können über das Typsystem schon zur Compile-Zeit erkannt werden
Die Kraft des Parsens
- Der Unterschied zwischen Parsen und Validieren liegt darin, wie Informationen erhalten bleiben
- Die Funktion
validateNonEmpty prüft zwar, dass eine Liste nicht leer ist, bewahrt diese Information aber nicht
- Die Funktion
parseNonEmpty prüft, dass eine Liste nicht leer ist, und bewahrt diese Information im Typ NonEmpty
Die Risiken der Validierung
- Ein validierungsbasierter Ansatz kann zum Problem des „shotgun parsing“ führen
- Das kann Situationen verursachen, in denen ein Programm einen Teil der Eingabe verarbeitet und erst danach feststellt, dass der Rest ungültig ist
- Parsen teilt das Programm in zwei Phasen: In der ersten wird die Gültigkeit der Eingabe geprüft, in der zweiten werden nur noch gültige Eingaben verarbeitet
Parsen in der Praxis
- Den Fokus auf Datentypen legen und Typsignaturen von Funktionen so konkret wie möglich machen
- Datenstrukturen verwenden, in denen ungültige Zustände nicht darstellbar sind, und Daten so früh wie möglich in konkrete Repräsentationen umwandeln
- Datentypen den Code anleiten lassen, statt dass der Code die Datentypen kontrolliert
- Funktionen, die
m () zurückgeben, sollten mit Bedacht verwendet werden
- Keine Scheu davor haben, Daten in mehreren Schritten zu parsen
- Denormalisierte Repräsentationen von Daten vermeiden und sie bei Bedarf durch Kapselung handhaben
- Abstrakte Datentypen verwenden, die Validatoren wie Parser aussehen lassen
Zusammenfassung, Reflexion, weiterführende Lektüre
- Das Typsystem von Haskell maximal auszunutzen ist nicht schwer und erfordert keine modernen Language Extensions
- Die Kernidee ist das „Schreiben totaler Funktionen“, was einfach klingt, in der Praxis aber schwer umzusetzen sein kann
- Als weiterführende Lektüre werden Matt Parsons Blogpost „Type Safety Back and Forth“ und Matt Noonans Paper „Ghosts of Departed Proofs“ empfohlen
Zusammenfassung von GN⁺
- Dieser Artikel erklärt, wie sich mit dem Typsystem von Haskell Sicherheit und Korrektheit von Code erhöhen lassen
- Er betont, wie wichtig es ist, den Unterschied zwischen Parsen und Validieren zu verstehen und die Gültigkeit von Eingaben durch Parsen sicherzustellen
- Wichtig ist außerdem, Datenstrukturen zu verwenden, in denen ungültige Zustände nicht darstellbar sind, und Daten so früh wie möglich in konkrete Repräsentationen umzuwandeln
- Als weiterführende Lektüre werden Matt Parsons Blogpost und Matt Noonans Paper empfohlen
1 Kommentare
Hacker-News-Kommentare
Dieser Ratschlag und der Artikel sind sehr hilfreich
Auch für Menschen nützlich, die keine statisch typisierte funktionale Sprache verwenden
Diese Idee überschreitet Paradigmen
Ähnliche Konzepte finden sich auch in der objektorientierten Literatur der 80er und 90er Jahre, zum Beispiel Design by Contract
TypeScript wird oft so geschrieben, dass Typen zur Laufzeit verfeinert werden
Design by Contract hat wahrscheinlich Clojures spec beeinflusst (Clojure ist eine dynamische Sprache)
Im Grunde geht es dabei um Annahmen und Garantien (Anforderungen und Bereitstellungen)
Wenn Annahmen geprüft und Garantien hergestellt wurden, müssen in anderen Teilen des Programms keine doppelten Annahmen erneut geprüft werden
Es kann verwirrend sein, wenn im Code Eigenschaften erneut geprüft werden, die bereits garantiert sind; das erschwert das Verständnis und die Verbesserung des Codes
Dieses Muster funktioniert auch in modernem C# gut und spart zudem Platz
Es ist gut, ein starkes Typsystem zu nutzen, um Fehlerfälle so auszudrücken, dass sie nicht darstellbar sind; das hilft, Softwarefehler zu reduzieren
Es dauert länger, über das Problem nachzudenken und dem Design zu folgen, aber in vielen Fällen ist diese Zeit gut investiert
Der Slogan „Parse, don’t validate“ fasst typbasiertes Design gut zusammen
Persönlich finde ich es am besten, „Validierung immer nur in einem einzigen Konstruktor durchzuführen“, denn so können ungültige Objekte überhaupt nicht existieren
Um ein Objekt zu verändern, sollte man es so implementieren, dass derselbe Konstruktor erneut aufgerufen wird, um den neuen Zustand zu konstruieren
Das erinnert an Abschnitt 5 von qmail, in dem es unter anderem „nicht parsen“ sowie „es gibt gute Interfaces und Benutzeroberflächen“ heißt
Wenn ich einen Programmierkurs mittleren Niveaus unterrichten würde, würde ich die Studierenden einen Essay schreiben lassen, in dem sie diese Vorschläge vergleichen und gegenüberstellen; aus jedem lässt sich etwas lernen, und zunächst mögen sie widersprüchlich erscheinen
Verwandtes Material: Richard Feldmans „Making Impossible States Impossible“
Frühere Diskussionen:
An Crowdstrike weitergeleitet
Das erinnert an einen Kommentar aus der XML-Hochphase Mitte der 2000er Jahre: Viele Organisationen entschieden sich für XML, weil XML einen Parser mitbringt
Obwohl das Schreiben eines Parsers weder schwierig ist noch keinen Spaß macht, kann ich nicht verstehen, warum Menschen keine Parser schreiben wollen
Ich frage mich, ob das der Ansicht widerspricht, dass das Schlüsselwort „required“ in Protocol Buffers ein großer Fehler war
Am besten wäre es wohl, sowohl flexibles, ungeprüftes Parsing als auch validiertes Parsing zu haben