3 Punkte von GN⁺ 2024-07-23 | 1 Kommentare | Auf WhatsApp teilen

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

 
GN⁺ 2024-07-23
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

    • Beispielcode:
      if(!Whatever.TryParse<Thingy>(input, out var output)) output = some-sane-default;
      
    • Beispielcode:
      if(!Whatever.TryParse<Thingy>(input, out var output)) throw new ApplicationException($"Not a valid Thingy: {input}");
      
    • In Kernel-Mode-Treibern wird empfohlen, Letzteres nicht zu verwenden
  • 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