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
).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With