使用MTL分离DSL中的问题

时间:2017-01-19 11:27:20

标签: haskell testing dsl monad-transformers

我按照提出的想法使用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级别的测试。为此,所有 这些操作createProjectgetProjectsdeleteProject将是 通过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的任何一种实现方式。上面的问题可以吗? 克服?

1 个答案:

答案 0 :(得分:0)

monad堆栈的具体构造函数不必直接与mtl - 样式类型对应。 This article and Reddit discussion是相关的。 mtlMonadState s mStateT中具有通用的哑实现,但您也可以为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)