在 IO 中使用 Reader `(-> r)` 的意外行为?

Unexpected behavior using Reader `(-> r)` inside IO?

我决定使用 Reader 实例 (->) r 来清理我的一些代码,以避免不断传递一些值。

经过一番尝试,我发现了以下行为差异:

let p x y = print (x,y)
    f = ($ (1 :: Int))

f $ do
    p <$> (+ 2) <*> (+ 5)
    p <$> (* 6) <*> (* 2)
-- prints (6,2) but not (3,6)

do
  f $ p <$> (+ 2) <*> (+ 5)
  f $ p <$> (* 6) <*> (* 2)
-- prints both (6,2) and (3,6)

手动脱糖并添加一些类型注释后:

let p x y = print (x,y)
    f = ($ (1 :: Int))
f $ ((>>) :: forall m a. m ~ (Int -> a) => m -> m -> m)
    (p <$> (+ 2) <*> (+ 5) :: Int -> IO ())
    (p <$> (* 6) <*> (* 2))

((>>) :: forall m. m ~ IO () => m -> m -> m)
  (f (p <$> (+ 2) <*> (+ 5) :: Int -> IO ()))
  (f (p <$> (* 6) <*> (* 2)))

现在我怀疑问题是,虽然 >> for IO 实例确实做了我想要的,但 >> for (-> r) 懒得看它的左侧。

所以我的问题是,有没有办法(可能是 >> 对 LHS 严格的变体?),在不 运行 解决这个懒惰问题的情况下做这样的事情?

不确定 ReaderT 在这种情况下是否有帮助,但我认为这最终可能会使代码变得更加复杂,所以我还没有尝试过。

严格控制 LHS 根本无关紧要:仅查看 IO 操作并不能执行它。它必须与另一个 IO 操作(通过 >> 等)结合,并最终作为 main 的一部分(或给 GHCI 的操作)结束。

也就是说,正如您似乎已经意识到的那样,在第一种情况下,您有两个 (Int -> IO ()) 值,您使用 Reader monad 中的 (>>) 将它们组合起来,结果是仅返回第二个 IO 操作。在第二种情况下,您有两个 IO () 操作,您使用 IO monad 中的 (>>) 组合它们。当然,这将两者合并为一个动作。

这只是 IO 操作组合方式的结果,即您必须显式组合它们:仅仅计算它们然后丢弃它们是不够的。您所要求的类似于要求您在 Reader monad 中计算的两个数字隐式相加:

f :: Int -> Int -> Int
f x = (* x)

foo :: Int -> Int
foo = do
  f 3
  f 5

bar :: Int
bar = foo 4

首先你计算 4*3,当然是 12,然后你扔掉它并计算 4*5,当然是 20,因此值 bar 是 20。12 被扔掉是因为您明确表示不要对它做任何事情:将它们加在一起并让 bar=32,或将它们相乘并让 bar=240,或任何类似的事情都是错误的。同样,隐式组合 reader 计算中的 IO () 值也是不正确的。