Haskell 这个懒惰的评估示例背后的魔力是什么?

Haskell 这个懒惰的评估示例背后的魔力是什么?,haskell,optimization,lazy-evaluation,Haskell,Optimization,Lazy Evaluation,我想挑战GHC编译器,所以我写了这段代码(代码的细节实际上并不重要,只是为了说明必须做一些艰苦的工作才能得到这个无限列表的每个元素): 现在,我使用中描述的cleave函数 主要功能是什么 main :: IO () main = do print (take 5 (fst $ cleave hardwork), take 4 (snd $ cleave hardwork)) 与预期的一样,它打印值的速度很慢,因为它必须进行非常艰苦的工作才能得到结果。然而,令我惊讶的是,第一份名单一打

我想挑战GHC编译器,所以我写了这段代码(代码的细节实际上并不重要,只是为了说明必须做一些艰苦的工作才能得到这个无限列表的每个元素):

现在,我使用中描述的
cleave
函数

主要功能是什么

main :: IO ()
main = do
    print (take 5 (fst $ cleave hardwork), take 4 (snd $ cleave hardwork))
与预期的一样,它打印值的速度很慢,因为它必须进行非常艰苦的工作才能得到结果。然而,令我惊讶的是,第一份名单一打印出来,第二份名单就立即计算出来了

这是一个惊喜,因为
cleave-hardwork
的两次出现在代码中似乎是不相关的,而我们正在访问它们的不同部分,因此看起来一个幼稚的实现将再次努力获得第二个列表。然而,GHC似乎比我想象的更聪明

我的问题是:他们是如何做到这一点的?这背后的魔力是什么?更准确地说,, 运行时如何计算出一些已被评估的请求值(即使它们从未被访问)?这种簿记有费用吗

顺便说一句,为了确保我以正确的方式做正确的事情,我使用了一种不加糖的、循序渐进的方式来定义
努力工作
。当然还有其他的方法来实现它,但是如果它使用任何糖,那么行为可能取决于编译器如何对代码进行去糖的细节。此外,这种分步式样式使通过手动替换表达式进行纸张计算变得更容易

编辑

因此,根据答案,我重写了
hardwork
,使其不是CAF(这是一种比答案建议的更通用的方法):

现在它导致
main
在结果的两个部分都运行缓慢。但是如果我将
main
替换为

print (take 5 $ fst value, take 6 $ snd value) where value = cleave hardwork()

它的工作原理与第一个版本相同。因此,它看起来像是公认答案的证明。

努力工作
是一个常量,在程序的顶层定义,因此一旦计算一次,其结果就会被保存(就像您使用
让努力工作=…进入…
启动
main
)。如果您想计算它两次,可以将它定义为一个函数,忽略第一个参数或将其用作种子,例如将
辛勤工作的前几行
更改为

hardwork :: Int -> [Int]
hardwork seed = step1 where
    step1 = step2 seed

然后,如果您两次调用
hardwark 1
,每次都会重新计算相同的列表。

hardwark
是一个CAF,而不是一个函数,因此它的计算结果不会在两次访问之间得到GC


因此,您这里的意思是,一旦计算到第n个元素,所有计算值都将存储在其thunk中。我说得对吗?当然,这就是哈斯凯尔的评估工作原理。计算完成后,所有值都将无限期保存,直到垃圾收集器证明您无法再次访问它们为止。
hardwork :: a -> [Int]
hardwork = step1 where
    step1 _ = step2 1
    ...
print (take 5 $ fst value, take 6 $ snd value) where value = cleave hardwork()
hardwork :: Int -> [Int]
hardwork seed = step1 where
    step1 = step2 seed