【问题标题】:Why can Tail Recursion Modulo Cons be optimized?为什么尾递归模数可以优化?
【发布时间】:2020-06-13 22:08:32
【问题描述】:

例如,这不是尾调用:

map _ [] = []
map f (x : xs) = f x : map f xs

(:) 数据构造函数保护的递归调用,因此它不会像其他语言中的等价物那样建立一个巨大的堆栈。它是这样工作的:

map (+1) (1 : 2 : 3 : [])
2 : map (+1) (2 : 3 : [])
2 : 3 : map (+1) (3 : [])
2 : 3 : 4 : map (+1) []
2 : 3 : 4 : []

为什么不

map (+1) (1 : 2 : 3 : [])
2 : map (+1) (2 : 3 : [])
2 : (3 : map (+1) (3 : []))
2 : (3 : (4 : map (+1) []))
2 : (3 : (4 : []))
2 : (3 : [4])
2 : [3, 4]
[2, 3, 4]

这和WHNF有关,但我还是不能很好理解:(

【问题讨论】:

  • 这里没有优化,只是惰性求值。
  • 你的“为什么不”的开头看起来和你的“像这样工作”一模一样,除了一些多余的括号,之后的所有内容都只是语法糖。
  • @JosephSible-ReinstateMonica 他们希望[...] 代表一个完全实现的列表,这是我的解释。

标签: haskell recursion functional-programming lazy-evaluation tailrecursion-modulo-cons


【解决方案1】:

因为: 是懒惰的。它本身不会触发对其第二个参数的评估。

你展示的并不是故事的全部。 map 也不会做你自己展示的事情,只有当其他消费者要求其结果最终由 main(或 GHCi 的 REPL)要求时。比如,

GHCi> take 2 (map (1+) [1..4]
   {- implied `putStrLn . show` causes this -}
   = take 2 (2 : map (1+) (enumFromTo 2 4))
   = 2 : take 1 (map (1+) (enumFromTo 2 4))
   = 2 : take 1 (3 : map (1+) (enumFromTo 3 4))
   = 2 : 3 : take 0 (map (1+) (enumFromTo 3 4))
   = 2 : 3 : []

输入列表的其余部分甚至没有被计算,因为take 不需要来自map,因此不需要输入列表中的任何更多元素。

附注:TRMC 正在急切地评估语言的术语。在 Haskell 中,它被称为保护递归。递归调用必须在惰性构造函数之后。

我不相信 Haskell(即 GHC)在严格的构造函数情况下具有 TRMC 优化。如果结果类型是幺半群,它可能会像列表一样:

[a] ++ ([b] ++ ([c] ++ ....))
=
([a] ++ [b]) ++ ([c] ++ ....)

因此,在带有 TRMCO 的热切语言中,与其首先评估顶部 : 的两个参数,实际上像第二个 sn-p 所暗示的那样打开 O(n) 计算堆栈,而是创建顶部 :首先,然后填充其正确的插槽,在恒定堆栈空间中以迭代方式工作(就像维基百科代码 sn-ps 显示)。

但在 Haskell 中,这一切都不适用,当构造函数是惰性的并且无论如何都不会触发任何参数评估时。

【讨论】:

    猜你喜欢
    • 2011-01-25
    • 2017-05-23
    • 1970-01-01
    • 2020-05-01
    • 2012-03-04
    • 1970-01-01
    • 2010-10-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多