2016-07-05 84 views
6

Quindi, in transformers vedo,Perché MonadIO è specifico per IO, piuttosto che un MonadTrans più generico?

class (Monad m) => MonadIO m where 
    -- | Lift a computation from the 'IO' monad. 
    liftIO :: IO a -> m a 

instance MonadIO IO where 
    liftIO = id 

e capisco che la ragione per cui questo differisce da MonadTrans è che se avete un po M1T (M2T (M3T (M4T IO))) x fatta di 4 trasformatori monade composti, allora non si vuole, ma si lift . lift . lift . lift $ putStrLn "abc" Piuttosto solo liftIO $ putStrLn "abc".

Ma questa specificità per IO sembra molto strana quando la definizione fondamentale di cui sopra sembra essere questa strana serie di ricorsioni con liftIO. Sembra che ci dovrebbe essere o una dichiarazione newtype per qualche combinatore come (ExceptT :~: MaybeT) IO x in modo che un singolo lift è tutto ciò che mai bisogno (Suppongo che questo è un trasformatore trasformatore di monade?), Oppure un po 'più param tipo di classe,

class (Monad m) => MonadEmbed e m 
    -- | Lift a computation from the `e` monad. 
    embed :: e a -> m a 

instance (Monad m) => MonadEmbed m m where 
    embed = id 

Perché lo transformers non utilizza uno di questi approcci in modo che le sequenze MonadTrans non debbano essere radicate in IO? È solo il fatto che i trasformatori gestiscono tutti gli "altri" effetti in modo che le uniche cose in fondo siano o Identity (già gestite con return :: a -> m a) o IO? O quanto sopra richiede qualcosa come UndecidableInstances che la libreria transformers non può contenere? O cosa?

+0

AFAIK 'transformers' è Haskell 98 che ** include ** include classi di tipo multiparametro, quindi l'esempio" non è consentito "in base a questa restrizione. La libreria 'mtl' fornisce alcuni tipi di caratteri come' MonadState' e alcune istanze che consentono di evitare di sollevare molte volte. – Bakuriu

risposta

6

Ma, questa specificità per IO sembra molto strano

Sfido il presupposto che questo è specifico per IO. Vedo anche molte altre classi in mtl. Ad esempio:

class Monad m => MonadError e m | m -> e where 
    throwError :: e -> m a 
    catchError :: m a -> (e -> m a) -> m a 

class Monad m => MonadState s m | m -> s where 
    get :: m s 
    put :: s -> m() 
    state :: (s -> (a, s)) -> m a 

... e molti altri. Generalmente, il "modo mtl" per creare azioni monadiche è usare queste operazioni polimorfiche di tipografia, in modo che non sia mai necessario lift - piuttosto, monomorfo l'operazione al tipo sollevato appropriato. Ad esempio, MonadError sostituisce completamente un ipotetico liftMaybe :: MonadMaybe m => Maybe a -> m a: anziché sollevare un valore Maybe a, si avrebbe il produttore della chiamata Maybe a valore throwError e return anziché Nothing e Just.

sembra che ci dovrebbe essere una dichiarazione newtype per qualche combinatore come (ExceptT :~: MaybeT) IO x modo che un unico ascensore è tutto ciò che mai bisogno

Con questa proposta, si avrebbe bisogno di (almeno) due diversi generi di ascensori: un ascensore per passare da m a a trans m a e un ascensore per passare da trans m a a (trans' :~: trans) m a. Avere un'unica operazione che gestisce entrambi i tipi di sollevamento è più uniforme.

Sembra che ci dovrebbe essere un certo tipo multi-param classe,

class Monad m => MonadEmbed e m 
    -- | Lift a computation from the `e` monad. 
    embed :: e a -> m a 

instance Monad m => MonadEmbed m m where 
    embed = id 

Questo approccio sembra ingannevolmente piacevole in un primo momento.Tuttavia, se provi a scrivere e utilizzare questa classe, scoprirai rapidamente perché tutte le classi mtl includono dipendenze funzionali: l'istanza MonadEmbed m m è sorprendentemente difficile da scegliere! Anche un esempio molto semplice come

embed (Right()) :: Either String() 

è un errore di ambiguità. (Dopotutto, sappiamo solo che Right 3 :: Either a() per alcuni a - non lo sappiamo ancora a ~ String e quindi non possiamo scegliere l'istanza MonadEmbed m m!). Sospetto che la maggior parte delle altre istanze incontreranno problemi simili. Se aggiungi le ovvie dipendenze funzionali, i tuoi problemi di inferenza di tipo spariscono ma i controlli fundep ti limitano notevolmente: uno può sollevare solo dalla monade di base e non da monadi intermedie arbitrarie come potresti sperare. Questo è un problema doloroso nella pratica (e il "dolore" modo mtl è così piccolo) che non è fatto in mtl.

Detto questo, è possibile utilizzare il pacchetto transformers-base.

E 'solo il fatto che i trasformatori di gestire tutti "gli altri" effetti in modo che le uniche cose in fondo sono o Identity (già trattati con return :: a -> m a) o IO?

Come dici tu, le basi più comuni sono IO (per la quale abbiamo già MonadIO) o Identity (per i quali si usa generalmente solo return e un calcolo puro, piuttosto che un calcolo monadica sollevato). A volte lo ST è una comoda base monade, ma è un po 'più raro usare i trasformatori oltre ST piuttosto che usarli su IO.

5

Le classi Monad... sembrano progettate principalmente per direttamente generalizzare le operazioni caratteristiche di una monade per funzionare anche se quella monade è sepolta sotto una pila di trasformatori. Queste operazioni normalmente non sono un set così grande, ad es. State ha semplicemente bisogno di , put, state e modify se lo si desidera, ma il gioco è fatto.

Non così con IO; sarebbe piuttosto poco pratico fare la pletora di metodi di operazioni IO fondamentali della classe MonadIO. Ovviamente è possibile ottenere tutti di, se si introduce solo liftIO come una "funzione di conversione", ma scommetterei che questo è sempre stato considerato un po 'un trucco.

Inoltre: IO è la monade di base non banale più importante; Non considererei inappropriato assegnargli una funzione di sollevamento dedicata solo per questo motivo.

Per quanto riguarda l'idea di "ascensore singolo è tutto ciò che serve": il problema con :~: consiste nel fatto che i trasformatori ora formano un albero binario anziché una pila semplice con una gerarchia chiara. Questo rende l'intera idea delle classi mtl molto più problematica.