为什么这个应用语句没有被延迟评估,我怎么能理解为什么?

Why isn't this this applicative statement being lazily evaluated, and how can I understand why?

abc :: IO (Int)
abc = do
  print "abc"
  pure $ 10

xyz :: IO (Int)
xyz = undefined

main :: IO () 
main = do
  x <- (((+) <$> abc <*> abc) <* xyz)
  print x

为什么要对上面的xyz进行评估?我假设由于 Haskell 的惰性,它不需要评估 xyz(因此不会达到 undefined)?

我的假设是基于<*的类型:

Prelude> :t (<*)
(<*) :: Applicative f => f a -> f b -> f a

接下来是:

    -- | Sequence actions, discarding the value of the first argument.
    (*>) :: f a -> f b -> f b
    a1 *> a2 = (id <$ a1) <*> a2

并且:

(<$)        :: a -> f b -> f a
(<$)        =  fmap . const

因此 f b 永远不会被使用。


有没有办法让我理解/调查为什么要严格评估?查看 GHC 编译的核心对此有帮助吗?


感谢评论中的讨论(如果我错了,请有人纠正我)这是由于 IOMonad 实现,因为以下两个语句似乎评估不同的是:

身份:

runIdentity $ const <$> (pure 1 :: Identity Int) <*> undefined
1

输入输出:

const <$> (pure 1 :: IO Int) <*> undefined
*** Exception: Prelude.undefined

(<*) 不使用 f b 中的 b 值,但它确实使用 f 效果,因此它必须检查第二个参数。

Why does [putStrLn "Hello!" *> putStrLn "World!"] execute both while const (print "test") (print "test2") does not?

类型为const...

const :: a -> b -> a

... ab 都是完全参数化的,没有其他需要处理的。但是,对于 (<*),情况就大不相同了。对于初学者来说,(<*)Applicative 的方法,因此任何写 an Applicative instance for IO 的人都可以提供一个具体的...

(<*) :: IO a -> IO b -> IO a

...使用 IO 特定函数以任何认为必要的方式组合来自两个参数的效果的实现。

此外,即使 (<*) 不是 Applicative 的方法,它的类型...

(<*) :: Applicative f => f a -> f b -> f a

... 是这样的,虽然 ab 是完全参数化的,但 f 不是,因为 Applicative 约束。它的实现可以使用 Applicative 的其他方法,这些方法可以并且在大多数情况下会使用两个参数的效果。

请注意,这不是 IO 特有的问题。例如,这里 (<*) @Maybe 没有忽略其第二个参数的影响:

GHCi> Just 1 <* Just 2
Just 1
GHCi> Just 1 <* Nothing
Nothing