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的最外层始终是environmentReaderT
,但我们将在基本monad中保持多态性(因此您可以使用IO
或Gen
来提供随机性)。您可以使用monawarder
约束来完成所有这些,但由于一些模糊的技术原因,事情变得更加复杂(如果您真的需要,请添加注释,我将尝试发布一个补充答案)。也就是说,对于任意基monadb
,我们将在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