Haskell 为什么递归'let'使空间更高效?

Haskell 为什么递归'let'使空间更高效?,haskell,frp,Haskell,Frp,我在研究函数式反应式编程时发现了这句话,作者是刘海和保罗·胡达克(第5页): 这里的差异似乎很小,但它极大地促进了空间效率。为什么以及如何发生?我做的最好的猜测是手工评估: r = \x -> x: r x r 3 -> 3: r 3 -> 3: 3: 3: ........ -> [3,3,3,......] 如上所述,我们需要为这些递归创建无限的新thunk。然后我尝试评估第二个: r = \x -> let

我在研究函数式反应式编程时发现了这句话,作者是刘海和保罗·胡达克(第5页):

这里的差异似乎很小,但它极大地促进了空间效率。为什么以及如何发生?我做的最好的猜测是手工评估:

    r = \x -> x: r x
    r 3

    -> 3: r 3 
    -> 3: 3: 3: ........
    -> [3,3,3,......]
如上所述,我们需要为这些递归创建无限的新thunk。然后我尝试评估第二个:

    r = \x -> let xs = x:xs in xs
    r 3

    -> let xs = 3:xs in xs
    -> xs, according to the definition above: 
    -> 3:xs, where xs = 3:xs
    -> 3:xs:xs, where xs = 3:xs
在第二种形式中,
xs
出现,并且可以在它发生的每个地方共享,所以我想这就是为什么我们只需要
O(1)
空格,而不是
O(n)
。但我不确定我是否正确

顺便说一句:关键词“共享”来自同一篇论文的第4页:

这里的问题是标准的按需调用评估规则 无法识别该功能:

f = λdt → integralC (1 + dt) (f dt) 
同:

f = λdt → let x = integralC (1 + dt) x in x
前一种定义导致递归调用中重复工作 到f,而在后一种情况下,计算是共享的


简单地说,变量是共享的,但函数应用程序不是。在

repeat x = x : repeat x
从语言的角度来看,(co)递归调用repeat与同一个参数是巧合的。因此,如果没有额外的优化(称为静态参数转换),函数将被反复调用

但是当你写作的时候

repeat x = let xs = x : xs in xs
repeat x = let xs = x : xs in xs
没有递归函数调用。取一个
x
,并使用它构造一个循环值
xs
。所有共享都是明确的


如果您想更正式地理解它,您需要熟悉惰性评估的语义,例如。

您关于共享
xs
的直觉是正确的。在写作时,以重复而非整体的方式重述作者的例子:

repeat x = x : repeat x
该语言无法识别右侧的
repeat x
与表达式
x:repeat x
生成的值相同。如果你写

repeat x = let xs = x : xs in xs
repeat x = let xs = x : xs in xs
您正在显式创建一个结构,该结构在计算时如下所示:

{hd: x, tl:|}
^          |
 \________/

通过图片最容易理解:

  • 第一版

    repeat x = x : repeat x
    
    创建一个以thunk结尾的
    (:)
    构造函数链,当您需要更多构造函数时,它将用更多构造函数替换自己。因此,O(n)空间

  • 第二版

    repeat x = let xs = x : xs in xs
    
    使用
    let
    来“打结”,创建一个引用自身的
    (:)
    构造函数