Verwenden Sie die Fehlermonade mit Validierung besser in Ihren monadischen Funktionen oder implementieren Sie Ihre eigene Monade mit Validierung direkt in Ihrer Bindung?

9

Ich frage mich, was das Design für Benutzerfreundlichkeit / Wartbarkeit besser macht und was besser zur Community passt.

Angesichts des Datenmodells:

type Name = String

data Amount = Out | Some | Enough | Plenty deriving (Show, Eq)
data Container = Container Name deriving (Show, Eq)
data Category = Category Name deriving (Show, Eq)
data Store = Store Name [Category] deriving (Show, Eq)
data Item = Item Name Container Category Amount Store deriving Show
instance Eq (Item) where
  (==) i1 i2 = (getItemName i1) == (getItemName i2)

data User = User Name [Container] [Category] [Store] [Item] deriving Show
instance Eq (User) where
  (==) u1 u2 = (getName u1) == (getName u2)

Ich kann monadische Funktionen implementieren, um den Benutzer zu transformieren, indem ich beispielsweise Elemente oder Speicher usw. hinzufüge, aber möglicherweise habe ich einen ungültigen Benutzer, sodass diese monadischen Funktionen den Benutzer validieren müssen, den sie erhalten und / oder erstellen.

Also, sollte ich nur:

  • Wickeln Sie es in eine Fehlermonade und lassen Sie die monadischen Funktionen die Validierung ausführen
  • Wickeln Sie es in eine Fehlermonade ein und lassen Sie den Verbraucher eine monadische Validierungsfunktion in der Reihenfolge binden, in der die entsprechende Fehlerantwort ausgelöst wird (damit er entscheiden kann, ein ungültiges Benutzerobjekt nicht zu validieren und herumzutragen).
  • Bauen Sie es tatsächlich in eine Bindungsinstanz auf dem Benutzer ein und erstellen Sie effektiv meine eigene Art von Fehlermonade, die die Validierung bei jeder Bindung automatisch ausführt

Ich kann positive und negative Ergebnisse für jeden der drei Ansätze sehen, möchte aber wissen, was die Community für dieses Szenario häufiger tut.

Also in Code-Begriffen so etwas wie Option 1:

addStore s (User n1 c1 c2 s1 i1) = validate $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ someUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"]

Option 2:

addStore s (User n1 c1 c2 s1 i1) = Right $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ Right someUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"] >>= validate
-- in this choice, the validation could be pushed off to last possible moment (like inside updateUsersTable before db gets updated)

Option 3:

data ValidUser u = ValidUser u | InvalidUser u
instance Monad ValidUser where
    (>>=) (ValidUser u) f = case return u of (ValidUser x) -> return f x; (InvalidUser y) -> return y
    (>>=) (InvalidUser u) f = InvalidUser u
    return u = validate u

addStore (Store s, User u, ValidUser vu) => s -> u -> vu
addStore s (User n1 c1 c2 s1 i1) = return $ User n1 c1 c2 (s:s1) i1
updateUsersTable $ someValidUser >>= addStore $ Store "yay" ["category that doesnt exist, invalid argh"]
Jimmy Hoffa
quelle

Antworten:

5

Faust Ich würde mich fragen: Hat ein ungültiger UserCode-Fehler oder eine Situation, die normalerweise auftreten kann (zum Beispiel jemand, der eine falsche Eingabe in Ihre Anwendung eingibt). Wenn es sich um einen Fehler handelt, würde ich versuchen sicherzustellen, dass er niemals auftreten kann (z. B. mithilfe intelligenter Konstruktoren oder der Erstellung komplexerer Typen).

Wenn es sich um ein gültiges Szenario handelt, ist eine Fehlerverarbeitung zur Laufzeit angemessen. Dann würde ich fragen: Was bedeutet es wirklich für mich bedeuten , dass eine Userist ungültig ?

  1. Bedeutet dies, dass ein ungültiger UserCode zum Fehlschlagen eines Codes führen kann? Verlassen sich Teile Ihres Codes auf die Tatsache, dass a Userimmer gültig ist?
  2. Oder bedeutet es nur, dass es sich um eine Inkonsistenz handelt, die später behoben werden muss, aber während der Berechnung nichts beschädigt?

Wenn es 1 ist, würde ich definitiv eine Art Fehlermonade wählen (entweder Standard oder Ihre eigene), sonst verlieren Sie die Garantie, dass Ihr Code ordnungsgemäß funktioniert.

Das Erstellen einer eigenen Monade oder die Verwendung eines Stapels von Monadentransformatoren ist ein weiteres Problem. Vielleicht ist dies hilfreich: Hat jemand jemals einen Monadentransformator in freier Wildbahn gesehen? .


Update: Sehen Sie sich Ihre erweiterten Optionen an:

  1. Sieht nach dem besten Weg aus. Um wirklich sicher zu gehen, möchte ich den Konstruktor lieber ausblenden Userund stattdessen nur einige wenige Funktionen exportieren, mit denen keine ungültige Instanz erstellt werden kann. Auf diese Weise können Sie sicher sein, dass es jedes Mal, wenn es passiert, richtig gehandhabt wird. Eine generische Funktion zum Erstellen eines Userkann beispielsweise so etwas wie sein

    user :: ... -> Either YourErrorType User
    -- more generic:
    user :: (MonadError YourErrorType m) ... -> m User
    -- Or if you actually don't need to differentiate errors:
    user :: ... -> Maybe User
    -- or more generic:
    user :: (MonadPlus m) ... -> m User
    -- etc.
    

    Viele Bibliotheken nehmen eine ähnliche appropach, zum Beispiel Map, Setoder Seqdie zugrunde liegende Implementierung verstecken , so dass es nicht möglich ist , eine Struktur zu schaffen , die nicht ihre Invarianten nicht gehorchen.

  2. Wenn Sie die Validierung auf das Ende verschieben und Right ...überall verwenden, benötigen Sie keine Monade mehr. Sie können einfach reine Berechnungen durchführen und mögliche Fehler am Ende beheben. Meiner Meinung nach ist dieser Ansatz sehr riskant, da ein ungültiger Benutzerwert dazu führen kann, dass an anderer Stelle ungültige Daten vorhanden sind, da Sie die Berechnung nicht früh genug gestoppt haben. Und wenn es passiert, dass eine andere Methode den Benutzer aktualisiert, damit er wieder gültig ist, werden Sie irgendwo ungültige Daten haben und nicht einmal davon wissen.

  3. Hier gibt es mehrere Probleme.

    • Das Wichtigste ist, dass eine Monade jeden Typparameter akzeptieren muss, nicht nur User. Sie validatemüssten also einen Typ u -> ValidUser uohne Einschränkung haben u. Es ist also nicht möglich, eine solche Monade zu schreiben, die Eingaben von validiert return, da returnsie vollständig polymorhpisch sein muss.
    • Als nächstes verstehe ich nicht, dass Sie case return u ofin der Definition von übereinstimmen >>=. Der Hauptpunkt von ValidUsersollte darin bestehen, gültige und ungültige Werte zu unterscheiden, und daher muss die Monade sicherstellen, dass dies immer wahr ist. So könnte es einfach sein

      (>>=) (ValidUser u) f = f u
      (>>=) (InvalidUser u) f = InvalidUser u
      

    Und das sieht schon sehr ähnlich aus Either.

Im Allgemeinen würde ich eine benutzerdefinierte Monade nur verwenden, wenn

  • Es gibt keine vorhandenen Monaden, die Ihnen die Funktionalität bieten, die Sie benötigen. Bestehende Monaden haben normalerweise viele Unterstützungsfunktionen, und was noch wichtiger ist, sie haben Monadentransformatoren, sodass Sie sie zu Monadenstapeln zusammensetzen können.
  • Oder wenn Sie eine Monade benötigen, die zu komplex ist, um als Monadenstapel bezeichnet zu werden.
Petr Pudlák
quelle
Ihre letzten beiden Punkte sind von unschätzbarem Wert und ich habe nicht darüber nachgedacht! Auf jeden Fall die Weisheit, nach der ich gesucht habe, danke, dass du deine Gedanken geteilt hast, ich werde definitiv mit # 1 gehen!
Jimmy Hoffa
Habe letzte Nacht das ganze Modul zusammengebunden und du hattest absolut recht. Ich habe meine Validierungsmethode in eine kleine Anzahl von Schlüsselkombinatoren gesteckt, bei denen ich alle Modellaktualisierungen durchgeführt habe, und das macht tatsächlich viel mehr Sinn. Ich wollte wirklich nach # 3 gehen und jetzt sehe ich, wie ... unflexibel dieser Ansatz gewesen wäre, also vielen Dank, dass Sie mich klargestellt haben!
Jimmy Hoffa