Haskell 创建循环的好方法

Haskell 创建循环的好方法,haskell,Haskell,Haskell不像许多其他语言那样有循环。我理解它背后的原因,以及在没有它们的情况下用来解决问题的一些不同方法。但是,当需要循环结构时,我不确定创建循环的方式是否正确/良好 例如(平凡函数): 如果使用命令式语言(如Python)实现,它将如下所示: askNum = do putStrLn "Enter something" num <- getLine putStrLn "You entered: " ++ num dumdum

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

例如(平凡函数):

如果使用命令式语言(如Python)实现,它将如下所示:

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

dumdum = forever askNum
def a():
打印(“1”)
打印(“2”)
()
这最终会导致最大递归深度错误。哈斯克尔的情况似乎并非如此,但我不确定这是否会导致潜在问题


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

对于一般迭代,递归函数调用本身肯定是一条路。如果您的调用在中,则它们不会使用任何额外的堆栈空间,其行为更像
goto
1。例如,下面是一个使用常量堆栈空间2对前n个整数求和的函数:

它大致相当于以下伪代码:

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

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


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

2这些感叹号是为了使累加器参数得到急切的求值,因此加法与递归调用同时发生(默认情况下Haskell是惰性的)。如果愿意,您可以省略“!”s,但这样您就有可能遇到错误

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

我知道还有其他创建循环的选项,比如
Control.Monad.LoopWhile
Control.Monad.forever
——我应该改用它们吗?(我对哈斯克尔还是很陌生,还不了解单子。)

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

事实上,说Haskell没有循环只是半个事实。语言中没有内置循环是正确的。然而,在标准库中,循环的种类比命令式语言中的还要多。在Python之类的语言中,您有“for循环的
”,每当您需要迭代某个内容时,都可以使用该循环。在哈斯克尔,你有

  • 地图
    折叠
    任何
    全部
    扫描
    地图累积
    展开
    查找
    过滤器
    (数据.列表)
  • mapM
    表单
    永远
    (Control.Monad)
  • 遍历
    用于
    (数据.可遍历)
  • foldMap
    asum
    concatMap
    (Data.Foldable)
还有很多很多其他的

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

在编写Haskell代码时,我们会大量使用它们,因为它们允许我们更智能地对代码和数据进行推理。当您看到有人在Python中使用
for
循环时,您必须阅读并理解该循环以了解它的功能。当您看到有人在Haskell中使用
map
循环时,您不必阅读它的功能,就知道它不会将任何元素添加到列表中–因为我们有“函子定律”,它们只是说明任何
map
函数必须以这种或那种方式工作的规则


回到您的示例,我们可以首先定义一个
askNum
“函数”(从技术上讲,它不是一个函数,而是一个IO值……我们可以暂时假设它是一个函数),它要求用户只输入一次内容,然后将其显示给他们。当你想让你的程序永远保持询问时,你只需要把这个“函数”作为
永远
循环的参数,而
永远
循环将永远保持询问

整个事情可能看起来像:

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

dumdum = forever askNum

你应该用谷歌搜索“尾部呼叫优化”。
a=mapmprint[1..2]>>a
function sum(N)

    var s, n = 0, N
    loop: 
       if n == 0 then
           return s
       else
           s,n = (s+n, n-1)
           goto loop
askNum = do
         putStrLn "Enter something"
         num <- getLine
         putStrLn "You entered: " ++ num

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