Meglio usare la monade degli errori con la convalida nelle funzioni monadiche o implementare la propria monade con la convalida direttamente nel proprio bind?

9

Mi chiedo quale sia il design migliore per l'usabilità / manutenibilità, e cosa c'è di meglio per quanto riguarda il fitting con la community.

Dato il modello di dati:

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)

Posso implementare funzioni monadiche per trasformare l'utente, ad esempio, aggiungendo oggetti o negozi, ecc., ma potrei finire con un utente non valido, quindi quelle funzioni monadiche dovrebbero convalidare l'utente che ottengono o creano.

Quindi, dovrei solo:

  • avvolgere in una monade di errore e fare in modo che le funzioni monadiche eseguano la convalida
  • racchiuderlo in una monade di errore e fare in modo che il consumatore leghi una funzione di convalida monadica nella sequenza che genera la risposta all'errore appropriata (in modo che possa scegliere di non convalidare e trasportare un oggetto utente non valido)
  • effettivamente lo costruisce in un'istanza di bind sull'utente che crea efficacemente il mio tipo di monade di errore che esegue automaticamente la convalida con ogni bind

Riesco a vedere i lati positivi e negativi di ciascuno dei tre approcci, ma voglio sapere cosa viene fatto più comunemente per questo scenario dalla comunità.

Quindi in termini di codice qualcosa come, opzione 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"]

opzione 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)

opzione 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"]
    
posta Jimmy Hoffa 28.01.2013 - 22:19
fonte

1 risposta

4

Pugno mi chiedo: sta avendo un% co_de non valido un bug di codice o una situazione che può normalmente verificarsi (ad esempio qualcuno che immette un input sbagliato nella tua applicazione). Se si tratta di un bug, proverei a garantire che non possa mai accadere (ad esempio utilizzando costruttori intelligenti o creando tipi più sofisticati ).

Se si tratta di uno scenario valido, alcuni errori di elaborazione durante il runtime sono appropriati. Poi chiederei: cosa significa per me che un User è non valido ?

  1. Significa che un User non valido può far fallire un codice? Alcune parti del tuo codice si basano sul fatto che una User è sempre valida?
  2. O significa solo che è una incoerenza che deve essere risolta più tardi, ma non rompere nulla durante il calcolo?

Se è 1., farei sicuramente una sorta di errore monad (standard o proprio), altrimenti perderai garanzie che il tuo codice funzioni correttamente.

Creare la tua monade o usare una pila di trasformatori monad è un altro problema, forse questo sarà utile: Qualcuno ha mai incontrato una Monade Transformer in the wild? .

Aggiornamento: analisi delle opzioni espanse:

  1. Sembra il modo migliore per andare. Forse, per essere davvero sicuro, preferirei nascondere il costruttore di User e esportare solo alcune funzioni che non consentono di creare un'istanza non valida. In questo modo sarai sicuro che ogni volta che succede viene gestito correttamente. Ad esempio, una funzione generica per la creazione di User potrebbe essere qualcosa di simile a

    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.
    

    Molte librerie accettano un appropaco simile, ad esempio User , Map o Set nascondono l'implementazione sottostante in modo che non sia possibile creare una struttura che non obbedisce ai loro invarianti.

  2. Se rimandi la convalida alla fine e usi Seq ovunque, non hai più bisogno di una monade. Puoi semplicemente fare calcoli puri e risolvere eventuali errori alla fine. IMHO questo approccio è molto rischioso, poiché un valore utente non valido può portare ad avere dati non validi altrove, perché non hai fermato il calcolo abbastanza presto. E, se accade che qualche altro metodo aggiorni l'utente in modo che sia di nuovo valido, finirai con l'avere dati non validi da qualche parte e nemmeno conoscerli.

  3. Qui ci sono diversi problemi.

    • La cosa più importante è che una monade deve accettare qualsiasi parametro di tipo, non solo Right ... . Quindi il tuo User dovrebbe avere il tipo validate senza alcuna restrizione su u -> ValidUser u . Quindi non è possibile scrivere una tale monade che convalida gli input di u , perché return deve essere completamente polymorhpic.
    • Successivamente, quello che non capisco è che tu combini su return nella definizione di case return u of . Il punto principale di >>= dovrebbe essere quello di distinguere valori validi e non validi, e quindi la monade deve garantire che questo sia sempre vero. Quindi potrebbe essere semplicemente

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

    E questo sembra già molto simile a ValidUser .

Generalmente, userei una monade personalizzata solo se

  • Non esistono monadi che ti forniscano le funzionalità di cui hai bisogno. Le monadi esistenti di solito hanno molte funzioni di supporto e, cosa più importante, hanno trasformatori monad in modo da poterle comporre in pile monad.
  • O se hai bisogno di una monade troppo complessa per descriverla come una pila monad.
risposta data 28.01.2013 - 23:16
fonte

Leggi altre domande sui tag