【问题标题】:Lazy list wrapped in IO包含在 IO 中的惰性列表
【发布时间】:2015-09-09 21:51:43
【问题描述】:

假设代码

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

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

当我在 ghci 中运行 g 时,它会导致 stackoverflow。但我在想也许它可以被懒惰地评估并产生[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 条目,则甚至不必构造列表的其余部分。而且我感觉这是不可能的:/。

【问题讨论】:

  • h' 是否呼叫h?你能告诉我们真正的代码吗?您是否希望它只执行足够的IO 来产生所需的结果,还是应该无论如何都执行它?
  • 只是为了产生所需的结果,实际代码要大得多,也更混乱,我仍然相信我在粘贴简化版本时会有所帮助。 h 不是从 h' 调用的
  • 我犯错了,我应该睡在这上面。
  • 啊,好吧,如果你想通过评估来推动执行,unsafeInterleaveIO 是你唯一的选择。

标签: haskell lazy-evaluation io-monad


【解决方案1】:

是的,IO 是罪魁祸首。 &gt;&gt;= for IO 在“世界状态”中是严格的。如果你写m &gt;&gt;= h,你会得到一个动作,它首先执行动作m,然后将h应用于结果,最后执行动作h产生。您的 f 操作没有“做任何事情”并不重要;无论如何都必须执行。因此,您最终会陷入无限循环,一遍又一遍地开始 f 操作。

谢天谢地,有办法解决这个问题,因为IOMonadFix 的一个实例。您可以从该操作中“神奇地”访问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 中甚至还有一些语法糖,让您可以将do 表示法与rec 关键字或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

【讨论】:

  • 我的想法可能过于简单,我会编辑原帖。
【解决方案2】:

听起来你想要一个混合列表和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 相比的权衡。

【讨论】:

  • 这似乎是一个非常简单的方法,但是那个库有点令人讨厌——为什么它不导出 ListT 构造函数?
  • 有没有可能有一天这个ListT 替换了transformers 中的那个?
  • 我是否可以将ListT IO (Either a b) 懒惰地转换为IO [Either a b],这样我就不必用ListT 来污染调用者了?我原以为 toList 会偷懒,但似乎不是:/ 我还希望选择是使用 take 3 还是仅使用身份,所以我预计流可能是无限的。
  • @JakubDaniel 你当然可以在main 的上述定义中用id 代替take 3,没问题。确实,toList 在惰性 IO 的意义上并不惰性——出于我在另一个答案中概述的所有原因,您应该将其视为一件好事。在调用toList 之前,您需要决定是要调用take 3 还是id。但是,toList 确实是将ListT IO (Either a b) 转换为IO [Either a b] 的正确方法。
  • 是的,但是如果我选择id,那么当函数没有终止时,严格性会导致我无法读取中间结果:(这超出了使用ListT而不是普通列表的目的在我对原始问题的编辑中。通常情况下,函数不会终止。
【解决方案3】:

我不确定这是否是一个合适的用法,但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]

【讨论】:

  • 这似乎可以解决问题,谢谢。使用这个有什么缺点吗,名字不是很让人放心;)。
  • 嗯,这就是为什么我说我不确定它是否合适。 unsafeInterleaveIO 有时还可以,但总的来说很危险;找一个比我更有经验的人来告诉你这是否是个好主意。
  • @JakubDaniel 缺点是它会破坏你对IO 发生方式的直觉:通常,在m &gt;&gt;= f 中,我们预计mIO 会在f 之前发生应用于论点;但在unsafeInterleaveIO m &gt;&gt;= f 中,mIO 将被延迟。将此转换为您的问题上下文,这意味着h 列表的消费者决定执行h' 的哪些(如果有!)迭代。它对编译器优化也很脆弱。因此,如果您正在做一些您真正关心 IO 发生的事情 - 这几乎总是 - 这是一个非常糟糕的主意!
猜你喜欢
  • 1970-01-01
  • 2013-02-01
  • 1970-01-01
  • 2013-11-28
  • 1970-01-01
  • 2013-08-30
  • 2018-04-17
  • 2023-04-03
  • 1970-01-01
相关资源
最近更新 更多