我是一名新生,我在计算机科学专业学习。我们正在处理Haskell,虽然我理解Haskell的想法,但我似乎无法弄清楚我们应该看到的代码片段究竟是如何工作的:
module U1 where
double x = x + x
doubles (d:ds) = (double d):(doubles ds)
ds = doubles [1..]
我承认,对于知道发生了什么的人来说,这似乎相当简单,但我无法绕过它。如果我写'#34;取5 ds",它显然会回复[2,4,6,8,10]。我没有得到的,是为什么。
这是我的思路:我叫ds,然后寻找双打。因为我也提交了值[1 ..],双精度(d:ds)应该表示d = 1且ds = [2 ..],对吗?然后我将d加倍,返回2并将其放在列表的开头(数组?)。然后它调用自身,将ds = [2 ..]转换为d = 2和ds = [3 ..],然后再次将d加倍并再次调用自身,依此类推,直到它可以返回5个值, [2,4,6,8,10]。
首先,我的理解是对的吗?我的思绪中是否有任何严重的错误? 第二,因为它似乎将所有加倍的d保存到列表中以便稍后调用,那么该列表的名称是什么?我在哪里确切地定义了它?
提前致谢,希望你能帮助学生理解这个x)
答案 0 :(得分:5)
我认为关于doubles
如何遍历无限列表的每个元素的递归/循环部分你是正确的。
现在关于
它似乎将所有加倍的d保存到列表中以便稍后调用,是什么 那个名单的名字?我在哪里确切地定义了它?
这涉及一个在Haskell中称为Lazy Evaluation的功能。该列表未经预先计算并存储在任何位置。相反,您可以想象列表是C ++中的函数对象,可以在需要时生成元素。 (您可能会看到的正常语言是表达式是按需评估的)。所以当你这样做时
take 5 [1..]
[1..]
可以被视为与head
,take
等一起使用时生成数字的函数对象。所以,
take 5 [1..] == (1 : take 4 [2..])
此处[2..]
也是一个"功能对象"给你数字。同样,你可以拥有
take 5 [1..] == (1 : 2 : take 3 [3..]) == ... (1 : 2 : 3 : 4 : 5 : take 0 [6..])
现在,我们不需要关心[6..]
,因为任何take 0 xs
的{{1}}都是xs
。因此,我们可以
[]
无需存储任何"无限"像take 5 [1..] == (1 : 2 : 3 : 4 : 5 : [])
这样的列表。如果您想了解Lazy计算实际上是如何发生的,可以将它们视为函数对象/生成器。
答案 1 :(得分:1)
你的思路看起来是正确的。其中唯一的微小不准确在于使用表达式来描述计算,例如“它加倍2而然后调用自身......”。在纯函数式编程语言中,例如Haskell,实际上没有固定的评估顺序。具体来说,在
double 1 : double [2..]
在将列表的其余部分加倍之前是否发生加倍1是未指定的。理论结果保证了秩序确实无关紧要,因为 - 大致 - 即使你以不同的顺序评估你的表达式,你也会得到相同的结果。我建议您使用Lambda Bubble Pop网站查看此属性:您可以按不同顺序弹出气泡以模拟任何评估顺序。无论你做什么,你都会得到相同的结果。
请注意,由于评估顺序无关紧要,因此Haskell编译器可以自由选择它认为最适合您的代码的任何评估顺序。例如,让ds
定义为代码的最后一行,并考虑
take 5 (drop 5 ds)
这导致[12,14,16,18,20]
。请注意,编译器不需要将前5个数字加倍,因为您正在删除它们,因此可以在完全计算之前删除它们(!!)。
如果你想进行实验,可以自己定义一个计算成本非常高的函数(比如,在递归定义后写fibonacci
)。
fibonacci 0 = 0
fibonacci 1 = 1
fibonacci n = fibonacci (n-1) + fibonacci (n-2)
然后,定义
const5 n = 5
并计算
fibonacci 100
并观察实际需要多长时间。然后,评估
const5 (fibonacci 100)
并且看到结果立即到达 - 参数甚至没有计算(!),因为不需要它。