2015-04-27 9 views
7

Ecco un problema di incollare insieme le monadi. Non in una pila, ma nella forma di dover scartare una Monade per eseguire l'operazione all'interno di un'altra.Qual è un modo pulito per gestire una chiamata di monade in un'altra?

Due domini: Weblog e App. Tuttavia, tieni presente che il dominio App chiamerà in altri nello stesso modo in cui attualmente chiama Weblog. Entrambi hanno i propri stack monad. Entrambi tengono traccia del proprio stato.

newtype WeblogM a = WeblogM (ReaderT Weblog (ErrorT WeblogError IO) a) 
    deriving (Monad, MonadIO, Reader.MonadReader Weblog, Error.MonadError WeblogError) 

newtype AppM a = AppM (ReaderT App (EitherT AppError IO) a) 
    deriving (Functor, Applicative, Monad 
      , MonadReader App, MonadError AppError) 

Al fine di eseguire un'operazione di WeblogM all'interno di una funzione AppM, ho constatato che devo scartare il WeblogM e Rewrap, usando le funzioni in questo modo:

runWeblogHere :: forall a. Weblog.Weblog -> Weblog.WeblogM a -> AppM a 
runWeblogHere weblog action = 
    runIO (left . WeblogError) (Weblog.runWeblog weblog action) 

runIO :: (e -> EitherT AppError IO a) -> IO (Either e a) -> AppM a 
runIO handler = AppM . lift . handleT handler . EitherT 

Tuttavia, che non lasciare le mie operazioni passthrough effettivi piuttosto semplice:

getPage :: Weblog.PageId -> AppM Weblog.WikiPage 
getPage pageid = do 
    App{weblog} <- ask 
    runWeblogHere weblog $ Weblog.getWikiPage pageid 

Questo mi dà fastidio già perché ho altre librerie monadici che So già che collegherò all'architettura AppM e sono preoccupato per la scrittura di un metodo runXHere, che è veramente standard, per ognuno di essi.

Ho un suggerimento per creare una classe MonadWeblog corrispondere alla WeblogM, più o meno nello stesso modo in cui MonadReader corrisponde ReaderT. Mi piace di più perché posso iniziare ad isolare la colla monade nella mia istanza di MonadWeblog (o, in realtà, MonadX).

+0

Questo 'MonadWeblog' ti aiuta comunque? Sembra che tu voglia una versione generica di 'runXHere'; potresti creare una classe che includa come metodi tutti gli assunti necessari per implementare 'runXHere'. Tuttavia, non è così irragionevole dover implementare questo per ogni monade. Ogni monade di solito viene con un eliminatore in normali valori Haskell, che devi scrivere. Non è un crimine per i tuoi eliminatori andare in "AppM". – luqui

+1

Oh penso di vedere dove stavi andando con la classe. Tutte le tue monadi saranno 'ReaderT r (ErrorT e IO)'? Potresti considerare di astrarre ciò come per esempio 'GenApp rea', basando tutte le tue monadi su questo, poi scrivendo i combinatori come' embed :: (r '-> r) -> (e -> e') -> GenApp rea -> GenApp r 'e' a', che rende 'runXHere' banale. – luqui

+0

Sì a entrambe le domande.Ho dimenticato cosa sia esattamente 'WeblogM', ma' runWeblog' restituisce 'IO (O WeblogError a)'. Il contratto per ciascuno dei miei domini è che posso eseguirlo per ottenere 'IO (o errore a)'. E questo potrebbe essere il punto chiave per me per astrarre meglio le cose. –

risposta

3

Se si trascurano le newtypes, e convertire sia trasformatori di errore per ExceptT, i due monadi pile condividono una struttura simile:

import Control.Monad 
import Control.Monad.Trans.Except (ExceptT, catchE) 
import Control.Monad.Trans.Reader 

type M env err r = ReaderT env (ExceptT err IO) r 

Usando i withReaderT e mapReaderT funzioni, noi possiamo definire:

changeMonad :: (env' -> env) 
      -> (err -> ExceptT err' IO r) 
      -> M env err r 
      -> M env' err' r 
changeMonad envLens handler = withReaderT envLens . mapReaderT (flip catchE handler) 

Modifica: Per semplificare il wrapping e lo unwrapping dei newtypes, possiamo renderli istanze di Wrapped da lens biblioteca e definire una funzione di conversione più generale:

{-# LANGUAGE FlexibleInstances #-} 
{-# LANGUAGE TemplateHaskell #-} 

newtype N1 r = N1 { getN1 :: M (Int,Int) String r } 

$(makeWrapped ''N1) 

--instance Wrapped (N1 r) where 
-- type Unwrapped (N1 r) = M (Int,Int) String r 
-- _Wrapped' = iso getN1 N1 

newtype N2 r = N2 { getN2 :: M Int Char r } 

$(makeWrapped ''N2) 

changeMonad' :: (Wrapped (n1 r), 
       Unwrapped (n1 r) ~ M env' err' r, 
       Wrapped (n2 r), 
       Unwrapped (n2 r) ~ M env err r) 
      => (env' -> env) 
      -> (err -> ExceptT err' IO r) 
      -> n2 r 
      -> n1 r 
changeMonad' envLens handler = 
    view _Unwrapped' . changeMonad envLens handler . view _Wrapped' 

changeN2N1 :: N2 r -> N1 r 
changeN2N1 = changeMonad' fst (\c -> throwE [c]) 

Wrapped è un typeclass che dice: "Sono in realtà un newtype, ecco un modo generico per aggiungere/rimuovere il costruttore newtype".

Se la dipendenza lens è troppo pesante, il pacchetto newtype offre funzionalità simili.