Answers:
主要是因为它可以提高效率-如果不使用值,则无需计算值。例如,我可以将三个值传递给一个函数,但是根据条件表达式的顺序,实际上只能使用一个子集。在像C这样的语言中,无论如何都要计算所有三个值。但是在Haskell中,仅计算必要的值。
它还允许诸如无限列表之类的很酷的东西。我不能像C这样的语言拥有无限的列表,但是在Haskell中,这没问题。无限列表经常在某些数学领域中使用,因此具有操纵它们的功能可能会很有用。
惰性评估的一个有用示例是使用quickSort
:
quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)
如果现在要查找列表的最小值,则可以定义
minimum ls = head (quickSort ls)
它首先对列表进行排序,然后获取列表的第一个元素。但是,由于延迟评估,因此仅计算头。例如,如果我们使用列表的最小值,则[2, 1, 3,]
quickSort将首先过滤掉所有小于两个的元素。然后,它对已经足够的对象执行quickSort(返回单例列表[1])。由于采用了惰性计算,因此其余部分永远不会被排序,从而节省了大量计算时间。
这当然是一个非常简单的示例,但是对于大型程序,懒惰的工作方式相同。
但是,所有这些都有一个缺点:很难预测程序的运行速度和内存使用情况。这并不意味着惰性程序会变慢或占用更多内存,但是很高兴知道。
take k $ quicksort list
仅需O(n + k log k)时间,其中n = length list
。对于非延迟比较排序,这将始终花费O(n log n)时间。
我发现惰性评估对许多事情很有用。
首先,所有现有的惰性语言都是纯净的,因为很难推理惰性语言的副作用。
纯语言使您可以使用方程式推理对函数定义进行推理。
foo x = x + 3
不幸的是,在非惰性设置中,失败返回的语句比在惰性设置中返回的语句更多,因此在ML之类的语言中,它的用处不大。但是用懒惰的语言,您可以放心地考虑平等。
其次,像Haskell这样的惰性语言不需要诸如ML中的“值限制”之类的东西。这导致语法上的混乱。类似于ML的语言需要使用var或fun这样的关键字。在Haskell中,这些事情可以归结为一个概念。
第三,懒惰使您可以编写可理解的功能非常强大的代码。在Haskell中,通常编写如下的函数体:
foo x y = if condition1
then some (complicated set of combinators) (involving bigscaryexpression)
else if condition2
then bigscaryexpression
else Nothing
where some x y = ...
bigscaryexpression = ...
condition1 = ...
condition2 = ...
通过此功能,您可以“自上而下”地理解函数的主体。类似ML的语言会迫使您使用let
严格评估的。因此,您不敢将let子句“提升”到函数的主体中,因为如果它昂贵(或有副作用),则您不希望始终对其进行评估。Haskell可以将详细信息“推送”到where子句,因为它知道该子句的内容只会根据需要进行评估。
在实践中,我们倾向于使用防护措施并进一步崩溃以:
foo x y
| condition1 = some (complicated set of combinators) (involving bigscaryexpression)
| condition2 = bigscaryexpression
| otherwise = Nothing
where some x y = ...
bigscaryexpression = ...
condition1 = ...
condition2 = ...
第四,懒惰有时会为某些算法提供更为优雅的表达。在Haskell中,一种懒惰的“快速排序”是一种单行代码,它的好处是,如果仅查看前几项,则只需支付与仅选择这些项的成本成比例的成本。没有什么可以阻止您严格执行此操作,但是您可能每次都必须重新编码算法以实现相同的渐近性能。
第五,懒惰使您可以用该语言定义新的控制结构。您不能使用严格的语言编写新的“ if .. then .. else ..”之类的构造。如果您尝试定义如下功能:
if' True x y = x
if' False x y = y
如果使用严格的语言,则无论条件值如何,都将评估两个分支。当您考虑循环时,情况会变得更糟。所有严格的解决方案都要求该语言为您提供某种形式的报价或显式的lambda构造。
最后,同样,某些类型系统中处理副作用的最佳机制(例如monad)实际上只能在惰性环境中有效表达。通过将F#的工作流程与Haskell Monads的复杂性进行比较可以证明这一点。(您可以使用严格的语言来定义monad,但是不幸的是,由于缺乏懒惰,您经常会失败一两个monad法则,相比之下,工作流程会带来很多严格的限制。)
let
是一种危险的野兽,在R6RS方案中,#f
只要严格按照一个循环进行,它就可以在您的任期中出现random !没有双关语的意图,但是严格地说let
,在懒惰的语言中,更递归的绑定是明智的。严格性也加剧了这样一个事实,即where
除了SCC之外,根本没有办法对相对效果进行排序,这是语句级的构造,其效果可以严格按照任何顺序发生,即使您使用的是纯语言,您也会感到厌恶。#f
问题。严格where
将代码与非本地问题混为一谈。
ifFunc(True, x, y)
将同时评估x
和y
而不是x
。
正常订单评估与懒惰评估(在Haskell中)有所不同。
square x = x * x
评估以下表达式...
square (square (square 2))
...渴望评估:
> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256
...具有正常订单评估:
> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * (square (square 2))
> ((2 * 2) * (square 2)) * (square (square 2))
> (4 * (square 2)) * (square (square 2))
> (4 * (2 * 2)) * (square (square 2))
> (4 * 4) * (square (square 2))
> 16 * (square (square 2))
> ...
> 256
...懒惰的评价:
> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * ((square 2) * (square 2))
> ((2 * 2) * (2 * 2)) * ((2 * 2) * (2 * 2))
> (4 * 4) * (4 * 4)
> 16 * 16
> 256
那是因为懒惰的评估会查看语法树并进行树转换...
square (square (square 2))
||
\/
*
/ \
\ /
square (square 2)
||
\/
*
/ \
\ /
*
/ \
\ /
square 2
||
\/
*
/ \
\ /
*
/ \
\ /
*
/ \
\ /
2
...而正常顺序评估仅会进行文本扩展。
这就是为什么我们在使用惰性评估时会变得更强大(评估终止的频率比其他策略高),而性能则等同于热切评估(至少在O表示法中)。
与CPU相关的惰性评估与与RAM相关的垃圾收集的方法相同。GC允许您假装您拥有无限的内存量,因此可以根据需要在内存中请求尽可能多的对象。运行时将自动回收不可用的对象。LE允许您假装您拥有无限的计算资源-您可以根据需要进行任意数量的计算。运行时将不会执行不必要的(对于给定的情况)计算。
这些“伪装”模型的实际优势是什么?它在某种程度上使开发人员从管理资源中释放出来,并从源代码中删除了一些样板代码。但更重要的是,您可以在更广泛的上下文中有效地重用您的解决方案。
想象您有一个数字S和一个数字N的列表。您需要从列表S中找到最接近数字N的数字M。您可以有两个上下文:单个N和一些N的列表L(对于L中的每个N,ei您会在S中查找最接近的M)。如果使用惰性求值,则可以对S进行排序并应用二进制搜索以找到最接近N的M。对于良好的惰性排序,单个N和O(ln(size(S))*都需要O(size(S))*个步骤。 (size(S)+ size(L)))个步骤。如果没有懒惰的评估来获得最佳效率,则必须为每个上下文实现算法。
如果您相信Simon Peyton Jones,那么懒惰的评估本身并不重要而只是作为迫使设计师保持语言纯正的“上衣”。我发现自己对此观点表示同情。
理查德·伯德(Richard Bird),约翰·休斯(John Hughes)以及较小的拉尔夫·欣泽(Ralf Hinze)能够通过懒惰的评估做出令人惊奇的事情。阅读他们的作品将帮助您欣赏它。很好的起点是Bird出色的Sudoku求解器和Hughes的论文《为什么函数编程很重要》。
IO
单子)的签名main
会String -> String
,你可能已经编写正确的互动节目。
IO
monad?
考虑一个井字游戏程序。它具有四个功能:
这样可以很好地将关注点分离。特别是,移动生成功能和棋盘评估功能是唯一需要了解游戏规则的功能:移动树和minimax功能是完全可重用的。
现在让我们尝试实施国际象棋而不是井字游戏。在“急切”(即常规)语言中,这将不起作用,因为移动树无法容纳在内存中。因此,现在必须将电路板评估和移动生成功能与移动树和minimax逻辑混合在一起,因为必须使用minimax逻辑来确定要生成的移动。我们漂亮的干净模块化结构消失了。
但是,在惰性语言中,仅根据minimax函数的要求生成移动树的元素:在让minimax放到顶部元素之前,不需要生成整个移动树。因此,我们干净的模块化结构仍然可以在真实游戏中使用。
考虑一下:
if (conditionOne && conditionTwo) {
doSomething();
}
仅当conditionOne为true 且 conditionTwo为true时,才会执行doSomething()方法。在conditionOne为false的情况下,为什么需要计算conditionTwo的结果?在这种情况下,对conditionTwo的评估会浪费时间,特别是如果您的条件是某些方法过程的结果时。
那是懒惰的评估兴趣的一个例子...
它可以提高效率。这是显而易见的,但实际上并不是最重要的。(另请注意,懒惰可以杀死效率太-这个事实是不会立即明显然而,通过存储了大量的临时结果,而不是立即计算它们,就可以使用了一个巨大的RAM量。)
它使您可以使用普通的用户级代码定义流控制结构,而不是将其硬编码为该语言。(例如,Java具有for
循环; Haskell具有for
功能。Java具有异常处理; Haskell具有各种类型的异常monad。C#具有goto
; Haskell具有延续monad ...)
它使您可以将用于生成数据的算法与用于确定要生成多少数据的算法分离。您可以编写一个函数,该函数生成一个概念上无限的结果列表,而另一个函数则可以处理它决定所需数量的列表。更重要的是,您可以拥有五个生成器函数和五个使用者函数,并且可以有效地产生任何组合-而不是手动编码同时组合两个动作的5 x 5 = 25个函数。(!)我们都知道去耦是一件好事。
它或多或少迫使您设计一种纯函数式语言。它总是诱人走捷径,但在一个慵懒的语言,丝毫杂质,使你的代码似地不可预知的,强烈有碍于走捷径。
懒惰的一个巨大好处是能够以合理的摊销范围编写不可变的数据结构。一个简单的示例是不可变堆栈(使用F#):
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
let rec append x y =
match x with
| EmptyStack -> y
| StackNode(hd, tl) -> StackNode(hd, append tl y)
该代码是合理的,但是在最佳,最差和平均情况下,将两个堆栈x和y相加需要O(x的长度)时间。追加两个堆栈是一个整体操作,它触摸堆栈x中的所有节点。
我们可以将数据结构重写为惰性堆栈:
type 'a lazyStack =
| StackNode of Lazy<'a * 'a lazyStack>
| EmptyStack
let rec append x y =
match x with
| StackNode(item) -> Node(lazy(let hd, tl = item.Force(); hd, append tl y))
| Empty -> y
lazy
通过在其构造函数中暂停对代码的评估来工作。一旦使用进行了评估.Force()
,则返回值将被缓存,并在以后的每一次重用.Force()
。
在惰性版本中,追加操作是O(1)操作:它返回1个节点并中止列表的实际重建。当您获得此列表的头部时,它将评估节点的内容,强制其返回头部,并使用其余元素创建一个悬浮,因此,以列表的头部进行O(1)操作。
因此,我们的惰性列表处于不断重建的状态,在遍历列表的所有元素之前,您无需支付重建列表的费用。使用懒惰,此列表支持O(1)精简和附加。有趣的是,由于我们在访问节点之前不评估节点,因此完全有可能构造具有潜在无限元素的列表。
上面的数据结构不需要在每次遍历时都重新计算节点,因此它们与.NET中的原始IEnumerables明显不同。
此摘要显示了惰性评估和非惰性评估之间的区别。当然,这个斐波那契函数本身可以被优化,并且可以使用惰性求值而不是递归求值,但这会破坏示例。
假设我们可以必须对某些东西使用前20个数字,而不必进行惰性评估,所有这20个数字都必须预先生成,但是对于惰性评估,它们只会根据需要生成。因此,您仅在需要时支付计算价格。
样品输出
不偷懒一代:0.023373 惰性代:0.000009 非延迟输出:0.000921 延迟输出:0.024205
import time
def now(): return time.time()
def fibonacci(n): #Recursion for fibonacci (not-lazy)
if n < 2:
return n
else:
return fibonacci(n-1)+fibonacci(n-2)
before1 = now()
notlazy = [fibonacci(x) for x in range(20)]
after1 = now()
before2 = now()
lazy = (fibonacci(x) for x in range(20))
after2 = now()
before3 = now()
for i in notlazy:
print i
after3 = now()
before4 = now()
for i in lazy:
print i
after4 = now()
print "Not lazy generation: %f" % (after1-before1)
print "Lazy generation: %f" % (after2-before2)
print "Not lazy output: %f" % (after3-before3)
print "Lazy output: %f" % (after4-before4)
其他人已经给出了所有重要原因,但是我认为帮助理解懒惰为何重要的一个有用练习是尝试使用严格的语言编写定点函数。
在Haskell中,定点函数非常简单:
fix f = f (fix f)
这扩展到
f (f (f ....
但是因为Haskell很懒,所以无限的计算链是没有问题的。评估是“从内到外”完成的,一切工作都很棒:
fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)
重要的是,fix
懒惰并不重要,而是f
懒惰。一旦有了严格的要求f
,您就可以把手举起来放弃,也可以将其展开并弄乱东西。(这很像Noah所说的,它是严格/惰性的库,而不是语言)。
现在想象一下在严格的Scala中编写相同的函数:
def fix[A](f: A => A): A = f(fix(f))
val fact = fix[Int=>Int] { f => n =>
if (n == 0) 1
else n*f(n-1)
}
您当然会得到堆栈溢出。如果您希望它起作用,则需要根据需要f
调用参数:
def fix[A](f: (=>A) => A): A = f(fix(f))
def fact1(f: =>Int=>Int) = (n: Int) =>
if (n == 0) 1
else n*f(n-1)
val fact = fix(fact1)
我不知道您目前如何看待事物,但是我发现将惰性评估视为库问题而不是语言功能很有用。
我的意思是,在严格的语言中,我可以通过构建一些数据结构来实现惰性评估,而在惰性语言中(至少是Haskell),我可以在需要时要求严格性。因此,语言的选择并不会真正使您的程序变得懒惰或非懒惰,而只会影响默认情况下获得的代码。
一旦想到了这一点,便想到所有编写数据结构的地方,以后可以用来生成数据(在此之前不必过多看),您会发现懒惰有很多用途。评价。
我使用过的对惰性求值的最有用的利用是按特定顺序调用一系列子功能的功能。如果这些子功能中的任何一个失败(返回false),则调用函数需要立即返回。所以我可以这样来做:
bool Function(void) {
if (!SubFunction1())
return false;
if (!SubFunction2())
return false;
if (!SubFunction3())
return false;
(etc)
return true;
}
或者,更优雅的解决方案:
bool Function(void) {
if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
return false;
return true;
}
一旦开始使用它,您将发现越来越多地使用它的机会。
没有懒惰的评估,您将无法编写以下内容:
if( obj != null && obj.Value == correctValue )
{
// do smth
}
懒惰的评估是穷人的方程式推理(理想情况下,可以从涉及的类型和运算的属性中推断出代码的属性)。
效果很好的示例: sum . take 10 $ [1..10000000000]
。我们不介意将其减少为10个数字的总和,而不仅仅是一个直接和简单的数字计算。当然,如果没有惰性评估,这将在内存中创建一个巨大的列表,仅使用前10个元素。这肯定会非常慢,并且可能会导致内存不足错误。
示例不如我们期望的那么好:sum . take 1000000 . drop 500 $ cycle [1..20]
。即使是循环而不是列表,它实际上将求和1 000 000个数字;仍然应该简化为一个直接的数值计算,几乎没有条件和公式。这将是好了很多,然后总结1 000 000号。即使在循环中,也不在列表中(例如,森林砍伐优化后)
另一件事是,它使得可以使用尾部递归模态cons样式进行编码,并且可以正常工作。
cf. 相关答案。
如果用“惰性评估”来表示,例如在组合布尔值中,例如在
if (ConditionA && ConditionB) ...
那么答案很简单,就是程序消耗的CPU周期越少,它运行的速度就越快...并且,如果一条处理指令的块对程序的结果没有影响,那么这是不必要的,(因此浪费了)时间)来执行它们...
如果是otoh,则表示我所谓的“惰性初始化程序”,例如:
class Employee
{
private int supervisorId;
private Employee supervisor;
public Employee(int employeeId)
{
// code to call database and fetch employee record, and
// populate all private data fields, EXCEPT supervisor
}
public Employee Supervisor
{
get
{
return supervisor?? (supervisor = new Employee(supervisorId));
}
}
}
好的,这项技术允许使用类的客户端代码避免为Supervisor数据记录调用数据库,除非使用Employee对象的客户端需要访问主管的数据...这使得实例化Employee的过程更快,但是当您需要Supervisor时,对Supervisor属性的第一次调用将触发Database调用,并且数据将被提取并可用。
摘自高阶函数
让我们找到100,000以下可被3829整除的最大数字。为此,我们将过滤出我们知道解决方案所在的一组可能性。
largestDivisible :: (Integral a) => a
largestDivisible = head (filter p [100000,99999..])
where p x = x `mod` 3829 == 0
我们首先列出所有小于100,000的数字,然后递减。然后,我们通过谓词对其进行过滤,并且由于数字以降序排序,因此满足谓词的最大数字就是过滤列表的第一个元素。我们甚至不需要为开始集使用有限列表。这又是懒惰。因为我们只使用过滤列表的开头,所以过滤列表是有限的还是无限的都没有关系。找到第一个适当的解决方案后,评估将停止。