惰性列表包装在 IO 中

Lazy list wrapped in IO

假设代码

f :: IO [Int]
f = f >>= return . (0 :)

g :: IO [Int]
g = f >>= return . take 3

当我在ghci中运行 g时,导致Whosebug。但我在想也许它可以被懒惰地评估并产生 [0, 0, 0] 包装在 IO 中。我怀疑 IO 是罪魁祸首,但我真的不知道。显然以下作品:

f' :: [Int]
f' = 0 : f'

g' :: [Int]
g' = take 3 f'

编辑:其实我对这么简单的功能不感兴趣f,原代码看起来更像:

h :: a -> IO [Either b c]
h a = do
    (r, a') <- h' a
    case r of
        x@(Left  _) -> h a' >>= return . (x :)
        y@(Right _) -> return [y]

h' :: IO (Either b c, a)
-- something non trivial

main :: IO ()
main = mapM_ print . take 3 =<< h a

h 进行一些 IO 计算并在列表中存储无效 (Left) 响应,直到生成有效响应 (Right)。尝试是懒惰地构建列表,即使我们在 IO monad 中也是如此。这样阅读 h 结果的人甚至可以在列表完成之前就开始使用列表(因为它甚至可能是无限的)。如果阅读结果的人无论如何只关心前 3 个条目,则甚至不必构建列表的其余部分。我觉得这是不可能的:/.

是的,IO 是罪魁祸首。 >>= 对于 IO 在 "state of the world" 中是严格的。如果你写 m >>= h,你将得到一个首先执行操作 m 的操作,然后将 h 应用于结果,最后执行操作 h yields。您的 f 操作不 "do anything" 并不重要;无论如何都必须执行。因此,您最终陷入无限循环,一遍又一遍地开始 f 操作。

谢天谢地,有办法解决这个问题,因为 IOMonadFix 的一个实例。您可以 "magically" 从该操作中访问 IO 操作的结果。至关重要的是,该访问必须足够惰性,否则您将陷入无限循环。

import Control.Monad.Fix
import Data.Functor ((<$>))

f :: IO [Int]
f = mfix (\xs -> return (0 : xs))

-- This `g` is just like yours, but prettier IMO
g :: IO [Int]
g = take 3 <$> f

GHC 中甚至有一点语法糖,让您可以使用带有 rec 关键字的 do 符号或 mdo 符号。

{-# LANGUAGE RecursiveDo #-}

f' :: IO [Int]
f' = do
  rec res <- (0:) <$> (return res :: IO [Int])
  return res

f'' :: IO [Int]
f'' = mdo
  res <- f'
  return (0 : res)

有关使用 MonadFix 的更多有趣示例,请参阅 Haskell Wiki

我不确定这是否是一种合适的用法,但是 unsafeInterleaveIO 会通过将 f 的 IO 操作推迟到f 要求:

module Tmp where
import System.IO.Unsafe (unsafeInterleaveIO)

f :: IO [Int]
f = unsafeInterleaveIO f >>= return . (0 :)

g :: IO [Int]
g = f >>= return . take 3

*Tmp> g
[0,0,0]

听起来您想要一个混合了列表和 IO 功能的 monad。幸运的是,这正是 ListT 的用途。这是您在该表单中的示例,其中 h' 计算 Collat​​z 序列并询问用户他们对序列中每个元素的感受(我真的想不出任何符合您轮廓形状的令人信服的东西) .

import Control.Monad.IO.Class
import qualified ListT as L

h :: Int -> L.ListT IO (Either String ())
h a = do
  (r, a') <- liftIO (h' a)
  case r of
    x@(Left  _) -> L.cons x (h a')
    y@(Right _) -> return y

h' :: Int -> IO (Either String (), Int)
h' 1 = return (Right (), 1)
h' n = do
  putStrLn $ "Say something about " ++ show n
  s <- getLine
  return (Left s, if even n then n `div` 2 else 3*n + 1)

main = readLn >>= L.traverse_ print . L.take 3 . h

这是它在 ghci 中的样子:

> main
2
Say something about 2
small
Left "small"
Right ()
> main
3
Say something about 3
prime
Left "prime"
Say something about 10
not prime
Left "not prime"
Say something about 5
fiver
Left "fiver"

我想现代方法会使用管道或导管或迭代器或其他东西,但我对它们的了解还不够多,无法谈论与 ListT 相比的权衡。