Har*_*aye 7 evaluation haskell lazy-evaluation java-stream
因此,这种惰性求值的概念被广泛使用,尤其是在阅读函数式编程、Java 流等内容时。
流是惰性的;仅在启动终端操作时才执行对源数据的计算,并且仅在需要时消耗源元素。
哈斯克尔很懒。这意味着除非另有明确说明,Haskell 不会执行函数并计算事物,直到它真正被迫向您显示结果为止。
现在我的理解是,如果我有一个数据列表,我希望对其执行 N 次操作,那么惰性求值只会对整个列表进行 1 次传递,而不是 N 次。为什么这如此理想?在我看来,对单个列表进行 N 次传递所导致的操作次数与对列表进行 1 次传递但对列表中包含的每个元素执行 N 次操作的操作次数相同。
我的问题是:
有人可以以与语言无关的方式回答这个问题吗,因为我对这个概念而不是特定的语言更好奇。
Ben*_*Ben 12
在某种程度上,这是一个你可以写一本书的主题,但我认为我们可以给出一个 StackOverflow 大小的概述。
从技术上讲,严格与非严格和渴望与惰性是谈论不同事物的两种不同区别。从技术上讲,严格性是程序语义的一个属性,当我们谈论不存在实际计算机、RAM、评估等的级别时使用。而惰性评估是实际评估程序的一种策略,而急切性则相反战略。
然而,通常人们使用惰性求值(在整个语言的级别)来实现非严格语义,如果想要严格的语义,则使用急切求值。因此,在不太正式的情况下,惰性和非严格经常可以互换使用,同样,渴望和严格也经常互换使用。
这绝对不总是好的。通常认为惰性求值比急切求值的性能更差;它通常涉及分配“记住”操作以供以后使用的内存结构,如果您无论如何都肯定要执行该操作,那么这比仅仅执行该操作要慢。
与急切地执行完全相同的操作相比,天真地懒惰地执行所有操作通常会增加恒定的开销。常数因子大多足够小,不会产生巨大的差异。但是,如果操作非常小并且只会产生立即值(例如机器整数,而不是堆分配的对象),那么惰性的开销仍然只是一个常数因子,但该常数因子相对于运营的“内在”成本;如果你的程序主要做这种事情,那么惰性求值确实会产生显着的负面影响。
惰性求值还使得很容易丢失各种操作执行的确切顺序。事情不是按照您编写代码的顺序完成的,而是按照操作之间的数据依赖关系确定的顺序完成的;事情只有在需要其结果时才会执行。通常,这种“需求”是由非常非本地的代码决定的。
在纯函数代码中,这通常并不重要,因为此类代码的结果完全由您编写的代码决定,而不管各种事物的执行顺序如何。在计算机科学理论中,分析简单的纯 lambda 演算,有一个严格的数学证明,即如果程序的任何求值顺序都可以产生明确定义的结果,那么惰性求值就会产生该结果;急切求值可能会遇到惰性求值可以避免的错误或无限循环。这意味着您不是一个纯粹的函数式程序员,实际上不必非常关心事物将以什么顺序执行。无论他们脑子里有什么执行顺序,如果它产生了明确定义的结果,那么实际的惰性求值将产生相同的结果,即使他们头脑中的执行顺序与实际的惰性求值不同。(当然,这是假设该语言忠实地传递了简单 lambda 演算所证明的属性)
在具有副作用的代码中,失去对操作执行顺序的跟踪对于程序员来说是一场噩梦。它很容易犯一些难以调试的错误。如果将执行两段代码并且都更改共享变量,则您需要能够轻松准确地预测它们将运行的顺序,以便了解变量的最终状态。因此,编写不纯代码的程序员需要对编译器/解释器的行为有相当彻底的操作理解。因此,在允许未跟踪副作用的语言中,您基本上永远不会看到“默认情况下所有操作都是惰性的”;如果这些语言直接支持惰性求值,那么它们通常要求程序员显式地选择对其部分代码进行惰性求值,并相信程序员只在安全的地方(即他们编写了纯代码的地方,即使该语言不会强制执行这一点)。
我现在听起来好像懒惰的评估总是不好的。但有一些重要的警告。有时,惰性求值可以提高性能,或者允许算法正常工作。
通常,这是当计算遍历非常大的数据集时;延迟计算的代码可能能够处理整个数据集,而无需将其全部立即驻留在内存中;这会对性能产生巨大的影响。但有时惰性求值也只是按照对 CPU 缓存、垃圾收集器等更有利的顺序执行操作,即使对相同代码的急切求值不会使用更多的内存。
惰性求值通常还可以实现更多的解耦代码。生成数据结构的代码可以用简单的直接风格编写,以生成“全部”数据结构,即使是无限的。使用数据结构的代码只是简单地检查它想要的结构,并且通过检查它将使生产者实际运行“刚好足够”来生成所需的数据。因此,无论生产者如何确定,所生成的数据结构的数量都可以完全满足消费者的需求,而生产者根本不知道消费者的存在。
在急切的评估下,任何数据结构都必须完整地生成,然后消费者才能查看其中的任何一个。如果这是不可取的(因为结构非常大或需要很长时间才能完成),那么我们需要一种方法让生产者只生产部分结构。这通常涉及额外的参数来控制产生的数量,可能会涉及数据结构的额外复杂性,以允许消费者区分“这是我们迄今为止生成的”和“这是数据真正结束的地方” “,可能需要生产者能够从之前的部分结果恢复生产,等等。这很容易给实现相当简单的想法的代码增加很多复杂性,而额外的复杂性通常最终会使生产者与消费者之间的耦合度大大增加比懒惰的生产者和消费者需要的更紧密。
之前的讨论可能有点抽象。举个例子,考虑一个生成用于分析国际象棋等游戏的移动树的程序。一个懒惰的生产者可以只返回一棵树,其中包含每个可能位置的每个可能的动作,而不知道任何人想用它做什么。它可能会生成一个结构Move,player其中startingSquare包含endingSquare描述移动本身的字段 、 ,以及另一个字段,该字段只是在此字段之后可能发生followOnMoves的所有可能发生的列表;当然,每个s 都会再次包含可能的后续动作的另一个列表,依此类推直至无穷大。MoveMove
如果这是由惰性函数生成的,那么消费者可以直接探索这棵树,而不必知道它是如何生成的。当消费者开始运行时,这些字段中的每一个(但最重要的是followOnMoves)实际上并不存在,它们只会包含对需要运行来填充它们的代码的惰性引用(如果消费者确实想要查看它们)。因此,如果消费者正在执行诸如极小极大修剪之类的操作,那么生产者将自动地永远不会浪费时间来生产消费者不决定查看的树部分。可以存在多个不同的消费者,它们使用相同的数据结构执行不同的操作,从而导致相同的单个生产者代码自动生成树的不同部分。人类用户甚至可以交互式地确定需要树的哪些部分!生产者和消费者的实现可以彼此非常独立;基本上他们共享的只是简单数据类型的定义Move。
急切的生产者根本无法返回Move这样的树,因为它本质上是无限的(我认为在某些竞争规则下,国际象棋技术上不是无限的,因为一个位置可以重复的次数有限制,但整个树仍然是不切实际的巨大)。要么它必须返回移动树的一小部分(这意味着它需要知道哪些部分对消费者有用,本质上是将消费者逻辑嵌入到生产者中),要么它必须公开仅执行单个操作的各种函数-steps,消费者现在负责在需要更多数据时调用这些单步函数(本质上是将生产者逻辑嵌入到消费者中)。
无论哪种方式,双方可能都必须更多地了解对方的实施情况,以便在需要时就生成数据的策略进行合作。您可以为这个问题设计良好的解决方案,同时仍然使热切的生产者和热切的消费者合理地解耦,但是设计一个足够灵活以供所有用途同时仍然具有高性能的良好界面可能是一个棘手的问题,并且可能会发生很多这样的情况当您的代码被延迟计算时,这根本不是您需要考虑的问题。
这部分我真的觉得我不能很好地总结。
基本的大 O 复杂性分析仍然有效,如果计算不是从根本上使用惰性,甚至不会发生太大变化。如果执行的操作无论如何都是完全相同的,只是顺序不同,那么您可以执行与严格评估代码时相同的大 O 分析。(当然,Big-O 复杂性并没有考虑缓存局部性、thunk 的额外内存或内存不足等影响)
当算法更根本地依赖于惰性(并且如果不需要的话根本不执行事情),那么这当然是行不通的。但我认为我无法在这里公正地阐述这个主题,就像我无法在一篇文章中解释“如何分析急切算法的性能”一样。
这个范围太宽泛了。您如何回答“急切评估的一些典型用例是什么?” 两者的答案实际上都是“一般编程的所有典型用例”。所有任务都可以由两者来实现,但是当您使用急切或惰性求值时,有些事情的完成方式会有所不同;你会选择不同的算法来实现任务。
然而,正如我上面提到的,我可以说的一件事是,当急切算法需要更多代码来显式管理非常大的数据集何时以及有多少内存在内存中时,惰性求值尤其符合人体工程学。一次。
惰性求值对于任何语言中的许多控制结构都至关重要。例如,如果在开始执行条件选择逻辑之前始终评估和部分if/then/else,则不会很有用。因此,几乎每种语言都为语法的一些特定部分内置了这种非常有限的“惰性”。但在一切都是惰性的语言中,您可以创建自己的控制结构。在 Haskell 中,类似于while 循环和for-each 循环的东西可以简单地实现为普通的库代码,不需要编译器专门实现它们。因此,与急切的评估相比,这是另一个突出的“典型用例”。thenelse
如果您有一流的函数(或可以模拟它们的其他功能),那么您始终可以模拟惰性求值。您可以显式存储稍后生成值的函数并显式调用,而不是依赖运行时系统隐式创建 thunk(这就是我们所说的稍后将在需要时运行的操作的内存中记录)当需要时。需要更多的技巧来确保这样的函数只运行一次以产生值,无论有多少引用 - 但这也是可以做到的。有些语言甚至有足够的灵活性将所有这些包装在一个界面中,使您看起来就像正常使用值一样,将 thunk 函数隐藏在幕后。
默认情况下惰性计算的语言通常还允许程序员显式地使某些事情急切。旨在获得良好性能的惰性语言通常还会有一个优化编译器,旨在检测操作何时无法从惰性中受益并立即执行它。例如,Haskell 默认向您承诺非严格语义,我们通常认为它是使用惰性求值来实现这一点,但它实际上做了很多优化,并且会急切地求值大量代码;它只是承诺不会在可能改变代码结果的地方这样做,并尝试在会使代码变慢的地方不这样做。
因此,无论您使用默认的惰性语言还是默认的急切语言,您都可以选择其他评估策略(尽管需要付出不同的努力)。
\n\n现在我的理解是,如果我有一个数据列表,我希望对其执行 N 次操作,则惰性求值只会对整个列表进行 1 次传递,而不是 N 次。
\n
我想在某些特定情况下您可以这样看待它,但这绝对不是一般情况下惰性求值的良好表征。这里似乎存在一些误解:
\n\n\n我有一个数据列表
\n
如果您已经有一个数据列表,例如从文件中读取数据,那么惰性语言和严格语言之间实际上没有任何区别。在这两种情况下,列表只会存在于内存中,无论您对其进行了多少次遍历。\xe2\x80\xa0
\n\n\n惰性求值只会对整个列表进行 1 次传递
\n
一般来说绝对不是真的。如果将两个不同的函数映射到一个列表上,那么通常需要对列表进行两次单独的传递。原则上,编译器可以重新排序,将两个传递融合为一个传递,实际上 GHC 有时会做这种事情,但这实际上与惰性求值没有太大关系。
\n事实是,如果您l'通过将一个函数映射到现有列表上来定义一个新列表,则N 次访问l'将只需要一次映射操作。但这又与严格语言中的完全相同。唯一的区别是,在严格的语言中,传递将在您编写的地方发生map,而在惰性语言中,它将等到第一次需要结果时才发生。所以,
\n\n与 N 相反
\n
确实没有意义。在严格的语言中,它也只是一次传递,例如在Python中
\n l = someListOfData\n l2 = map(f, l)\nRun Code Online (Sandbox Code Playgroud)\n前提确实成立的地方是,用严格的语言,您通过使用类似的东西来明确延迟评估
\n l = someListOfData\n l2 = lambda: map(f, l)\nRun Code Online (Sandbox Code Playgroud)\n这是手动的 \xe2\x80\x9clazyness\xe2\x80\x9d,但是map当有人需要时,Python 会一遍又一遍地进行传递l2()。
\n\n惰性评估总是好的吗?如果不是,我们接受它会做出什么权衡?
\n
惰性评估是一种工具。在适当的时候使用它总是好的。对给定的代码片段进行惰性求值并不总是更好。
\n为了强烈简化对比,权衡围绕着这一点:惰性将指称语义(如果需要的话,值应该是 \xe2\x80\x93 )与操作语义(当,或者实际上如果,该值曾经是完全计算过)。在很多情况下,指称才是您真正感兴趣的,因此惰性语言非常适合专注于此。
但另一方面,计算仍然需要在真实的计算机上进行,具有真实的 CPU 时间,特别是真实的内存,当涉及懒惰时,对此进行推理和做出保证通常会变得更加困难。
本对更多方面和您的其他问题进行了精彩的讨论,所以我就到此为止。
\n\xe2\x80\xa0值得注意的是,Haskell 传统上除了延迟求值之外还执行延迟 IO,即您可以读取文件,但运行时实际上只会在需要元素时从光盘读取。然而,现在这被广泛认为是不好的,并且现代 Haskell 库不再鼓励这样做。
\n我\xe2\x80\x99将尝试以与语言无关的方式进行简要总结。
\n\n\n惰性评估总是好的吗?如果不好,我们接受它会做出什么权衡?
\n
没有\xe2\x80\x94it\xe2\x80\x99s空间\xe2\x80\x93时间权衡。
\n热切评价中中,您将整个值推入函数\xe2\x80\x99s 输入中,然后从其输出中推出整个值。
\n这不能避免产生额外的输出,因为该函数不知道您需要什么。如果你不\xe2\x80\x99t使用所有输出,这会浪费(可能大量)空间,并且还会浪费一些时间来计算稍后不需要的数据。为了避免超支,您需要将数据流转换为显式控制流(例如,生成器而不是列表)。
\n在惰性求值中,您从 function\xe2\x80\x99s 输出中提取一个子值,然后将一个子值提取到其输入中。
\n这不能避免过度保留输入(和捕获的变量),因为您不知道该函数需要什么。如果您确实使用了所有输出,那么延迟工作就是浪费空间:您可以更早开始计算它,而不是分配一个 thunk。为了避免超支,您需要将控制流转换为显式数据流(例如,在 Haskell 中,使用seq, 或各种语法糖)。
\n\n如何分析惰性算法的性能?
\n
Banker \xe2\x80\x99s 方法。Chris Okasaki 的《纯函数式数据结构》中有\xe2\x80\x99 一章详细描述了这一点。
\n在急切评估中,您会计算代码中的时间成本,因为只有在支付全部价格来计算数据结构后,您才能获得数据结构。在惰性求值中,您会计算数据结构中的时间成本:您可以立即获取数据结构,但每个延迟计算都是 \xe2\x80\x9cdebt\xe2\x80\x9d ,必须付费才能使用它。
\n\n\n惰性求值的典型用例有哪些?
\n
您可以使用普通数据类型编写漂亮的可读数据流,并获得为您提供一些增量计算和缓存所需的自动控制流。
\n它还为您提供了等式推理和引用透明度。我无法夸大这对于与同事沟通的好处。如果你编写一些代码 X,并且我可以轻松证明 X = Y,并且 Y 在某些方面更好,那么我可以绝对有信心建议 Y,即使我不\xe2\x80\x99t 知道它是如何工作的。
\n\n\n我可以用不支持立即进行惰性求值的语言来创建惰性函数吗?
\n
根据语言的不同,您可以对其进行编码,但生成的代码通常不太清晰。评估策略是语言的深层方面,它对使用该语言解决问题的方法有很大影响。
\n| 归档时间: |
|
| 查看次数: |
1651 次 |
| 最近记录: |