Meglio usare la monade degli errori con la validazione nelle tue funzioni monadiche o implementare la tua monade con la validazione direttamente nel tuo bind?


9

Mi chiedo cosa sia meglio il design saggio per usabilità / manutenibilità e cosa c'è di meglio per quanto riguarda la comunità.

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 articoli o negozi, ecc., Ma potrei finire con un utente non valido, quindi quelle funzioni monadiche dovrebbero convalidare l'utente che ottengono e o creano.

Quindi, dovrei solo:

  • avvolgerlo in una monade di errore e fare eseguire le convalide alle funzioni monadiche
  • avvolgerlo in una monade di errore e fare in modo che il consumatore associ una funzione di convalida monadica nella sequenza che genera la risposta di errore appropriata (in modo che possano scegliere di non convalidare e trasportare un oggetto utente non valido)
  • crearlo in un'istanza di bind sull'utente creando in modo efficace 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 3 approcci, ma voglio sapere cosa viene fatto più comunemente per questo scenario dalla comunità.

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

Risposte:


5

Pugno mi chiedo: è un Usererrore di codice non valido o una situazione che può verificarsi normalmente (ad esempio qualcuno che inserisce un input errato nella tua applicazione). Se è un bug, proverei ad assicurarmi che non possa mai accadere (come usare costruttori intelligenti o creare tipi più sofisticati).

Se si tratta di uno scenario valido, è opportuno elaborare alcuni errori durante l'esecuzione. Quindi chiederei: cosa significa veramente per me che a nonUser è valido ?

  1. Significa che un invalido Userpuò far fallire un po 'di codice? Alcune parti del codice si basano sul fatto che a Userè sempre valido?
  2. O significa solo che è un'incoerenza che deve essere risolta in seguito, ma non rompe nulla durante il calcolo?

Se è 1., sceglierei sicuramente una specie di monade di errore (standard o tua), altrimenti perderai la garanzia che il tuo codice funzioni correttamente.

Creare la tua monade o usare una pila di trasformatori di monade è un altro problema, forse questo sarà utile: qualcuno ha mai incontrato un trasformatore di monade in natura? .


Aggiornamento: esaminando le opzioni espanse:

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

    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 biblioteche adottano un approccio simile, ad esempio Map, Seto Seqnascondono l'implementazione sottostante in modo che non sia possibile creare una struttura che non obbedisca ai loro invarianti.

  2. Se rinvii la validazione fino alla fine e la usi Right ...ovunque, non hai più bisogno di una monade. Puoi semplicemente eseguire 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 interrotto il calcolo abbastanza presto. E, se succede che qualche altro metodo aggiorna l'utente in modo che sia nuovamente valido, finirai per avere dati non validi da qualche parte e nemmeno saperlo.

  3. Ci sono molti problemi qui.

    • Il più importante è che una monade deve accettare qualsiasi parametro di tipo, non solo User. Quindi il tuo validatedovrebbe avere il tipo u -> ValidUser usenza alcuna restrizione u. Quindi non è possibile scrivere una tale monade che convalida gli input di return, perché returndeve essere completamente polimorfa.
    • Quindi, ciò che non capisco è che ti abbini case return u ofnella definizione di >>=. Il punto principale di ValidUserdovrebbe essere quello di distinguere valori validi e non validi, e quindi la monade deve assicurarsi che ciò sia sempre vero. Quindi potrebbe essere semplicemente

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

    E questo sembra già molto simile Either.

Generalmente, userei una monade personalizzata solo se

  • Non ci sono monadi esistenti 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 di monade in modo da poterle comporre in pile di monade.
  • O se hai bisogno di una monade troppo complessa per essere descritta come una pila di monadi.

I tuoi ultimi due punti sono preziosi e non ci ho pensato! Sicuramente la saggezza che stavo cercando, grazie per aver condiviso i tuoi pensieri, andrò sicuramente con il numero 1!
Jimmy Hoffa,

Ho appena legato l'intero modulo ieri sera ed eri morto nel modo giusto. Ho inserito il mio metodo di convalida in un piccolo numero di combinatori chiave che avevo fatto tutti gli aggiornamenti del modello e in realtà ha molto più senso in questo modo. Stavo davvero andando dopo il n. 3 e ora vedo come ... sarebbe stato un approccio inflessibile, quindi grazie mille per avermi chiarito!
Jimmy Hoffa,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.