懒惰求值强制Haskell纯净化。
懒惰求值强制Haskell纯净化。
我记得看过一个演示文稿,其中SPJ说懒惰求值迫使他们保持Haskell的纯净(或类似的言论)。我经常听到许多Haskeller也是这么说的。
因此,我想了解懒惰求值策略如何迫使他们保持Haskell纯净,而不是使用严格求值策略?
我认为Jubobs的答案已经很好地总结了这个问题(并且有好的参考资料)。但是,我的理解是,SPJ及其朋友所指的问题是:
在某些时候,必须要经过“monad”这个过程会非常不方便。Stack Overflow上关于“怎样删除这个IO
?”的问题数量之多,证明了有时,你真的很想仅仅打印出这个值——通常是为了弄清楚究竟出了什么问题!
在一个急切的语言中,很容易诱惑人们开始添加魔法不纯的函数,让你可以直接做不纯的事情,就像其他语言一样。毫无疑问,你最开始会从一些小的事情开始,但是慢慢地你会滑向这个滑坡,之后效果就弥漫到各个地方。
在像Haskell这样的惰性语言中,这种诱惑仍然存在。很多时候,能够在这里或那里快速插入一个小效果会非常有帮助。但是,由于惰性,添加效果后几乎没有用处。你无法控制任何事情发生的时间。甚至只有Debug.trace
也往往产生无法理解的结果。
简而言之,如果你正在设计一个惰性语言,那么你真的被迫提出一个关于如何处理效果的连贯故事。不能像“啊,我们假装这个函数是神奇的”一样;没有更精准地控制效果的能力,你会立即陷入可怕的混乱中!
总之在急切的语言中,你可以作弊。但在惰性的语言中,你真的必须恰当地处理事情,否则它根本不会起作用。
“这就是为什么我们雇了Alex——等等,打错了窗口……”
懒惰求值并不是导致纯净的原因;Haskell本来就是纯净的。相反,懒惰求值迫使语言的设计者保持语言的纯净。
以下是来自文章《Haskell发展史:延时求值见证了纯净》的相关段落:
一旦我们致力于使用懒惰语言,纯净形式就不可避免。反过来则不然,但要注意的是,实践中大多数纯函数式编程语言也是延迟求值的。 为什么?
纯净性是一个具有普遍影响的大赌注。无限制的副作用无疑非常方便。由于缺乏副作用,Haskell的输入/输出最初非常笨拙,这是一件令人尴尬的事情。 由于需要创新,这种尴尬最终导致了单子I / O的发明,我们现在认为这是Haskell对世界的主要贡献之一,我们将在第7节中详细讨论。
是否使用纯净语言(带单子效应)最终是编写程序的最佳方式仍然是个开放的问题,但它确实是一个极端而优美的挑战程序设计的方法,正是这种强大和美丽的结合激励了设计者。因此,回顾过去,也许懒惰的最大单一利益并不是懒惰本身,而是懒惰使我们保持了纯粹,从而促进了单子和封装状态的许多有益工作。
我还邀请你听一下 Simon Peyton Jones 在 Software Engineering Radio podcast #108 中说的 18 分 30 秒的内容,这是关于函数式编程和 Haskell 的解释。这里还有一个更长但相关的段落,摘自 SPJ 在 Peter Seibel 的 Coders at Work 中的访谈:
“我现在认为作为懒惰求值的重要之处,是它让我们保持了纯粹性。[...] 如果你有一个懒惰求值的求值器,那么很难预测一个表达式在何时被求值。所以,如果你想在屏幕上打印些东西,那么任何一个按值调用的语言,根据求值的顺序都有一个不纯的‘函数’——我打引号是因为它根本不是一个函数——它的类型大概是 string 到 unit。你调用这个函数,它的副作用会把东西放到屏幕上。这种情况在 Lisp 中也会发生,也出现在 ML 中,出现在几乎所有按值调用的语言中。
现在在一个纯函数式编程语言中,如果你有一个从 string 到 unit 的函数,你永远不需要调用它,因为你知道它只会给你一个答案,而这个答案就是 unit。这是一个函数能够做到的全部,给你一个答案。而且你知道这个答案是什么。但是,如果它有副作用,你非常需要调用它。在一个懒惰语言中的问题是,如果你说“f 应用于 print ‘hello’”,那么 f 是不是对它的第一个参数求值并不明显对函数调用者来说。这是与函数内部有关的一些事情。如果你向它传递两个参数,f of print ‘hello’ 和 print ‘goodbye’,那么你可能会将它们中的一个或两个打印出来,或者打印它们的顺序与你期望的不同。因此,使用懒惰求值来进行输入/输出操作是不可行的。你不能用这种方式编写明智、可靠、可预测的程序。所以,我们不得不接受这一点。这有点尴尬,因为你真的不能做任何输入/输出。因此,很长一段时间内我们的程序只能将一个字符串映射到另一个字符串。这就是整个程序所能做的全部。输入字符串是输入,输出字符串是输出,这就是程序所能做的全部了。”你可以通过使输出字符串编码一些输出命令,这些命令被某个外部解释器解释,从而变得更加聪明。因此,输出字符串可能会说:“在屏幕上打印这个; 把那个放到磁盘上。” 一个解释器实际上可以执行这些命令。所以你可以想象函数式程序全都是好的,干净的,有一种邪恶的解释器解释了一串命令。但是,如果你读取一个文件,如何把输入带回程序呢? 这不是问题,因为你可以输出一串命令,由邪恶的解释器解释,使用惰性评估,它可以将结果转储回程序的输入。因此,现在程序接受请求流的响应流。请求流发送到邪恶的解释器,解释器处理完成后会得到响应,然后这个响应将被反馈到输入。由于评估是惰性的,程序会及时生成响应,以便它回到循环并作为输入被消耗。但是这有点脆弱,因为如果你对响应产生的太急迫,那么就可能会遇到某种死锁。因为你会要求回答一个问题,但你还没有将答案从后端吐出来。这样做的目的是:懒惰迫使我们陷入了一个困境,我们必须想到解决这个I/O问题的方法。我认为这非常重要。懒惰带给我们的最重要的东西就是它驱使我们这样做。