处理一个 monad 调用另一个 monad 的干净方法是什么?
What is a clean way to handle one monad calling into another?
这是一个将 monad 粘合在一起的问题。不是堆栈形式,而是需要将一个 monad 解包到另一个 monad 中的 运行 操作的形式。
两个域:Weblog 和 App。但是,请记住,App 域将以与当前调用 Weblog 相同的方式调用其他域。两者都有自己的 monad 栈。两者都跟踪自己的状态。
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)
为了 运行 WeblogM
函数内部的 AppM
操作,我发现我必须打开 WeblogM
并重新包装它,使用像这样的功能:
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
但是,这确实让我的实际直通操作变得非常简单:
getPage :: Weblog.PageId -> AppM Weblog.WikiPage
getPage pageid = do
App{weblog} <- ask
runWeblogHere weblog $ Weblog.getWikiPage pageid
这已经困扰我了,因为我有其他我已经知道要插入到 AppM
架构中的单子库,而且我担心编写 runXHere
方法,这真的是样板文件,对于他们中的每一个。
我建议创建一个 MonadWeblog
class 来对应 WeblogM
,就像 MonadReader
对应 ReaderT
一样。这对我更有吸引力,因为我可以开始将 monad 胶水隔离到我的 MonadWeblog
(或者,实际上,MonadX
)实例中。
如果我们忽略新类型,并将两个错误转换器转换为 ExceptT
,两个 monad 堆栈共享相似的结构:
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
使用withReaderT
and mapReaderT
函数,我们可以定义:
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)
编辑: 为了简化新类型的包装和解包,我们可以使它们成为 lens
库中 Wrapped
的实例,并定义一个更通用的转换函数:
{-# 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
是一个类型类,表示:"I'm actually a newtype, here's a generic way to add/remove the newtype constructor".
如果 lens
依赖太重,newtype
包提供了类似的功能。
这是一个将 monad 粘合在一起的问题。不是堆栈形式,而是需要将一个 monad 解包到另一个 monad 中的 运行 操作的形式。
两个域:Weblog 和 App。但是,请记住,App 域将以与当前调用 Weblog 相同的方式调用其他域。两者都有自己的 monad 栈。两者都跟踪自己的状态。
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)
为了 运行 WeblogM
函数内部的 AppM
操作,我发现我必须打开 WeblogM
并重新包装它,使用像这样的功能:
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
但是,这确实让我的实际直通操作变得非常简单:
getPage :: Weblog.PageId -> AppM Weblog.WikiPage
getPage pageid = do
App{weblog} <- ask
runWeblogHere weblog $ Weblog.getWikiPage pageid
这已经困扰我了,因为我有其他我已经知道要插入到 AppM
架构中的单子库,而且我担心编写 runXHere
方法,这真的是样板文件,对于他们中的每一个。
我建议创建一个 MonadWeblog
class 来对应 WeblogM
,就像 MonadReader
对应 ReaderT
一样。这对我更有吸引力,因为我可以开始将 monad 胶水隔离到我的 MonadWeblog
(或者,实际上,MonadX
)实例中。
如果我们忽略新类型,并将两个错误转换器转换为 ExceptT
,两个 monad 堆栈共享相似的结构:
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
使用withReaderT
and mapReaderT
函数,我们可以定义:
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)
编辑: 为了简化新类型的包装和解包,我们可以使它们成为 lens
库中 Wrapped
的实例,并定义一个更通用的转换函数:
{-# 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
是一个类型类,表示:"I'm actually a newtype, here's a generic way to add/remove the newtype constructor".
如果 lens
依赖太重,newtype
包提供了类似的功能。