为什么惰性评估有用?


Answers:


96

主要是因为它可以提高效率-如果不使用值,则无需计算值。例如,我可以将三个值传递给一个函数,但是根据条件表达式的顺序,实际上只能使用一个子集。在像C这样的语言中,无论如何都要计算所有三个值。但是在Haskell中,仅计算必要的值。

它还允许诸如无限列表之类的很酷的东西。我不能像C这样的语言拥有无限的列表,但是在Haskell中,这没问题。无限列表经常在某些数学领域中使用,因此具有操纵它们的功能可能会很有用。


6
Python通过迭代器懒惰地评估了无限列表
Mark Cidade

4
您实际上可以使用生成器和生成器表达式(类似于列表理解的方式)在Python中模拟无限列表:python.org/doc/2.5.2/ref/genexpr.html
John Montgomery,

24
生成器使懒惰列表在Python中变得容易,但是其他懒惰评估技术和数据结构明显不那么优雅。
伯恩斯伯恩斯

3
恐怕我不同意这个答案。我曾经以为懒惰是关于效率的,但是在大量使用Haskell之后,再切换到Scala并比较经验,我不得不说懒惰通常很重要,但由于效率而很少。我认为Edward Kmett是真正的原因。
欧文(Owen)

3
我同样不同意,尽管由于严格的评估,C中没有明确的无限列表概念,但是您可以通过使用thunk并传递函数来轻松地在任何其他语言中(甚至在大多数惰性语言的实际实现中)发挥相同的作用。指针使用由相似表达式产生的无限结构的有限前缀的指针。
Kristopher Micinski 2012年

71

惰性评估的一个有用示例是使用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])。由于采用了惰性计算,因此其余部分永远不会被排序,从而节省了大量计算时间。

这当然是一个非常简单的示例,但是对于大型程序,懒惰的工作方式相同。

但是,所有这些都有一个缺点:很难预测程序的运行速度和内存使用情况。这并不意味着惰性程序会变慢或占用更多内存,但是很高兴知道。


19
更一般而言,take k $ quicksort list仅需O(n + k log k)时间,其中n = length list。对于非延迟比较排序,这将始终花费O(n log n)时间。
短暂

@ephemient你不是说O(nk log k)吗?
MaiaVictor

1
@Viclib不,我的意思是我所说的。
ephemient

@ephemient然后我想我不明白,可惜
MaiaVictor 2013年

2
@Viclib一种用于从n中找到前k个元素的选择算法是O(n + k log k)。当您以惰性语言实现quicksort时,仅对其评估足够远以确定前k个元素(在此之后停止评估),它与非惰性选择算法将进行完全相同的比较。
2013年

70

我发现惰性评估对许多事情很有用。

首先,所有现有的惰性语言都是纯净的,因为很难推理惰性语言的副作用。

纯语言使您可以使用方程式推理对函数定义进行推理。

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法则,相比之下,工作流程会带来很多严格的限制。)


5
非常好; 这些才是真正的答案。以前,我一直认为这与效率有关(稍后延迟计算),直到我大量使用Haskell并发现这根本不是原因。
欧文,

11
同样,尽管从技术上讲惰性语言不一定是纯语言(例如R),但不纯真的惰性语言却可以做很奇怪的事情(例如R)。
欧文(Owen)

4
当然可以。在严格的语言中,递归let是一种危险的野兽,在R6RS方案中,#f只要严格按照一个循环进行,它就可以在您的任期中出现random !没有双关语的意图,但是严格地说let,在懒惰的语言中,更递归的绑定是明智的。严格性也加剧了这样一个事实,即where除了SCC之外,根本没有办法对相对效果进行排序,这是语句级的构造,其效果可以严格按照任何顺序发生,即使您使用的是纯语言,您也会感到厌恶。#f问题。严格where将代码与非本地问题混为一谈。
爱德华·KMETT

2
您能解释一下懒惰如何避免价值限制吗?我还无法理解这一点。
汤姆·埃利斯

3
@PaulBone您在说什么?懒惰与控制结构有很多关系。如果使用严格的语言定义自己的控制结构,则要么必须使用一堆lambda或类似的东西,否则它会很烂。因为ifFunc(True, x, y)将同时评估xy而不是x
分号

28

正常订单评估与懒惰评估(在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表示法中)。


25

与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)))个步骤。如果没有懒惰的评估来获得最佳效率,则必须为每个上下文实现算法。


与GC的类比对我有所帮助,但是请您举一个“删除一些样板代码”的示例吗?
阿卜杜勒

1
@Abdul,任何ORM用户都熟悉的示例:延迟关联加载。它“及时”从数据库加载关联,同时使开发人员无需明确指定何时加载以及如何缓存它(这是我的意思)。这是另一个示例:projectlombok.org/features/GetterLazy.html
Alexey

25

如果您相信Simon Peyton Jones,那么懒惰的评估本身并不重要而只是作为迫使设计师保持语言纯正的“上衣”。我发现自己对此观点表示同情。

理查德·伯德(Richard Bird),约翰·休斯(John Hughes)以及较小的拉尔夫·欣泽(Ralf Hinze)能够通过懒惰的评估做出令人惊奇的事情。阅读他们的作品将帮助您欣赏它。很好的起点是Bird出色的Sudoku求解器和Hughes的论文《为什么函数编程很重要》


它不只是迫使他们保持语言纯洁,也只是他们这样做的时候(以前以引进的IO单子)的签名mainString -> String,你可能已经编写正确的互动节目。
左右左转

@leftaroundabout:是什么阻止了严格的语言将所有效果强制转换为IOmonad?
汤姆·埃利斯

13

考虑一个井字游戏程序。它具有四个功能:

  • 一种移动生成功能,它采用当前板并生成一个新板列表,每个板都应用了一个移动。
  • 然后是一个“移动树”功能,该功能应用了移动生成功能来推导从该位置可能产生的所有可能的板位置。
  • 有一个minimax函数可以使树(或可能仅是树的一部分)走动,以找到最佳的下一步动作。
  • 有一个董事会评估功能,可确定其中一名玩家是否获胜。

这样可以很好地将关注点分离。特别是,移动生成功能和棋盘评估功能是唯一需要了解游戏规则的功能:移动树和minimax功能是完全可重用的。

现在让我们尝试实施国际象棋而不是井字游戏。在“急切”(即常规)语言中,这将不起作用,因为移动树无法容纳在内存中。因此,现在必须将电路板评估和移动生成功能与移动树和minimax逻辑混合在一起,因为必须使用minimax逻辑来确定要生成的移动。我们漂亮的干净模块化结构消失了。

但是,在惰性语言中,仅根据minimax函数的要求生成移动树的元素:在让minimax放到顶部元素之前,不需要生成整个移动树。因此,我们干净的模块化结构仍然可以在真实游戏中使用。


1
[在“急切”(即常规)语言中,这是行不通的,因为移动树无法容纳在内存中。)-对于井字游戏,它肯定会。最多可以存储3 ** 9 = 19683个头寸。如果我们将每一个存储在一个多余的50字节中,那几乎是1兆字节。没什么...
乔纳斯·科尔克(JonasKölker)2009年

6
是的,那是我的意思。渴望的语言可以为琐碎的游戏提供清晰的结构,但必须针对任何实际的东西妥协该结构。惰性语言没有这个问题。
Paul Johnson

3
公平地说,惰性评估可能会导致其自身的内存问题。人们经常问为什么haskell会为某种内存消耗内存,而这种内存在急切的评估中会消耗O(1)
RHSeeger,2009年

@PaulJohnson如果您评估所有职位,则无论是急切还是懒惰地评估都没有关系。必须完成相同的工作。如果您停在中间并只评估一半的位置,那么这也没有任何区别,因为在两种情况下都必须完成一半的工作。两次评估之间的唯一区别是,如果懒惰地编写,该算法看起来更好。
ceving

12

我认为在讨论中还没有提出另外两点。

  1. 惰性是并发环境中的一种同步机制。这是一种创建对某些计算的引用并在许多线程之间共享其结果的轻巧简便的方法。如果多个线程尝试访问未评估的值,则只有一个线程将执行该值,而其他线程将相应地阻塞,并在该值可用时接收该值。

  2. 惰性是在纯设置中摊销数据结构的基础。Okasaki在“ 纯功能数据结构”中对此进行了详细描述,但是基本思想是,惰性评估是一种受控的突变形式,对于允许我们有效地实现某些类型的数据结构至关重要。虽然我们经常说懒惰迫使我们穿上纯正的衬衫,但另一种方法也适用:它们是一对协同的语言功能。


10

当您打开计算机的电源时,Windows拒绝在Windows资源管理器中打开硬盘驱动器上的每个目录,而拒绝启动计算机上安装的每个程序,直到您指出需要某个目录或需要某个程序为止,是“懒惰”的评价。

“懒惰”评估是在需要和需要时执行操作。当它是编程语言或库的功能时,它很有用,因为通常比单独预先计算所有内容更难于自己实现惰性评估。


1
有人可能会说这确实是“懒惰的执行”。除了在像Haskell这样的纯净语言中,区别实际上并不重要。但不同之处在于,不仅延迟了计算,而且还带来了与之相关的副作用(例如打开和读取文件)。
欧文(Owen)

8

考虑一下:

if (conditionOne && conditionTwo) {
  doSomething();
}

仅当conditionOne为true conditionTwo为true时,才会执行doSomething()方法。在conditionOne为false的情况下,为什么需要计算conditionTwo的结果?在这种情况下,对conditionTwo的评估会浪费时间,特别是如果您的条件是某些方法过程的结果时。

那是懒惰的评估兴趣的一个例子...


我认为那是短路,而不是懒惰的评估。
Thomas Owens

2
这是一个懒惰的评估,因为仅当确实需要conditionTwo时(即conditionOne为true时)才进行计算。
罗曼·林索拉斯

7
我认为短路是懒惰评估的一种退化情况,但绝对不是考虑它的常用方法。
rmeador

19
实际上,短路是延迟评估的一种特殊情况。显然,懒惰评估不仅包括短路。或者,短路有什么超越懒惰的评价呢?
yfeldblum

2
@朱丽叶:你对“懒惰”有一个很强的定义。您的带有两个参数的函数示例与短路if语句不同。短路if语句可避免不必要的计算。我认为与您的示例更好地比较的是Visual Basic的运算符“ andalso”,它强制对两个条件都进行评估

8
  1. 它可以提高效率。这是显而易见的,但实际上并不是最重要的。(另请注意,懒惰可以杀死效率太-这个事实是不会立即明显然而,通过存储了大量的临时结果,而不是立即计算它们,就可以使用了一个巨大的RAM量。)

  2. 它使您可以使用普通的用户级代码定义流控制结构,而不是将其硬编码为该语言。(例如,Java具有for循环; Haskell具有for功能。Java具有异常处理; Haskell具有各种类型的异常monad。C#具有goto; Haskell具有延续monad ...)

  3. 它使您可以将用于生成数据的算法与用于确定要生成多少数据的算法分离。您可以编写一个函数,该函数生成一个概念上无限的结果列表,而另一个函数则可以处理它决定所需数量的列表。更重要的是,您可以拥有五个生成器函数和五个使用者函数,并且可以有效地产生任何组合-而不是手动编码同时组合两个动作的5 x 5 = 25个函数。(!)我们都知道去耦是一件好事。

  4. 它或多或少迫使您设计一种函数式语言。它总是诱人走捷径,但在一个慵懒的语言,丝毫杂质,使你的代码似地不可预知的,强烈有碍于走捷径。


6

懒惰的一个巨大好处是能够以合理的摊销范围编写不可变的数据结构。一个简单的示例是不可变堆栈(使用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明显不同。


5

此摘要显示了惰性评估和非惰性评估之间的区别。当然,这个斐波那契函数本身可以被优化,并且可以使用惰性求值而不是递归求值,但这会破坏示例。

假设我们可以必须对某些东西使用前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)

5

延迟评估对数据结构最有用。您可以定义一个数组或向量,以归纳方式仅指定结构中的某些点,并就整个数组表示所有其他点。这使您可以非常简洁地生成数据结构,并具有较高的运行时性能。

为了了解这一点,您可以看一下我的名为instinct的神经网络库。它大量使用了惰性评估来获得优雅和高性能。例如,我完全摆脱了传统的命令式激活计算。一个简单的懒惰表达式可以为我做所有事情。

例如,它用于激活函数以及反向传播学习算法(我只能发布两个链接,因此您需要自己learnPatAI.Instinct.Train.Delta模块中查找该函数)。传统上,两者都需要复杂得多的迭代算法。


4

其他人已经给出了所有重要原因,但是我认为帮助理解懒惰为何重要的一个有用练习是尝试使用严格的语言编写定点函数。

在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)

3

我不知道您目前如何看待事物,但是我发现将惰性评估视为库问题而不是语言功能很有用。

我的意思是,在严格的语言中,我可以通过构建一些数据结构来实现惰性评估,而在惰性语言中(至少是Haskell),我可以在需要时要求严格性。因此,语言的选择并不会真正使您的程序变得懒惰或非懒惰,而只会影响默认情况下获得的代码。

一旦想到了这一点,便想到所有编写数据结构的地方,以后可以用来生成数据(在此之前不必过多看),您会发现懒惰有很多用途。评价。


1
用严格的语言实现懒惰评估通常是图灵塔比(Turing Tarpit)。
itsbruce 2012年

2

我使用过的对惰性求值的最有用的利用是按特定顺序调用一系列子功能的功能。如果这些子功能中的任何一个失败(返回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;
}

一旦开始使用它,您将发现越来越多地使用它的机会。


2

没有懒惰的评估,您将无法编写以下内容:

  if( obj != null  &&  obj.Value == correctValue )
  {
    // do smth
  }

好吧,imo,这样做不是一个好主意。尽管此代码可能是正确的(取决于您要实现的目标),但很难阅读,这始终是一件坏事。
布兰恩

12
我不这么认为。它是C语言及其亲属的标准结构。
Paul Johnson

这是短路评估而不是惰性评估的示例。还是那实际上是一回事?
RufusVS

2

除其他外,惰性语言允许多维无限数据结构。

虽然scheme,python等允许带有流的一维无限数据结构,但您只能沿一维遍历。

惰性对于相同的边缘问题很有用,但值得注意的是该链接中提到的协程连接。


2

懒惰的评估是穷人的方程式推理(理想情况下,可以从涉及的类型和运算的属性中推断出代码的属性)。

效果很好的示例: sum . take 10 $ [1..10000000000]。我们不介意将其减少为10个数字的总和,而不仅仅是一个直接和简单的数字计算。当然,如果没有惰性评估,这将在内存中创建一个巨大的列表,仅使用前10个元素。这肯定会非常慢,并且可能会导致内存不足错误。

示例不如我们期望的那么好:sum . take 1000000 . drop 500 $ cycle [1..20]。即使是循环而不是列表,它实际上将求和1 000 000个数字;仍然应该简化为一个直接的数值计算,几乎没有条件和公式。这是好了很多,然后总结1 000 000号。即使在循环中,也不在列表中(例如,森林砍伐优化后)


另一件事是,它使得可以使用尾部递归模态cons样式进行编码,并且可以正常工作

cf. 相关答案


1

如果用“惰性评估”来表示,例如在组合布尔值中,例如在

   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调用,并且数据将被提取并可用。


0

摘自高阶函数

让我们找到100,000以下可被3829整除的最大数字。为此,我们将过滤出我们知道解决方案所在的一组可能性。

largestDivisible :: (Integral a) => a  
largestDivisible = head (filter p [100000,99999..])  
    where p x = x `mod` 3829 == 0 

我们首先列出所有小于100,000的数字,然后递减。然后,我们通过谓词对其进行过滤,并且由于数字以降序排序,因此满足谓词的最大数字就是过滤列表的第一个元素。我们甚至不需要为开始集使用有限列表。这又是懒惰。因为我们只使用过滤列表的开头,所以过滤列表是有限的还是无限的都没有关系。找到第一个适当的解决方案后,评估将停止。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.