Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is a clean way to handle one monad calling into another?

Here is an issue of gluing together monads. Not in a stack form, but in a form of needing to unwrap one monad to run the operation inside another.

Two domains: Weblog and App. But, keep in mind that the App domain will be calling into additional ones in the same way that it currently calls in to Weblog. Both have their own monad stacks. Both keep track of their own state.

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)

In order to run a WeblogM operation inside of an AppM function, I'm finding that I have to unwrap the WeblogM and rewrap it, using functions like this:

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

However, that does leave my actual passthrough operations quite simple:

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

This bothers me already because I have other monadic libraries that I already know that I'm going to plug in to the AppM architecture, and I'm worried about writing a runXHere method, which is really boilerplate, for each one of them.

I have a suggestion to create a MonadWeblog class to correspond to WeblogM, in much the same way that MonadReader corresponds to ReaderT. That appeals to me more because I can start isolating the monad glue into my instance of MonadWeblog (or, really, MonadX).

like image 730
Savanni D'Gerinel Avatar asked Apr 27 '15 16:04

Savanni D'Gerinel


1 Answers

If we ignore the newtypes, and convert both error transformers to ExceptT, the two monads stacks share a similar structure:

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

Using the withReaderT and mapReaderT functions, we we can define:

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)  

Edit: To ease the wrapping and unwrapping of the newtypes, we can make them instances of Wrapped from the lens library, and define a more general conversion function:

{-# 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 is a typeclass that says: "I'm actually a newtype, here's a generic way to add/remove the newtype constructor".

If the lens dependency is too heavy, the newtype package provides similar functionality.

like image 143
danidiaz Avatar answered Oct 23 '22 06:10

danidiaz