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

Is it possible to use typeclasses to change `ReaderT (IO a) IO a` into `ReaderT (i a) IO a`?

我正在学习 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 String) 更改为 ReaderT (i String) 并使其更通用,以便我可以将其换出以进行单元测试。问题是,因为我们在 userInput 中使用 liftIO,它有点 ties iIO。有什么方法可以用其他东西替换 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)

请记住,ReaderT r m ar -> m anewtype 包装器。具体来说,MonadIO m => ReaderT (IO a) m b 等同于 MonadIO m => IO a -> m b。那么让我重新表述一下你的问题:

Can you convert MonadIO m => IO a -> m b to MonadIO m => m a -> m b?

答案是,因为IO a作为函数类型的输入出现。 (有时你会看到人们说 "in negative position",这与 "input" 的意思大致相同。)这里重要的是,转换函数输入与转换函数输出的方向相反。


让我们退后一步,考虑一个更一般的情况。如果你有一个函数 a -> b 并且你想转换它的输出以获得函数 a -> c,你需要能够将 bs 转换成 cs。如果你能给我一个将 bs 转换为 cs 的函数,我可以在它们从 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,则必须将 c 转换为 b。如果你能给我一个将 cs 转换为 bs 的函数,我可以在它们进入 b -> a 函数之前将其应用于值。

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)

(偶尔你会听到 covariancecontravariance 这两个词与转换类型的想法有关。他们指的是这个想法该转换器函数可以沿两个方向之一进行。函数在其输出参数方面是协变的,在其输入方面是逆变的。)


回到问题,

Can you convert MonadIO m => IO a -> m b to MonadIO m => m a -> m b?

希望你能看到这个问题实际上是在寻求一种将 m a 变成 IO a 的方法。 (您必须将 m a 转换为 IO a 才能将其提供给原始函数。)MonadIO 包含一个方法 liftIO :: IO a -> m a,它嵌入了一个 IO 计算到 "bigger" monad 中,它可能包含其他效果,但这与我们需要的完全相反。没有别的路可走。

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