似乎有很多聪明的事情是用延迟评估的语言完成的,而在严格评估的环境中是无法完成的。例如 Haskell 中的无限列表或在一次 pass 中用树的最小值替换树中的每个元素。
有没有用严格评估的语言完成的聪明事情不能用懒惰评估的语言轻松完成的例子?
似乎有很多聪明的事情是用延迟评估的语言完成的,而在严格评估的环境中是无法完成的。例如 Haskell 中的无限列表或在一次 pass 中用树的最小值替换树中的每个元素。
有没有用严格评估的语言完成的聪明事情不能用懒惰评估的语言轻松完成的例子?
您可以用急切(严格)的语言而不是惰性语言轻松完成的主要事情:
从源代码预测程序的时间和空间成本
允许副作用,包括可变数组的恒定时间更新,这使得更容易快速实现一些算法
在我看来,Eager 语言的主要好处是它更容易让你的代码按照你想要的方式执行,并且很少有性能陷阱,其中代码的微小变化会导致性能的巨大变化。
话虽如此,总的来说我更喜欢用 Haskell 写复杂的东西。
不; 有些事情你可以用惰性求值(AKA 正常顺序归约,或最左外归约)做,而严格求值是无法做到的,反之则不行。
原因是惰性求值在某种程度上是“最通用”的求值方式,它被称为:
计算充分性定理:如果某些求值顺序终止并产生特定结果,则惰性求值也将终止并产生相同结果。
*(请注意,我们在这里不是在谈论图灵等价性)
好吧,不,根据定义或多或少。在惰性评估语言中,根据定义,您应该得到与剧烈评估相同的结果(人们现在真的在使用“严格”吗?)评估,除了延迟评估直到需要,存储影响等等。所以如果你能得到一些不同的行为,那将是一个错误。
惰性在日常语言中最明显的用法是“if”语句,其中只执行条件的一个分支。
纯粹非严格(惰性)语言的对立面是纯粹严格的语言。
至少有一种情况是“纯严格”是有益的,即分支谓词。
链接文章的粗略解释:
很久以前,在 CPU 的世界中,执行指令是在测试分支条件时加载的。在某些时候,添加了指令流水线以减少加载时间。缺点是 CPU 不知道它需要加载哪个分支,因此默认情况下它会加载一个。如果分支走另一条路,则管道将在加载另一个分支的代码时停止。
解决方案是加载两个分支,执行两个分支,然后条件的结果告诉您保留哪个分支结果,以及丢弃哪个分支结果。然后你就不会遇到管道停顿了。
这是我最喜欢的(唯一的?)例子,说明了纯严格语言的好处。
'Hello, World' 程序浮现在脑海,或者基本上与副作用有关的所有事情。
在严格的评估中,评估表达式很容易产生副作用,因为您对评估顺序有清晰的了解,因此副作用的顺序通常在副作用中很重要。这是严格评估的基本优势,也是大多数语言拥有它的原因。以及为什么甚至像 C 这样的面向性能的语言也经常使用值传递模型。
两者都可以做同样的事情,只是人类难度不同,你可以用严格的语言很好地模拟无限列表,你可以用非严格的语言模拟副作用的所有效果。
正如查理马丁 所写,严格和懒惰程序的结果应该是等价的。差异在于时间和空间限制和/或语言表达能力。除了对不需要的值的惰性的性能影响之外,在惰性语言中可以轻松引入新的语言结构而无需额外的语言概念(例如 LISP 中的宏)。无论如何,懒惰会咬你
Haskell 尾递归是如何工作的?同样的想法可能比严格的语言更复杂。(haskell 编译器不应该认识到计算+1
比 make thunk 便宜( x + 1 )
吗?)
在 C# 等严格的求值语言中,可以通过将 thunk 返回到值 (Func) 而不是值本身来实现惰性求值。作为在 c# 中构建 Y-Combinator 的示例,可以通过以下方式完成:
public Func<int, int> Y(Func<Func<int, int>, Func<int, int>> f)
{
return x => f(Y(f))(x);
}
这个声明在惰性环境中会更简洁:
Y f = f (Y f)
我用 Erlang 编程了一下,发现我在大学学到的缺乏惰性评估非常令人沮丧。
我一直在简要地研究一些欧拉问题,特别是那些处理素数的问题。
通过惰性求值,您可以拥有一个返回所有素数列表的函数,但它实际上只返回您真正想要的那些。因此,说“给我前n 个素数”真的很容易。
如果没有惰性求值,您最终会得到一个更具约束性的“给我一个 1 到n之间所有素数的列表”。
不幸的是,被评为最佳答案的答案存在逻辑错误。从 Porges 引用的定理来看,并不能得出一个人可以用一种懒惰的语言做更多的事情。
相反的证明是,惰性语言中的所有程序都被编译为严格语言中的等效程序(进一步编译为汇编程序)或由用严格语言编写的解释器执行(是的,解释器最终是汇编程序)。