11

我记得看过一个演示文稿,其中 SPJ 说懒惰的评估迫使他们保持 Haskell 的纯净(或类似的东西)。我经常看到许多 Haskellers 也这么说。

所以,我想了解惰性评估策略是如何迫使他们保持 Haskell 纯净而不是严格的评估策略?

4

4 回答 4

10

与其说是懒惰的评估导致了纯度,不如说是。Haskell 从一开始就是纯粹的。相反,惰性求值迫使语言的设计者保持语言的纯净。

以下是文章A History of Haskell: Being Lazy with Class的相关段落:

一旦我们致力于一种懒惰的语言,一种纯粹的语言是不可避免的。反之亦然,但值得注意的是,实际上大多数纯编程语言也是惰性的。为什么?因为在按值调用的语言中,无论是否是函数式的,在“函数”内部允许不受限制的副作用的诱惑几乎是不可抗拒的

纯度是一个很大的赌注,具有普遍的后果。无限制的副作用无疑是非常方便的。由于没有副作用,Haskell 的输入/输出最初非常笨拙,这是一个相当尴尬的根源。必要性是发明之母,这种尴尬最终导致了 monadic I/O 的发明,我们现在将其视为 Haskell 对世界的主要贡献之一,正如我们在第 7 节中更详细讨论的那样。

纯语言(具有单子效应)最终是否是编写程序的最佳方式仍然是一个悬而未决的问题,但它无疑是对编程挑战的一种激进而优雅的攻击,正是这种力量与美的结合激发了设计师。因此,回想起来,也许懒惰的最大好处不是懒惰本身,而是懒惰让我们保持纯洁,从而激发了大量关于单子和封装状态的富有成效的工作。

(我的重点)

我还邀请您收听 18'30'' 的Software Engineering Radio 播客 #108,以了解该人本人的解释。以下是 SPJ 在Peter Seibel 的Coders at Work采访中的一段较长但相关的段落:

我现在认为懒惰的重要之处在于它让我们保持纯洁。[...]

[...] 如果您有一个惰性求值器,则更难准确预测何时将求值表达式。所以这意味着如果你想在屏幕上打印一些东西,每一种按值调用的语言,其中评估的顺序是完全明确的,通过一个不纯的“函数”来做到这一点——我在它周围加上引号,因为它现在根本不是一个函数——类型类似于字符串到单位。你调用这个函数,作为一个副作用,它会在屏幕上显示一些东西。这就是在 Lisp 中发生的事情。它也发生在 ML 中。它基本上发生在每一种按值调用的语言中。

现在在纯语言中,如果你有一个从字符串到单元的函数,你永远不需要调用它,因为你知道它只是给出了答案单元。这就是一个函数所能做的,就是给你答案。你知道答案是什么。但是当然,如​​果它有副作用,那么你确实调用它是非常重要的。在惰性语言中,问题在于如果你说“<code>f 应用于print "hello"”,那么函数调用者看不到是否f计算它的第一个参数。这与函数的内部结构有关。如果你传递两个参数,fofprint "hello"print "goodbye",那么您可以按任一顺序打印一个或两个,或者都不打印。因此,不知何故,通过惰性评估,通过副作用进行输入/输出是不可行的。你不能那样写明智、可靠、可预测的程序。所以,我们不得不忍受这一点。真的有点尴尬,因为你真的不能做任何输入/输出。所以很长一段时间以来,我们基本上都有可以将字符串转换为字符串的程序。这就是整个程序所做的。输入字符串是输入,结果字符串是输出,这就是程序真正可以做的所有事情。

通过使输出字符串对一些由外部解释器解释的输出命令进行编码,您可以变得有点聪明。所以输出字符串可能会说,“在屏幕上打印这个;把它放在磁盘上。” 口译员实际上可以做到这一点。因此,您可以想象函数式程序都是美好而纯粹的,并且有一种邪恶的解释器可以解释一串命令。但是,当然,如果您读取一个文件,您如何将输入返回到程序中?好吧,这不是问题,因为您可以输出一串由邪恶解释器解释的命令,并使用惰性求值,它可以将结果转储回程序的输入中。所以程序现在对请求流采取响应流。请求流转到对世界做事的邪恶解释器。每个请求都会生成一个响应,然后将其反馈给输入。而且由于评估是惰性的,因此程序会及时发出响应,以便它绕过循环并被用作输入。但它有点脆弱,因为如果你过于急切地消耗你的响应,那么你就会陷入某种僵局。因为你会问一个你还没有从后端吐出的问题的答案。

重点是懒惰把我们逼到了一个角落,我们不得不想办法解决这个 I/O 问题。我认为那非常重要。懒惰最重要的一点是它把我们带到了那里。

(我的重点)

于 2015-07-17T14:53:13.537 回答
8

我认为 Jubobs 的回答已经很好地总结了它(有很好的参考)。但是,用我自己的话来说,我认为 SPJ 和朋友们所指的是:

不得不经历这个“单子”业务有时真的很不方便。Stack Overflow 上的大量问题都在问“我该如何删除这个IO东西?” 这证明了有时候,你真的很想在这里打印出这个值——通常是为了弄清楚真正的 **** 发生了什么!

在一个热切的语言中,开始添加魔法不纯函数让你直接做不纯的事情是非常诱人的,就像在其他语言中一样。毫无疑问,一开始你会从小事做起,但慢慢地你会滑下这个滑坡,不知不觉中,效果无处不在。

在 Haskell 这样的惰性语言中,这种诱惑仍然存在。很多时候,能够在这里或那里快速潜入这个小效果真的很有帮助。除了因为懒惰,添加效果几乎完全没用。你无法控制什么时候发生。即使只是Debug.trace倾向于产生完全无法理解的结果。

简而言之,如果你正在设计一种惰性语言,你真的不得不想出一个连贯的故事来说明你如何处理效果。你不能只是说“嗯,我们会假装这个功能只是魔法”;如果没有更精确地控制效果的能力,您将立即陷入可怕的混乱!

TL;DR用一种热切的语言,你可以逃避作弊。在懒惰的语言中,您确实必须正确地做事,否则就无法正常工作。

就是我们聘请亚历克斯的原因——等等,错误的窗口……

于 2015-07-17T20:05:27.000 回答
2

这取决于您在这种情况下对“纯”的含义。

  • 如果你的意思是函数式的,那么@MathematicalOrchid 是正确的:通过惰性求值,你将不知道执行不纯动作的顺序,因此你根本无法编写有意义的程序,你被迫更纯粹(使用IO单子)。

    但是,在这种情况下,我发现这并不令人满意。真正的函数式语言已经将纯代码和不纯代码分开,因此即使是严格的语言也应该有某种IO.

  • 但是,该语句可能更广泛并且指的是纯粹的事实,因为您能够更轻松地以更具声明性、组合性和高效的方式表达代码。

看看这个答案,这正是你引用的陈述链接到休斯的文章为什么函数式编程很重要,这可能是你正在谈论的那个。

该论文展示了高阶函数惰性求值如何允许编写更多模块化代码。请注意,它并没有说明纯粹的功能等。它的重点是在不损失效率的情况下更加模块化和更具声明性。

文章提供了一些例子。例如 Newton-Raphson 算法:在严格的语言中,您必须将计算下一个近似值的代码和检查您是否获得足够好的解决方案的代码组合在一起。

使用惰性求值,您可以在函数中生成无限的近似值列表,并从返回第一个足够好的近似值的另一个函数调用它。


用 Haskell 进行函数式思考中,理查德·伯德正是指出了这一点。如果我们看第 2 章,练习 D:

Beaver 是一个热心的评估者,而 Susan 是一个懒惰的评估者。

[...]

Beaver 可能更喜欢什么替代方案head . filter p . map f

答案是:

[...] first p f = head . filter p . map fBeaver 可能会定义而不是定义

first :: (b -> Bool) -> (a -> b) -> [a] -> b
first p xs | null xs = error "Empty list"
           | p x = x
           | otherwise = first p f (tail xs)
           where x = f (head xs)

关键是,对于急切求值,大多数函数必须使用显式递归来定义,而不是根据有用的组件函数来定义mapfilter

这里如此纯粹意味着允许声明性、组合性和高效的定义,而使用声明性和组合定义的热切评估可能会导致不必要的低效代码。

于 2015-07-17T14:18:28.233 回答
1

严格来说,这种说法是不正确的,因为 Haskell 有unsafePerformIO,这是语言功能纯度的一个大漏洞。(它利用了 GHC Haskell 的功能纯度中的一个漏洞,该漏洞最终可以追溯到通过向语言添加严格片段来实现未装箱算术的决定)。 unsafePerformIO之所以存在,是因为大多数程序员都无法抗拒说“好吧,我将在内部使用副作用实现这一个函数”的诱惑。但是如果你看看unsafePerformIO[1] 的缺点,你就会明白人们在说什么:

  • unsafePerformIO a不保证永远执行a
  • a如果确实执行,也不能保证只执行一次。
  • 也不能保证a与程序的其他部分执行的 I/O 的相对顺序。

这些缺点unsafePerformIO大多仅限于最安全和最谨慎的使用,也是人们IO直接使用直到变得太不方便的原因。

[1] 除了类型不安全;let r :: IORef a; r = unsafePerformIO $ newIORef undefined给你一个多态 r :: IORef a的,可以用来实现unsafeCast :: a -> b。ML 有一个避免这种情况的参考分配解决方案,如果纯度不被认为是可取的,Haskell 可以用类似的方式解决它(无论如何,单态限制几乎是一个解决方案,你只需要禁止人们通过它来解决它使用类型签名,就像我在上面所做的那样)。

于 2015-07-17T15:14:08.937 回答