globToRegex' (c:cs) = escape c ++ globToRegex' cs
这个函数不是尾递归的,它说答案依赖于Haskell非严格(懒惰)评估策略。 (++)
运算符的简单定义如下,并不是尾递归。
(++) :: [a] -> [a] -> [a] (x:xs) ++ ys = x : (xs ++ ys) [] ++ ys = ys
在严格的语言中,如果我们评估
"foo" ++ "bar"
,则构造整个列表,然后返回。在严格的评估需要之前,非严格评估会将大部分工作推迟。如果我们要求表达式
"foo" ++ "bar"
的元素,函数定义的第一个模式匹配,我们返回表达式x : (xs ++ ys)
。 因为(:)
构造函数是非严格的,所以xs ++ ys
的评估可以推迟:我们以任何需要的速率生成更多结果元素。当我们生成更多结果时,我们将不再使用x
,因此垃圾收集器可以回收它。由于我们按需生成结果元素,并且不保留我们完成的部分,因此编译器可以在恒定空间中评估我们的代码 。
(强调补充。)
上面粗体的解释对Haskell来说是必不可少的,但是
x:(xs ++ ys)
会在恒定的空间中评估”,怎么样?这听起来像尾递归一样!答案 0 :(得分:8)
请记住,"foo"
只是'f':'o':'o':[]
的语法糖。
也就是说,String
只是[Char]
的别名,它只是一个链接的字符列表。
当客户端代码消费链接列表时,它会将其分解为头尾(例如x:xs
),对头部做某事(如果需要),然后进行递归为尾巴。
当您的代码构建链接列表时,由于延迟评估,所有它需要做的就是返回 thunk 或承诺它将返回链接列表要求。当头被解除引用时,它是按需提供的,并且尾部留作列表其余部分的承诺。
应该很容易看出,只要列表没有被复制或以其他方式存储,每个thunk将被使用一次然后被丢弃,以便整个存储空间是恒定的。
许多严格的语言暴露了一种机制(通常称为生成器)来完成相同类型的惰性列表生成,但是对于懒惰的评估,这些功能作为语言的一部分“免费”出现 - 实质上,所有Haskell列表都是生成器!
答案 1 :(得分:7)
与其他FP语言相比,依赖于延迟评估而不是尾递归是Haskell的一个特征。两者在限制内存使用方面发挥相关作用;哪一种是适当的机制取决于所产生的数据。
如果您的输出可以逐步消耗,那么您应该更喜欢利用延迟评估,因为输出只会在需要时生成,从而限制堆消耗。如果你急切地构造输出,那么你将自己辞去使用堆,但至少可以通过尾递归来保存堆栈。
如果您的输出无法逐步消耗 - 也许您正在计算Int
- 那么懒惰可能会让您留下一堆不需要的东西,其评估将会破坏您的筹码。在这种情况下,需要调用严格的累加器和尾递归。
所以,如果你渴望你可能会浪费堆构建一个大数据结构。如果你很懒,你可以将简化(例如将1 + 1
减少到2
)推迟到堆中,但最终只会在支付吹笛者时玷污你的筹码。
要玩硬币的两面,请考虑foldl'
和foldr
。
答案 2 :(得分:5)
尾递归会使堆栈保持不变,但是在严格的语言中,堆会随着x : (xs ++ ys)
的计算而增长。在Haskell中,因为它是非严格的,所以在计算下一个值之前将释放x
(除非调用者不必要地持有对x
的引用),因此堆也保持不变。