我按照提出的想法使用monad-transformers编写一个小型DSL 这里here。为了 插图我在这里提出了一个小的子集。
class Monad m => ProjectServiceM m where
-- | Create a new project.
createProject :: Text -- ^ Name of the project
-> m Project
-- | Fetch all the projects.
getProjects :: m [Project]
-- | Delete project.
deleteProject :: Project -> m ()
这种DSL的想法是能够编写API级别的测试。为此,所有
这些操作createProject
,getProjects
,deleteProject
将是
通过REST调用Web服务实现。
我还写了一篇DSL来写出期望。下面给出了一个片段:
class (MonadError e m, Monad m) => ExpectationM e m | m -> e where
shouldContain :: (Show a, Eq a) => [a] -> a -> m ()
你可以想象可以将更多的DSL添加到混合中进行日志记录,以及 绩效指标see the gist linked above。
使用这些DSL可以编写一些简单的测试,如下所示:
createProjectCreates :: (ProjectServiceM m, ExpectationM e m) => m ()
createProjectCreates = do
p <- createProject "foobar"
ps <- getProjects
ps `shouldContain` p
下面显示了两个口译员:
newtype ProjectServiceREST m a =
ProjectServiceREST {runProjectServiceREST :: m a}
deriving (Functor, Applicative, Monad, MonadIO)
type Error = Text
instance (MonadIO m, MonadError Text m) => ProjectServiceM (ProjectServiceREST m) where
createProject projectName = return $ Project projectName
getProjects = return []
deleteProject p = ProjectServiceREST (throwError "Cannot delete")
newtype ExpectationHspec m a =
ExpectationHspec {runExpectationHspec :: m a}
deriving (Functor, Applicative, Monad, MonadIO)
instance (MonadError Text m, MonadIO m) => ExpectationM Text (ExpectationHspec m) where
shouldContain xs x = if any (==x) xs
then ExpectationHspec $ return ()
else ExpectationHspec $ throwError msg
where msg = T.pack (show xs) <> " does not contain " <> T.pack (show x)
现在运行方案createProjectCreates
monad变换器可以
以不同的方式堆叠。我觉得有道理的一种方法是:
runCreateProjectCreates :: IO (Either Text ())
runCreateProjectCreates = ( runExceptT
. runExpectationHspec
. runProjectServiceREST
) createProjectCreates
需要:
instance ProjectServiceM (ProjectServiceREST (ExpectationM (ExceptT Text IO)))
instance ExpectationM Text (ProjectServiceREST (ExpectationM (ExceptT Text IO)))
这个问题是ProjectSeviceM
的实例都必须这样做
了解ExpectationM
并为其创建实例,反之亦然。这些
可以使用StandaloneDeriving
扩展名轻松创建实例,例如:
deriving instance (ExpectationM Text m) => ExpectationM Text (ProjectServiceREST m)
然而,如果可以避免这种情况会很好,因为我漏了一些 信息到DSL的任何一种实现方式。上面的问题可以吗? 克服?
答案 0 :(得分:0)
monad堆栈的具体构造函数不必直接与mtl
- 样式类型对应。 This article and Reddit discussion是相关的。 mtl
类MonadState s m
在StateT
中具有通用的哑实现,但您也可以为MonadState
实例化ReaderT (IORef s) IO
,或者为CPS变体实例化newtype ProdT m a = ProdT { runProdT :: ... }
deriving (Functor, Applicative, Monad, MonadTrans, ...)
newtype TestT m a = TestT { runTestT :: ... }
deriving (Functor, Applicative, Monad, MonadTrans, ...)
。最终,你仍然需要如何处理效果,你需要处理它。
假设您编写了两个抽象monad变换器:
class (MonadError e m, Monad m) => ExpectationM e m | m -> e where
shouldContain :: (Show a, Eq a) => [a] -> a -> m ()
然后定义所需的实例。您可以直接编写所需的直接实例,而不需要编写所有传递实例。
顺便说一句,如果它们是其他类的简单组合,我建议不定义类型类。
的类/实例定义shouldContain :: (MonadError e m, Show a, Eq a) => [a] -> a -> m ()
与
一样有效MonadError
你已经 能够改变基础monad,只要它有newtype ExpectationT m e a = ExpectationT { runExpectation :: WriterT [e] m a }
instance Monad m => MonadError (ExpectationT m e) e where
throwError = ExpectationT . tell
-- etc..
。测试实现可能是
Int(64)