Haskell 测试读卡器monad是否在错误的环境中调用

Haskell 测试读卡器monad是否在错误的环境中调用,haskell,testing,hspec,reader-monad,Haskell,Testing,Hspec,Reader Monad,我有一个monawarder,它为我正在处理的应用程序生成数据。这里的主monad根据一些环境变量生成数据。monad根据环境选择其他几个monad中的一个来生成数据。我的代码看起来有点像下面的mainMonad是主monad: data EnvironmentData = EnvironmentA | EnvironmentB type Environment = (EnvironmentData, Integer) mainMonad :: ( MonadReader Enviro

我有一个
monawarder
,它为我正在处理的应用程序生成数据。这里的主monad根据一些环境变量生成数据。monad根据环境选择其他几个monad中的一个来生成数据。我的代码看起来有点像下面的
mainMonad
是主monad:

data EnvironmentData = EnvironmentA | EnvironmentB 

type Environment = (EnvironmentData, Integer)

mainMonad ::
  ( MonadReader Environment m
  , MonadRandom m
  )
    => m Type
mainMonad = do
  env <- ask
  case env of
    EnvironmentA -> monadA
    EnvironmentB -> monadB

monadA ::
  ( MonadReader Environment m
  , MonadRandom m
  )
    => m Type
monadA = do
  ...
  result <- helperA 
  result <- helper
  ...

monadB ::
  ( MonadReader Environment m
  , MonadRandom m
  )
    => m Type
monadB = do
  start <- local (set _1 EnvironmentA) monadA
  ...
  result  <- helper
  ...

helperA ::
  ( MonadReader Environment m
  , MonadRandom m
  )
    => m String
helperA = do
  ...

helper ::
  ( MonadReader Environment m
  , MonadRandom m
  )
    => m String
helper = do
  ...

崩溃将在单元测试期间被捕获,我将发现问题。但是,这并不理想,因为我更希望客户体验在错误的环境中调用东西所导致的轻微问题,而不是在测试处理程序没有捕获到问题的情况下发生硬崩溃。这似乎是一种核选择。这并不可怕,但以我的标准和三者中最差的标准来看,这并不令人满意

2.使用类型安全 我还尝试更改了
monadA
monadB
的类型,以便
monadA
不能直接从
monadB
调用,反之亦然。这非常好,因为它在编译时捕获问题。这是一个有点痛苦的问题来维持,它是相当复杂的。由于
monadA
monadB
可能各自共享多个
(monaderrer m)=>m类型的公共单子,因此这些单子中的每一个都必须被解除。实际上,这几乎保证了每条线路现在都有电梯。我并不反对基于类型的解决方案,但我不想花费大量时间来维护单元测试

3.将局部变量移动到声明的内部 每个对
EnvironmentData
有限制的monad都可以从一个类似于以下内容的样板开始:

monadA ::
  ( MonadReader Environment m
  , MonadRandom m
  )
    => m Type
monadA = do
  env <- view _1 <$> ask
  case env of
    EnvironmentA ->
      ...
    _ ->
      local (set _1 EnvironmentA) monadA
然后使用
runReaderT
(如下所示)将包装器添加到来自和到我的
monader环境的调用中。我不能用错误的
EnvironmentData
调用它们,因为没有环境数据。这与上一期的问题几乎一模一样



那么,有没有一种方法可以确保我的monad总是在正确的环境中被调用?

尽管这看起来有点奇怪,但我想有一种方法是引入冗余的
读卡器:

 data EnvironmentA -- = ...
 data EnvironmentB -- = ...

 convertAToB :: EnvironmentA -> EnvironmentB
 convertBToA :: EnvironmentB -> EnvironmentA
 -- convertAToB = ...
 -- convertBToA = ...

 monadA :: MonadReader EnvironmentA m => m Type
 monadA = do
     env <- ask
     -- ...
     res <- runReaderT monadB (convertAToB env)
     -- ...

 monadB :: MonadReader EnvironmentB m => m Type
 monadB = do
     env <- ask
     -- ...
     res <- runReaderT monadA (convertBToA env)
     -- ...
数据环境A-->。。。
数据环境B-->。。。
convertAToB::EnvironmentA->EnvironmentB
convertBToA::EnvironmentB->EnvironmentA
--convertAToB=。。。
--convertBToA=。。。
蒙纳达::蒙纳达环境A m=>m类型
莫纳达

env您的示例过于简化,我无法判断这是否适用,但您也可以通过将环境类型参数化来获得。也许是个游手好闲的人,比如:

data Environment t where
    EnvironmentA :: Environment A
    EnvironmentB :: Environment B

data A
data B
然后,关心它在哪个特定环境中运行的代码可以有一个
monawarder(环境a)m
monawarder(环境B)m
约束,而同时使用两者的代码可以使用
monawarder(环境t)m
约束


这种方法唯一的缺点是标准GADT的缺点,即有时需要小心处理分支,以确保编译器手头有适当的类型相等证明。这通常是可以做到的,但需要多加小心

以下是我将采取的方法。根据@Carl的回答,我将使用由类型“标记”参数化的GADT在类型级别区分“A”和“B”环境。为标记使用一对空类型(
dataa
datab
,就像@Carl做的那样)是可行的,不过我更喜欢使用
datatypes
,因为这样可以让意图更清楚

以下是准备工作:

{-# OPTIONS_GHC -Wall -Wincomplete-uni-patterns #-}
{-# LANGUAGE DataKinds, GADTs, KindSignatures #-}

import Control.Monad.Reader
import Control.Monad.Random
以下是环境类型的定义:

data EnvType = A | B
data Environment (e :: EnvType) where
  EnvironmentA :: Integer -> Environment 'A
  EnvironmentB :: Integer -> Environment 'B
在这里,不同的环境碰巧具有相同的内部结构(即,它们各自包含一个
整数
),但不要求它们这样做

我将做一个简化的假设,即monad的最外层始终是environment
ReaderT
,但我们将在基本monad中保持多态性(因此您可以使用
IO
Gen
来提供随机性)。您可以使用
monawarder
约束来完成所有这些,但由于一些模糊的技术原因,事情变得更加复杂(如果您真的需要,请添加注释,我将尝试发布一个补充答案)。也就是说,对于任意基monad
b
,我们将在monad中工作:

type E e b = ReaderT (Environment e) b
现在,我们可以如下定义
mainMonad
操作。请注意,没有
monawarder
约束,这是由
eeb类型
签名处理的。基本monad上的
MonadRandom b
约束确保
E b
将有一个
MonadRandom
实例。由于签名
E E E b Type
E::EnvType
中是多态的,因此
mainMonad
可以用于任何类型的环境。通过在环境GADT上进行大小写匹配,它可以将约束
e~'A
等纳入范围,从而允许将其分派到
monadA

data Type = Type [String]  -- some return type

mainMonad ::
  ( MonadRandom b )
    => E e b Type
mainMonad = do
  env <- ask
  case env of
    EnvironmentA _ -> monadA
    EnvironmentB _ -> monadB
monadA
操作可以调用特定于A的
helperA
以及公共
helper

monadA = do
  result1 <- helperA
  result2 <- helper
  return $ Type [result1,  result2]
也可以直接在环境上进行案例匹配。在公共帮助器中,需要处理所有环境类型,但在特定于
EnvType
的帮助器中,只需要处理
EnvType
(即,模式匹配将是详尽的,因此即使使用
-Wall
,也不会生成关于不匹配情况的警告):

当然,最重要的是,您不能意外地从B型操作调用A型操作:

badMonadB ::
  ( MonadRandom b )
    => E 'B b Type
badMonadB = do
  monadA  -- error: couldn't match A with B
也不能意外地从泛型帮助器调用A类型操作:

-- this is a common helper
badHelper :: (Monad b) => E e b String
badHelper = do
  -- so it can't assume EnvironmentA is available
  helperA  -- error: couldn't match "e" with B
尽管您可以使用案例匹配来检查适当的环境,然后分派:

goodHelper :: (Monad b) => E e b String
goodHelper = do
  env <- ask
  case env of
    EnvironmentA _ -> helperA  -- if we're "A", it's okay
    _              -> return "default"
这是他的一个版本:

{-# OPTIONS_GHC -Wall -Wincomplete-uni-patterns #-}
{-# LANGUAGE FlexibleContexts #-}

import Control.Monad.Reader
import Control.Monad.Random

data EnvType = A | B
data EnvironmentMain = EnvironmentMain EnvType Integer
data EnvironmentA = EnvironmentA Integer
data EnvironmentB = EnvironmentB Integer

class Environment e where getData :: e -> Integer
instance Environment EnvironmentA where getData (EnvironmentA n) = n
instance Environment EnvironmentB where getData (EnvironmentB n) = n

convertAToB :: EnvironmentA -> EnvironmentB
convertAToB (EnvironmentA x) = EnvironmentB x
convertBToA :: EnvironmentB -> EnvironmentA
convertBToA (EnvironmentB x) = EnvironmentA x

data Type = Type [String]  -- some return type

mainMonad :: (MonadReader EnvironmentMain m, MonadRandom m) => m Type
mainMonad = do
  env <- ask
  case env of
    EnvironmentMain A n -> runReaderT monadA (EnvironmentA n)
    EnvironmentMain B n -> runReaderT monadB (EnvironmentB n)

monadA :: (MonadReader EnvironmentA m, MonadRandom m) => m Type
monadA = do
  result1 <- helperA
  result2 <- helper
  return $ Type $ [result1] ++ [result2]

monadB :: (MonadReader EnvironmentB m, MonadRandom m) => m Type
monadB = do
  env <- ask
  Type start <- runReaderT monadA (convertBToA env)
  result <- helper
  return $ Type $ start ++ [result]

helperA :: (MonadReader EnvironmentA m) => m String
helperA = do
  EnvironmentA n <- ask
  return $ show n

helper :: (Environment e, MonadReader e m, MonadRandom m) => m String
helper = do
  n <- asks getData
  x <- getRandomR (0,n)
  return $ show x
{-
helper2 :: (Monad b) => E e b String
helper2 = do
  env <- ask
  case env of
    -- all cases must be handled or you get "non-exhaustive" warnings
    EnvironmentA n -> return $ show n ++ " with 'A'-appropriate processing"
    EnvironmentB n -> return $ show n ++ " with 'B'-appropriate processing"
helperA2 :: (Monad b) => E 'A b String
helperA2 = do
  env <- ask
  case env of
    -- only A-case need be handled, and trying to match B-case generates warning
    EnvironmentA n -> return $ show n
monadB = do
  Type start <- withReaderT envBtoA monadA
  result <- helper
  return $ Type $ start ++ [result]

envBtoA :: Environment 'B -> Environment 'A
envBtoA (EnvironmentB x) = EnvironmentA x
badMonadB ::
  ( MonadRandom b )
    => E 'B b Type
badMonadB = do
  monadA  -- error: couldn't match A with B
-- this is a common helper
badHelper :: (Monad b) => E e b String
badHelper = do
  -- so it can't assume EnvironmentA is available
  helperA  -- error: couldn't match "e" with B
goodHelper :: (Monad b) => E e b String
goodHelper = do
  env <- ask
  case env of
    EnvironmentA _ -> helperA  -- if we're "A", it's okay
    _              -> return "default"
{-# OPTIONS_GHC -Wall -Wincomplete-uni-patterns #-}
{-# LANGUAGE DataKinds, GADTs, KindSignatures #-}

import Control.Monad.Reader
import Control.Monad.Random

data EnvType = A | B
data Environment (e :: EnvType) where
  EnvironmentA :: Integer -> Environment 'A
  EnvironmentB :: Integer -> Environment 'B
getData :: Environment e -> Integer
getData (EnvironmentA x) = x
getData (EnvironmentB x) = x

type E e b = ReaderT (Environment e) b

data Type = Type [String]  -- some return type

mainMonad :: (MonadRandom b) => E e b Type
mainMonad = do
  env <- ask
  case env of
    EnvironmentA _ -> monadA
    EnvironmentB _ -> monadB

monadA :: (MonadRandom b) => E 'A b Type
monadA = do
  result1 <- helperA
  result2 <- helper
  return $ Type [result1,  result2]

monadB :: (MonadRandom b) => E 'B b Type
monadB = do
  Type start <- withReaderT envBtoA monadA
  result <- helper
  return $ Type $ start ++ [result]
envBtoA :: Environment 'B -> Environment 'A
envBtoA (EnvironmentB x) = EnvironmentA x

helperA :: (Monad b) => E 'A b String -- we don't need MonadRandom on this one
helperA = do
  n <- asks getData
  return $ show n

helper :: (MonadRandom b) => E e b String
helper = do
  n <- asks getData
  x <- getRandomR (0,n)
  return $ show x
{-# OPTIONS_GHC -Wall -Wincomplete-uni-patterns #-}
{-# LANGUAGE FlexibleContexts #-}

import Control.Monad.Reader
import Control.Monad.Random

data EnvType = A | B
data EnvironmentMain = EnvironmentMain EnvType Integer
data EnvironmentA = EnvironmentA Integer
data EnvironmentB = EnvironmentB Integer

class Environment e where getData :: e -> Integer
instance Environment EnvironmentA where getData (EnvironmentA n) = n
instance Environment EnvironmentB where getData (EnvironmentB n) = n

convertAToB :: EnvironmentA -> EnvironmentB
convertAToB (EnvironmentA x) = EnvironmentB x
convertBToA :: EnvironmentB -> EnvironmentA
convertBToA (EnvironmentB x) = EnvironmentA x

data Type = Type [String]  -- some return type

mainMonad :: (MonadReader EnvironmentMain m, MonadRandom m) => m Type
mainMonad = do
  env <- ask
  case env of
    EnvironmentMain A n -> runReaderT monadA (EnvironmentA n)
    EnvironmentMain B n -> runReaderT monadB (EnvironmentB n)

monadA :: (MonadReader EnvironmentA m, MonadRandom m) => m Type
monadA = do
  result1 <- helperA
  result2 <- helper
  return $ Type $ [result1] ++ [result2]

monadB :: (MonadReader EnvironmentB m, MonadRandom m) => m Type
monadB = do
  env <- ask
  Type start <- runReaderT monadA (convertBToA env)
  result <- helper
  return $ Type $ start ++ [result]

helperA :: (MonadReader EnvironmentA m) => m String
helperA = do
  EnvironmentA n <- ask
  return $ show n

helper :: (Environment e, MonadReader e m, MonadRandom m) => m String
helper = do
  n <- asks getData
  x <- getRandomR (0,n)
  return $ show x