我构建了一个基于ReaderT设计模式的项目。我选择使用简单的处理程序注入作为函数参数,而不是使用典型的依赖注入方法。这个部分工作得很好,因为它能够静态地构造依赖树并动态地定义环境。
环境可能包含配置以及日志记录效果:: String -> IO ()、时间:: IO UTCDate的影响等。
import Control.Monad.Reader (runReaderT, liftIO, reader, MonadReader, MonadIO)
data SomeEnv
= SomeEnv
{ a :: Int
, logger :: String -> IO ()
}
class HasLogger a where
getLogger :: a -> (String -> IO())
instance HasLogger SomeEnv where
getLogger = logger
myFun :: (MonadIO m, MonadReader e m, HasLogger e) => Int -> m Int
myFun x = do
logger <- reader getLogger
liftIO $ logger "I'm going to multiply a number by itself!"
return $ x * x
doIt :: IO Int
doIt = runReaderT (myFun 1337) (SomeEnv 13 putStrLn)能不能概括一下记录器的作用?
logger :: String -> m ()具有使用记录器的动机,该记录器适合于单一堆栈。
myFun x = do
logger <- reader getLogger
logger "I'm going to multiply a number by itself!"
return $ x * x发布于 2020-05-13 18:32:19
我们可以尝试以下更改:
HasLogger成为一个将环境与“基本”单体相关联的双参数类型类型。就像这样:
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE StandaloneKindSignatures #-}
import Control.Monad.IO.Class
import Control.Monad.Reader
import Data.Kind (Constraint, Type)
type RT m = ReaderT (SomeEnv m) m
type SomeEnv :: (Type -> Type) -> Type
data SomeEnv m = SomeEnv
{ a :: Int,
logger :: String -> RT m (),
-- I'm putting the main fuction in the record,
-- perhaps we'll want to inject it into other logic, later.
myFun :: Int -> RT m Int
}
type HasLogger :: Type -> (Type -> Type) -> Constraint
class HasLogger r m | r -> m where
getLogger :: r -> String -> m ()
instance HasLogger (SomeEnv m) (RT m) where
getLogger = logger
_myFun :: (MonadReader e m, HasLogger e m) => Int -> m Int
_myFun x = do
logger <- reader getLogger
logger "I'm going to multiply a number by itself!"
return $ x * x现在,_myFun没有MonadIO约束。
我们可以创建一个示例环境并运行myFun
env =
SomeEnv
{ a = 13,
logger = liftIO . putStrLn,
myFun = _myFun
}
doIt :: IO Int
doIt = runReaderT (myFun env 1337) env该解决方案的一个缺点是,即使使用RT类型的同义词,环境中的函数签名也会变得更加复杂。
编辑:为了简化环境中的签名,我尝试了以下替代定义:
type SomeEnv :: (Type -> Type) -> Type
data SomeEnv m = SomeEnv
{ a :: Int,
logger :: String -> m (), -- no more annoying ReaderT here.
myFun :: Int -> m Int
}
instance HasLogger (SomeEnv m) m where
getLogger = logger
-- Yeah, scary. This newtype seems necessary to avoid an "infinite type" error.
-- Only needs to be defined once. Could we avoid it completely?
type DepT :: ((Type -> Type) -> Type) -> (Type -> Type) -> Type -> Type
newtype DepT env m r = DepT { runDepT :: ReaderT (env (DepT env m)) m r }
deriving (Functor,Applicative,Monad,MonadIO,MonadReader (env (DepT env m)))
instance MonadTrans (DepT env) where
lift = DepT . lift
env' :: SomeEnv (DepT SomeEnv IO) -- only the signature changes here
env' =
SomeEnv
{ a = 13,
logger = liftIO . putStrLn,
myFun = _myFun
}
doIt :: IO Int
doIt = runReaderT (runDepT (myFun env' 1337)) env'DepT基本上是一个ReaderT,但是您知道它的环境是由DeptT本身参数化的。它有通常的例子。
_myFun不需要改变这个替代的定义。
发布于 2020-05-14 11:08:55
我想总结一下应用丹尼迪亚斯方法的一些结果。
由于我的项目目前处于不支持第二种方法的GHC版本,所以我遵循了第一种方法。应用程序由两个子应用程序组成。
type RT m = ReaderT (Env m) mtype HRT m = CFSM.HouseT (ReaderT (AutomationEnvironment m) m)第一种方法以一元堆栈和环境之间的关系为代价,避免了无限递归类型。由于子应用程序使用不同的一元堆栈,因此必须引入特定的环境。由于引入了DepT,第二种方法似乎可以避免这种情况。
例如,可以从函数中删除MonadIO约束。
mkPostStatusService
:: (MonadIO m, MonadThrow m, MonadReader e m, HasCurrentTime e, HasRandomUUID e)
=> C.InsertStatusRepository m
-> PostStatusService m变成了
mkPostStatusService
:: (MonadThrow m, MonadReader e m, HasCurrentTime e m, HasRandomUUID e m)
=> C.InsertStatusRepository m
-> PostStatusService m由于环境与应用程序堆栈有关,所以join是liftIO的替代品。
currentTime <- reader getCurrentTime >>= liftIO
-- becomes
currentTime <- join (reader getCurrentTime)对于单元测试,将构造模拟环境。由于MonadIO的删除,模拟环境可以在没有副作用的情况下构建.对具有MonadIO和MonadThrow的服务的检查以前是通过定义模拟环境(如
data DummyEnvironment = DummyEnvironment (IO T.UTCTime) (IO U.UUID)
instance HasCurrentTime DummyEnvironment where
getCurrentTime (DummyEnvironment t _) = t
instance HasRandomUUID DummyEnvironment where
getRandomUUID (DummyEnvironment _ u) = u有了新方法,副作用就可以消除了。
type RT = ReaderT DummyEnvironment (CatchT Identity)
data DummyEnvironment = DummyEnvironment (RT T.UTCTime) (RT U.UUID)
instance HasCurrentTime DummyEnvironment RT where
getCurrentTime (DummyEnvironment t _) = t
instance HasRandomUUID DummyEnvironment RT where
getRandomUUID (DummyEnvironment _ u) = u正如我所指出的,第一种方法将环境连接到特定的堆栈,因此堆栈定义了环境。下一步将是集成第二种方法,因为它似乎再次使用DepT将堆栈与环境分离。
https://stackoverflow.com/questions/61780295
复制相似问题