是否可以使用类型类将“ ReaderT(i a)IO a”更改为“ ReaderT(i a)IO a”?

时间:2018-12-29 19:13:58

标签: unit-testing haskell monads typeclass monad-transformers

我正在学习Haskell,并且由于this answer(这只是一个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 String)更改为ReaderT (i String)并使其更通用,以便将其换出进行单元测试。问题是,因为我们在liftIO内使用userInput,它与i一起 tie 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)

1 个答案:

答案 0 :(得分:2)

请记住,ReaderT r m anewtype的{​​{1}}包装器。具体来说,r -> m a等效于MonadIO m => ReaderT (IO a) m b。因此,让我重新表述您的问题:

  

您可以将MonadIO m => IO a -> m b转换为MonadIO m => IO a -> m b吗?

答案为,因为MonadIO m => m a -> m b作为函数类型的输入出现。 (有时您会看到人们说“处于负数位置”,这与“输入”大致相同。)这里重要的是,转换函数输入的方向与转换函数输出的方向相反。


让我们退后一步,考虑一个更一般的情况。如果您具有函数IO a,并且想要转换其输出以获得函数a -> b,则需要能够将a -> c s转换为b s。如果您可以给我一个将c转换为b s的函数,则可以将它们应用于c函数之后的值。

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) 更好地称为convertOutput

转换函数的输入的方法与此相反。如果要将函数(.)转换为函数b -> a,则必须将c -> a s转换为c s。如果您可以给我一个将b转换为c s的函数,则可以在将其应用于b函数之前将其应用于值。

b -> a

(有时您会听到与转换类型有关的 covariance contravariance 一词。它们指的是转换器函数可以合而为一的想法两个方向。函数的输出参数是协变的,而输入则是协变的。)


回到问题,

  

您可以将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) 转换为MonadIO m => IO a -> m b吗?

希望您能看到这个问题确实是在寻求一种将MonadIO m => m a -> m b变成m a的方法。 (您必须将IO a转换为m a才能将其提供给原始功能。)IO a包含单个方法MonadIO,该方法嵌入了{{ 1}}计算为可能包含其他效果的“更大”单子,但这与我们需要的相反。没有其他选择。

也不应有。 liftIO :: IO a -> m a是一个可以执行各种未知效果的单子计算。在不知道影响是什么的情况下,无法将任意一元值转换为IO。而且,许多(大多数)单子效果都无法直接转换为m a计算;例如,运行IO计算需要状态的起始值。