创建循环的好方法

时间:2014-05-10 04:36:59

标签: haskell

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。)

2 个答案:

答案 0 :(得分:9)

对于一般迭代,具有递归函数调用本身绝对是可行的方法。如果您的通话位于tail position,则他们不会使用任何额外的堆叠空间,行为更像goto 1 。例如,这是一个使用常量堆栈空间 2 对前n个整数求和的函数:

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版本中我们如何使用sum累加器的函数参数而不是可变变量。这是尾递归代码的常见模式。

到目前为止,使用尾调用优化的一般递归应该会给你所有的循环功能。唯一的问题是手动递归(有点像getos,但有点好)是相对非结构化的,我们经常需要仔细阅读使用它来查看正在发生的事情的代码。就像命令式语言如何使用循环机制(for,while等)来描述最常见的迭代模式一样,在Haskell中我们可以使用更高阶的函数来完成类似的工作。例如,许多列表处理函数,如mapfoldl' 3 类似于纯代码中的简单for循环,当处理monadic代码时,控件中有函数.Monad或您可以使用的monad-loops包中。最后,它是一个风格问题,但我会更倾向于使用更高阶的循环函数。


1 您可能需要查看"Lambda the ultimate GOTO",这是一篇关于尾递归如何与传统迭代一样高效的经典文章。另外,由于Haskell是一种惰性语言,在某些情况下,非尾部位置的递归仍然可以在O(1)空间中运行(搜索&#34; Tail递归模数缺点&#34;)

2 这些感叹号是为了使累加器参数得到热切评估,因此添加与递归调用同时发生(默认情况下Haskell是惰性的)。如果需要,您可以省略&#34;!&#34; s然后您冒着遇到space leak的风险。

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

答案 1 :(得分:6)

  

我知道还有其他选项可用于创建Control.Monad.LoopWhileControl.Monad.forever等循环 - 我应该使用它们吗? (我仍然是Haskell的新手,还不了解monad。)

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

事实上,说Haskell没有循环只是半真半假。这是正确的,语言中没有内置循环。但是,在标准库中,有多种循环,而不是在命令式语言中。在像Python这样的语言中,只要需要迭代某些东西,就可以使用“for循环”。在Haskell,你有

  • mapfoldanyallscanmapAccumunfoldfindfilter(Data.List)
  • mapMforMforever(Control.Monad)
  • traversefor(Data.Traversable)
  • foldMapasumconcatMap(Data.Foldable)

以及许多其他人!

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

在编写Haskell代码时,我们大量使用它们,因为它们允许我们更加智能地推理我们的代码和数据。当你看到有人在Python中使用for循环时,你必须阅读并理解循环以了解它的作用。当你看到有人在Haskell中使用map循环时,你知道它没有阅读它的作用,它不会将任何元素添加到列表中 - 因为我们有“Functor法则”只是说任何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