Unit testing 是否可以使用TypeClass将`ReaderT(IO a)IO a`更改为`ReaderT(i a)IO a`?

Unit testing 是否可以使用TypeClass将`ReaderT(IO a)IO a`更改为`ReaderT(i a)IO a`?,unit-testing,haskell,monads,typeclass,monad-transformers,Unit Testing,Haskell,Monads,Typeclass,Monad Transformers,我正在学习Haskell,由于来自的帮助,我得到了以下代码,这只是一个echo程序。它工作得很好,但我想对它做一些改进,我遇到了麻烦 userInput :: MonadIO m => ReaderT (IO String) m String userInput = ask >>= liftIO -- this liftIO eliminates your need for join echo :: MonadIO m => ReaderT (IO String) m

我正在学习Haskell,由于来自的帮助,我得到了以下代码,这只是一个
echo
程序。它工作得很好,但我想对它做一些改进,我遇到了麻烦

userInput :: MonadIO m => ReaderT (IO String) m String
userInput = ask >>= liftIO -- this liftIO eliminates your need for join

echo :: MonadIO m => ReaderT (IO String) m ()
echo = userInput >>= liftIO . putStrLn -- this liftIO is just so you can use putStrLn in ReaderT

main :: IO ()
main = runReaderT echo getLine
我想做的是将
ReaderT(IO字符串)
更改为
ReaderT(I字符串)
,并使其更通用,这样我就可以将其替换为单元测试。问题是,因为我们在
userInput
中使用
liftIO
,它将
i
IO
联系在一起。有没有办法用其他东西来代替
liftIO
,使下面的代码正常工作

class Monad i => MonadHttp i where
  hole :: MonadIO m => i a -> ReaderT (i a) m a

instance MonadHttp IO where
  hole = liftIO

newtype MockServer m a = MockServer
  { server :: ReaderT (String) m a }
  deriving (Applicative, Functor, Monad, MonadTrans)

instance MonadIO m => MonadHttp (MockServer m) where
  -- MockServer m a -> ReaderT (MockServer m a) m1 a
  hole s = s -- What goes here?

userInput :: (MonadHttp i, MonadIO m) => ReaderT (i String) m String
userInput = ask >>= hole

echo :: (MonadHttp i, MonadIO m) => ReaderT (i String) m ()
echo = userInput >>= \input ->
         ((I.liftIO . putStrLn) input)

main = runReaderT echo (return "hello" :: MockServer IO String)
请记住,这是
r->ma
newtype
包装。具体来说,
monadiom=>ReaderT(ioa)mb
相当于
monadiom=>ioa->mb
。因此,让我重新表述你的问题:

你能把
monadiom=>ioa->mb
转换成
monadiom=>ma->mb

答案是,因为
IO a
显示为函数类型的输入。(有时你会看到人们说“处于负位置”,这与“输入”的意思大致相同。)这里重要的是转换函数输入的工作方向与转换函数输出的工作方向相反


让我们退一步,考虑一个更一般的情况。如果您有一个函数
a->b
,并且希望将其输出转换为函数
a->c
,则需要能够将
b
s转换为
c
s。如果你能给我一个将
b
s转换成
c
s的函数,我可以在值从
a->b
函数中出来后应用它

convertOutput :: (b -> c)  -- the converter function
              -> (a -> b)  -- the function to convert
              -> (a -> c)  -- the resulting converted function
convertOutput f g = \x -> f (g x)
convertInput :: (c -> b)  -- the converter function
             -> (b -> a)  -- the function to convert
             -> (c -> a)  -- the resulting converted function
convertInput f g = \x -> g (f x)
convertOutput
通常被称为
(.)

转换函数的输入的工作方式正好相反。如果要将函数
b->a
转换为函数
c->a
,则必须将
c
s转换为
b
s。如果你能给我一个将
c
s转换为
b
s的函数,我可以在值进入
b->a
函数之前将其应用于值

convertOutput :: (b -> c)  -- the converter function
              -> (a -> b)  -- the function to convert
              -> (a -> c)  -- the resulting converted function
convertOutput f g = \x -> f (g x)
convertInput :: (c -> b)  -- the converter function
             -> (b -> a)  -- the function to convert
             -> (c -> a)  -- the resulting converted function
convertInput f g = \x -> g (f x)
(偶尔你会听到“协方差”和“逆变”这两个词与转换类型的概念有关。它们指的是转换函数可以朝两个方向中的一个方向运行。函数的输出参数是协变的,输入参数是逆变的。)


回到问题上来

你能把
monadiom=>ioa->mb
转换成
monadiom=>ma->mb

希望您能看到,这个问题实际上是在寻求一种方法,将
ma
转换为
ioa
。(您必须将
ma
转换为
ioa
以将其提供给原始函数。)
MonadIO
包含一个方法,
liftIO::ioa->ma
,它将
IO
计算嵌入到一个“更大”的monad中,该monad可能包含其他效果,但这与我们需要的正好相反。没有别的路可走


也不应该有
ma
这是一个一元计算,可以执行各种未知效果。在不知道效果的情况下,不能将任意一元值转换为
IO
。而且许多(大多数)一元效应并没有直接转化为
IO
计算;例如,运行
状态
计算需要状态的起始值。

MonadIO
是一类monad,其中可以嵌入
IO
计算
liftIO
就是这样做的:将
IO
计算提升到您的
MonadIO
。如果您想用更通用的东西替换
IO
,您还需要用更通用的东西替换
MonadIO
。谢谢您的帮助!我想我感到困惑的是,我实际上不想改变
m
,在这种情况下,它是
IO
。我想更改
I
,它也是
IO
。但是
m
MonadIO
,而不是
i
。我想这里我缺少了一些东西。为什么模拟需要是一个
ReaderT字符串IO
,而不仅仅是
IO
?我只是在看这篇文章:哇,非常感谢你提供了详细易懂的答案!由于这种方法是不可能的,您是否会有一个建议,说明如何模拟用户输入进行单元测试?这取决于您正在尝试做什么!如果您只想提供一些固定的输入,可以将
return“myCannedInput”
作为读取器的
IO字符串
参数。如果您需要在每次执行操作时更改输入,则必须强制执行,方法是在操作的基础上构建一个状态机。