【问题标题】:Good way of creating loops创建循环的好方法
【发布时间】:2014-05-10 04:36:59
【问题描述】:

Haskell 不像许多其他语言那样有循环。我了解它背后的原因以及在没有它们的情况下用于解决问题的一些不同方法。但是,当需要循环结构时,我不确定创建循环的方式是否正确/良好。

例如(平凡的函数):

dumdum = do
         putStrLn "Enter something"
         num <- getLine
         putStrLn $ "You entered: " ++ num
         dumdum

这工作正常,但代码中是否存在潜在问题?

另一个例子:

a = do 
    putStrLn "1"
    putStrLn "2"
    a

如果以 Python 等命令式语言实现,则如下所示:

def a():
     print ("1")
     print ("2")
     a()

这最终会导致最大递归深度错误。 Haskell 中似乎不是这种情况,但我不确定它是否会导致潜在问题。

我知道还有其他用于创建循环的选项,例如 Control.Monad.LoopWhileControl.Monad.forever——我应该改用它们吗? (我对 Haskell 还很陌生,还不了解 monad。)

【问题讨论】:

  • 你应该在谷歌上搜索“尾调用优化”。
  • a = mapM print [1..2] &gt;&gt; a?

标签: haskell


【解决方案1】:

对于一般迭代,递归函数调用本身绝对是可行的方法。如果您的调用位于tail position 中,它们不会使用任何额外的堆栈空间,并且其行为更像goto1。例如,这是一个使用常量堆栈空间对前 n 个整数求和的函数2

sum :: Int -> Int
sum n = sum' 0 n

sum' !s 0 = s
sum' !s n = sum' (s+n) (n-1)

大致相当于下面的伪代码:

function sum(N)

    var s, n = 0, N
    loop: 
       if n == 0 then
           return s
       else
           s,n = (s+n, n-1)
           goto loop

请注意,在 Haskell 版本中,我们如何将函数参数用于求和累加器而不是可变变量。这是尾递归代码非常常见的模式。

到目前为止,带有尾调用优化的一般递归应该为您提供 goto 的所有循环功能。唯一的问题是手动递归(有点像 goto,但更好一点)相对非结构化,我们经常需要仔细阅读使用它的代码以了解发生了什么。就像命令式语言有循环机制(for、while 等)来描述最常见的迭代模式一样,在 Haskell 中我们可以使用高阶函数来完成类似的工作。例如,mapfoldl'3 等许多列表处理函数类似于纯代码中的直接 for 循环,并且在处理一元代码时,在 Control.Monad 或您可以使用的monad-loops 包。最后,这是一个风格问题,但我会错误地使用高阶循环函数。


1 您可能想查看"Lambda the ultimate GOTO",这是一篇关于尾递归如何与传统迭代一样高效的经典文章。此外,由于 Haskell 是一种惰性语言,因此在某些情况下,非尾部位置的递归仍然可以在 O(1) 空间中运行(搜索“Tail recursion modulo cons”)

2 那些感叹号是为了让累加器参数被急切地求值,所以加法与递归调用同时发生(Haskell 默认是惰性的)。如果需要,您可以省略“!”,但您可能会遇到space leak 的风险。

3 由于前面提到的空间泄漏问题,请始终使用foldl' 而不是foldl

【讨论】:

    【解决方案2】:

    我知道还有其他用于创建循环的选项,例如 Control.Monad.LoopWhileControl.Monad.forever——我应该使用这些选项吗? (我对 Haskell 还很陌生,还不了解 monad。)

    是的,你应该这样做。你会发现在“真正的”Haskell 代码中,显式递归(即在函数中调用函数)实际上非常少见。有时,人们这样做是因为它是最易读的解决方案,但通常使用 forever 之类的东西要好得多。

    事实上,说 Haskell 没有循环只是半真半假。语言中没有内置循环是正确的。但是,在标准库中,循环的种类比在命令式语言中发现的要多。在诸如 Python 之类的语言中,您有“for 循环”,您可以在需要迭代某些内容时使用它。在 Haskell 中,你有

    • map, fold, any, all, scan, mapAccum, unfold, find, filter (Data.List)
    • mapM, forM, forever (Control.Monad)
    • traverse, for (Data.Traversable)
    • foldMap, asum, concatMap (Data.Foldable)

    还有很多很多其他的!

    这些循环中的每一个都是针对特定用例量身定制的(有时还针对特定用例进行了优化)。

    在编写 Haskell 代码时,我们会大量使用它们,因为它们使我们能够更智能地推理代码和数据。当您看到有人在 Python 中使用 for 循环时,您必须阅读并理解该循环才能知道它的作用。当您看到有人在 Haskell 中使用 map 循环时,您无需阅读它的作用就知道它不会将任何元素添加到列表中——因为我们有“函子定律”,它们只是规则也就是说任何map 函数都必须这样或那样工作!


    回到你的例子,我们可以先定义一个askNum“函数”(从技术上讲,它不是一个函数,而是一个IO值......我们可以暂时假设它是一个函数)它要求用户输入一些东西只有一次,然后显示给他们。当您希望您的程序一直询问时,您只需将该“函数”作为参数提供给 forever 循环,forever 循环将一直询问!

    整个事情可能看起来像:

    askNum = do
             putStrLn "Enter something"
             num <- getLine
             putStrLn "You entered: " ++ num
    
    dumdum = forever askNum
    

    那么更有经验的程序员可能会在这种情况下摆脱askNum“函数”,并将整个事情变成

    dumdum = forever $ do
               putStrLn "Enter something"
               num <- getLine
               putStrLn "You entered: " ++ num
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-03-21
      • 1970-01-01
      • 1970-01-01
      • 2020-01-14
      • 2019-04-17
      • 1970-01-01
      • 2012-10-16
      相关资源
      最近更新 更多