【问题标题】:How infinite loop with IO work in haskell带有IO的无限循环如何在haskell中工作
【发布时间】:2017-03-22 18:06:49
【问题描述】:

在 haskell 中,我们编写的所有 IO 代码都只是一个动作(许多人建议将其视为正在生成的脚本)。它是最终执行它们的主要方法(执行构造的脚本)。那么下面的程序是如何工作的呢? infi 函数永远不会返回。那么为什么字符串会被无限打印呢?

infi = 
    do
     print "hello"
     infi

main = 
    do
     infi

【问题讨论】:

  • 为什么您的程序需要暂停才能执行任何操作?在这方面,这并不是 haskell 独有的。我可以用任何语言编写无限 for 循环,并在每次迭代时打印出来。
  • @jkeuhlen 但是在其他语言中,您通过副作用进行打印 - 这是微不足道的。 OP 的问题是,如果构造该 IO 值的函数永远不会返回(而答案是懒惰),那么如何执行任何构造的 IO 值。这个问题并不真正适用于其他语言。
  • @sepp2k 很公平,我错过了与问题的区别。我将更新我的答案以更多地处理懒惰。
  • infi 不是函数。此外,Haskell 函数不会“返回”。

标签: haskell io monads


【解决方案1】:

您似乎对 Haskell 实际如何实现 IO 存在误解。关于这个主题还有很多其他的文献和这个网站上处理它的其他几个答案,所以我将专注于你的具体例子而不是一般的例子。

infi = 
  do
   print "hello"
   infi

main = 
  do
    infi

首先,您可以简化主要操作(不需要do):

main = infi 

在 haskell 中,“return”与命令式“return”不同。它只是意味着注入一个单子动作,即return :: Monad m => a -> m a。所以让我们来谈谈这里评估的是什么东西,而不是什么时候返回。

您的所有main 函数所做的只是调用infi 类型的值infi :: IO ()。由于infiIO 动作,它可以执行打印。像任何其他值一样,它也可以引用其他值(在这种情况下,它是递归的,因此它会调用自己)。如果没有基本情况,infi 将继续执行以下序列(就像它在您的 do 块中布置的一样!):

  1. 将“hello”打印到 STDOUT
  2. 评估值 infi
    1. 将“hello”打印到 STDOUT
    2. 评估值 infi
      ... 堆栈永远持续下去,因为递归中没有基本情况。

这可以工作的主要原因是因为 Haskell 的惰性求值。 Haskell 从不实际计算值,直到您需要它。这就是为什么您也可以使用无限列表执行纯操作:

let x = [1..] -- x is an infinite list. If you told haskell to print every element, it would run forever since it would evaluate the whole thing. 
let y = x !! 3 -- y = 2. This is not infinite because you are only evaluating the first three elements, instead of the whole value.

你的无限价值infi也是如此。 Haskell 可以创建一个包含无限动作的“运行时脚本”,因为 infi 的 value 具有有限表示(它的名称),但它可以无限计算,因为它没有基本情况。

【讨论】:

  • 嗯。实际上很多博客都提到,当我们写 print "hello" 时,它会返回一个打印 hello 字符串的动作。这样的打印不会发生。它是主要的,然后评估打印它的动作
  • @FaizHalde 我添加了一些额外的内容来解释懒惰。这是否更有帮助?
  • @FaizHalde 这是正确的(大部分)。而do putStrLn 'hello' infi 不会“呼叫”infi。它返回一个打印 hello 字符串的操作,然后是“调用”infi
【解决方案2】:

infi 本质上是相同IO 动作的无限流,与一元排序 (>>) 而不是 cons (:) 连接:

fives = 5                :  fives
infi  = putStrLn "hello" >> infi

事实上,我们可以使用动作列表infi'抽象出一元绑定:

infi' :: [IO ()]
infi' = putStrLn "hello" : infi'

然后用sequence_恢复infi,可以实现为foldr (>>) (return ())

infi = sequence_ infi'
infi = (foldr (>>) (return ()) infi')
infi = putStrLn "hello" >> (foldr (>>) (return ()) infi')
infi = putStrLn "hello" >> (putStrLn "hello" >> (foldr (>>) (return ()) infi'))
infi = putStrLn "hello" >> (putStrLn "hello" >> (putStrLn "hello" >> ...))

像这样将动作存储在流中还可以让您将它们作为一等值进行操作:

> sequence_ (take 3 infi')
hello
hello
hello

当 Haskell 运行时执行您的 main 操作时,它会计算 infi,找到 >> 表达式,计算其左侧参数以生成操作 putStrLn "hello"执行动作,然后继续到右边的参数——恰好又是infi。评估是由Monad 实例中IO 的内部模式匹配驱动的。

【讨论】:

    【解决方案3】:

    和程序很像

    ones = 1 : ones
    

    上面是递归的,是的。它无数次地调用自己,是的。但它确实返回。它返回一个无限列表。相比之下,

    noList = noList
    

    将永远循环而不会返回列表。 (实际上,GHC 运行时检测到这一点并抛出异常,但这与讨论无关。)

    同样,

    printOnes = print 1 >> printOnes
    -- or, equivalently
    printOnes = do
       print 1
       printOnes
    

    构建一个将永远打印1 的 IO 操作,即使它确实无限次递归。相反,

    noPrint = noPrint
    

    将永远循环并且永远不会返回 IO 操作。

    【讨论】:

      【解决方案4】:

      因为 infi 被设置为递归调用它的自身。永远不会满足 main 预期的“返回”,因为对 infi 的第一次调用永远不会返回。

      # The first call to infi will never return as the calls to infi
      # will just continue to add more calls to the stack, until you exceed
      # the size :)
      
      main ->
        infi ->
          infi ->
            infi -> ..
      

      【讨论】:

      • 这是不正确的。 Infi 将被无休止地评估。
      • 这正是我刚才所说的
      • @user3206627 不,你说它永远不会返回。由于 Haskell 是懒惰的,infi 确实返回,问题是它的 求值 需要很长时间。
      • 啊,没注意语言。我读了一些关于 Haskell 的文章,我对一件事感到好奇——虽然“返回”和“评估”的语义不同,但从概念上讲,是什么让这种情况与非评估语言有任何不同?我们仍然以递归动作结束。
      • @user3206627。懒惰的评估是不同的。
      猜你喜欢
      • 2013-10-05
      • 2016-01-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-23
      • 1970-01-01
      • 1970-01-01
      • 2012-04-06
      相关资源
      最近更新 更多