Warning: file_get_contents(/data/phpspider/zhask/data//catemap/5/date/2.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Haskell 列表生成函数的惰性求值?_Haskell_Lazy Evaluation - Fatal编程技术网

Haskell 列表生成函数的惰性求值?

Haskell 列表生成函数的惰性求值?,haskell,lazy-evaluation,Haskell,Lazy Evaluation,我目前正在读格雷厄姆·赫顿的《哈斯克尔编程》 在第40页中,介绍了玩具素性测试: factors :: Int -> [Int] factors n = [x | x <- [1..n], n `mod` x == 0] prime :: Int -> Bool prime n = factors n == [1,n] 因子::Int->[Int] 因子n=[x | x布尔 素数n=因子n==[1,n] 作者接着解释了如何 “确定一个数字不是素数并不需要函数 prime要

我目前正在读格雷厄姆·赫顿的《哈斯克尔编程》

在第40页中,介绍了玩具素性测试:

factors :: Int -> [Int]
factors n = [x | x <- [1..n], n `mod` x == 0]

prime :: Int -> Bool
prime n = factors n == [1,n]
因子::Int->[Int]
因子n=[x | x布尔
素数n=因子n==[1,n]
作者接着解释了如何

“确定一个数字不是素数并不需要函数 prime要产生它的所有因子,因为在惰性评估下 结果
False
将在一个或多个因素之外的任何因素出现时立即返回 “数字本身产生”

作为一个来自C和Java的人,我发现这一点令人震惊。我希望
factors
调用首先完成,将结果保存在堆栈中,并将控制传递给调用函数。但显然,这里正在执行一个非常不同的程序:在
factors
和eq中必须有一个循环对于添加到因子列表中的每个新元素,都将检查是否签入
prime

这怎么可能? 这难道不会让你更难对程序的执行顺序进行推理吗?

你会觉得它“令人震惊”,因为你并不期待它。一旦你习惯了它……好吧,实际上它仍然会让人感到困惑。但过了一段时间,你最终会把你的注意力集中在它上面

Haskell的工作原理是:当你调用一个函数时,什么都不会发生!调用记录在某个地方,仅此而已。这几乎不需要花费任何时间。你的“结果”实际上只是一个“我欠你的”告诉计算机运行什么代码才能得到结果。请注意,不是整个结果,只是第一步。对于整数之类的东西,只有一步。但是对于列表,每个元素都是一个单独的步骤

让我向您展示一个更简单的示例:

print (take 10 ([1..] ++ [0]))
f x = 42
g n = g (n + 1)  -- infinite recursion
<>我跟一个C++程序员说了一句“这是令人震惊的”。当然,“<代码> +[+ 0 ] < /代码>部分必须“找到列表的末尾”才能把它附加到零上。这个代码怎么能在有限的时间内完成?! 它看起来像是这样构建
[1..]
(在无限列表中),然后
+[0]
扫描到此列表的末尾并插入一个零,然后
去掉10个
只修剪前10个元素,然后打印。当然,这需要无限的时间

下面是实际发生的情况。最外层的函数是
take
,这就是我们开始的地方。(我们没想到,呃?)
take
的定义如下:

take 0 (   _) = []
take n (  []) = []
take n (x:xs) = x : (take (n-1) xs)
很明显,10!=0,所以第一行不适用。第二行或第三行也适用。所以现在
take
查看
[1..]+[0]
,看看它是空列表还是非空列表

这里最外层的函数是
(++)

(  []) ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)
所以我们需要找出哪个等式适用。要么左边的参数是一个空列表(第一行适用),要么不是(第二行适用)。因为
[1..]
是一个无限列表,第二行总是适用。所以
[1..]+[0]
的“结果”是
1:([2..]+[0])
。正如您所看到的,这并没有完全执行;但它的执行程度足以说明这是一个非空列表。这就是
所关心的

take 10 ([1..] ++ [0])
take 10 (1 : ([2..] ++ [0]))
1 : take 9 ([2..] ++ [0])
1 : take 9 (2 : ([3..] ++ [0]))
1 : 2 : take 8 ([3..] ++ [0])
1 : 2 : take 8 (3 : ([4..] ++ [0]))
1 : 2 : 3 : take 7 ([4..] ++ [0])
...
1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : take 0 ([11..] ++ [0])
1 : 2 : 3 : 4 : 5 : 6 : 7 : 8 : 9 : 10 : []
你看到了吗


现在,回到您的具体问题:
(==)
操作符获取一对列表并对它们进行迭代,逐元素比较以确保它们相等。一旦发现差异,它立即中止并返回false:

(  []) == (  []) = True
(x:xs) == (y:ys) = (x == y) && (xs == ys)
(   _) == (   _) = False
如果我们现在尝试,比如说,
prime 6

prime 6
factors 6 == [1,6]
??? == [1,6]
1 : ??? == [1,6]
??? == [6]
2 : ??? == [6]
False

我将重点关注这一点:

这难道不使我们更难对程序的执行顺序进行推理吗

是的,但在纯函数式编程中,求值顺序并不重要。例如:

(1 * 3) + (4 * 5)
问:哪个乘法先执行?答:我们不在乎,结果是一样的。即使是C编译器也可以在这里选择任何顺序

(f 1) + (f 2)
Q:首先执行哪个函数调用?A:我们不在乎,结果是一样的。在这里,C编译器也可以选择任何顺序。但是,在C中,函数
f
可能会有副作用,使得上面求和的结果取决于求值的顺序。在纯函数编程中,没有副作用,所以我们确实不需要e

而且,惰性允许任何函数定义的语义保持扩展

f x = e -- e is an expression which can use x
我们称之为
f2
。其结果应该与
e2/x}
相同,即
e
,其中
x
的每一次(自由)出现都被
2
所取代。这只是“展开定义”,就像在数学中一样。例如

f x = x + 4

-- f 2 ==> 2 + 4 ==> 6
然而,假设我们称之为
f(g2)
。懒惰使这等同于
e{g2/x}
。同样,在数学中也是如此。例如:

print (take 10 ([1..] ++ [0]))
f x = 42
g n = g (n + 1)  -- infinite recursion
然后我们仍然有
f(g2)=42{g2/x}=42
,因为没有使用
x
。我们不必担心是否定义了
g2
(永远循环)。定义展开总是有效的

这实际上使得对程序行为进行推理变得更简单

不过,懒惰也有一些缺点。一个主要的缺点是,尽管程序的语义(可以说)是更简单的是,评估程序的性能就更难了。要评估性能,你需要了解的不仅仅是最终结果:你需要有一个导致该结果的所有中间步骤的模型。特别是在高级代码中,或者当一些巧妙的优化开始时,这需要一些关于ru如何运行的专业知识时间确实有效

这难道不使我们更难对程序的执行顺序进行推理吗

可能-至少,对于那些来自过程/OO范式的人来说,我在其他急切的求值语言中对迭代器和函数编程做了很多工作,对我来说,是懒惰求值