懒惰和纯洁之间有什么联系?
这则推文有两方面的内容:第一,从技术角度来看,懒惰通常要求纯度;其次,从实践观点来看,严格性仍然可以允许纯度,但实践中通常不会这样做(即,严格性会导致纯度 "不再存在")。
Simon Peyton-Jones在论文《Haskell 的历史:课程中的懒惰》中解释了这两个方面。在技术方面,他在第3.2节“Haskell 是纯函数式的”中写道(我用粗体强调):
懒惰的一个直接结果是评估顺序是需求驱动的。因此,几乎不可能在函数调用的结果中可靠地执行输入/输出或其他副作用。因此,Haskell 是一种纯函数式语言。
如果你不明白为什么懒惰会使不纯的效果变得不可靠,我相信这是因为你在过度思考。以下是一个简单的例子,说明了问题。考虑一个假想的不纯函数,它从配置文件中读取一些信息,即一些“基本”配置和一些“扩展”配置,其格式取决于头部中的配置文件版本信息:
getConfig :: Handle -> Config getConfig h = let header = readHeader h basic = readBasicConfig h extended = readExtendedConfig (headerVersion header) h in Config basic extended
其中readHeader
,readBasicConfig
和readExtendedConfig
都是不纯的函数,它们依次从文件中读取字节(即使用典型的基于文件指针的顺序读取)并将它们解析为适当的数据结构。
在一种懒惰的语言中,这个函数可能无法按预期工作。如果header、basic和extended变量的值都被懒惰地评估,那么如果调用者先强制执行basic,然后是extended,那么影响将按顺序调用readBasic、readHeader、readExtendedConfig;而如果调用者先强制执行extended,然后是basic,那么影响将按顺序调用readHeader、readExtendedConfig、readBasic。在任一情况下,本应由一个函数解析的字节将被另一个函数解析。
而且,这些评估顺序是非常简化的假设,假设子函数的影响是“原子性的,并且readExtendedConfig可靠地强制版本参数以便访问extended。如果不是这样,根据强制执行basic和extended的哪个部分,readBasic、readExtendedConfig和readHeader的(子)影响顺序可能被重新排序和/或交错。
你可以通过禁止顺序文件访问来绕过这个特定的限制(尽管这也有一些显著的成本!),但类似的不可预测的无序影响执行将会导致其他I/O操作出现问题(我们如何确保文件更新函数在截断文件以进行更新之前读取旧内容?)、可变变量(那个锁变量什么时候被递增?)等。关于实际方面(再次加粗我的字体),SPJ 写道:
一旦我们致力于一种懒惰的语言,纯语言是不可避免的。反之不成立,但值得注意的是,在实践中,大多数纯编程语言也是懒惰的。为什么呢?因为在按值调用的语言中,无论是函数式的还是不是函数式的,让一个“函数”内部允许无限制的副作用诱惑几乎是不可抗拒的。
...
回顾过去,因此,懒惰的最大好处可能不是懒惰本身,而是懒惰使我们保持纯洁,从而激发了许多有关单子和封装状态的有益工作。
在他的推文中,我认为 Hutton 指的不是懒惰导致纯度的技术后果,而是严格性诱使语言设计师在“这一个特殊情况下”放松纯度的实际后果,此后纯度很快就不复存在了。
你是对的,从现代的视角来看,这真的没有多大意义。真正的问题在于,按默认惯例进行惰性计算会使有副作用代码的推理变成一场噩梦,所以惰性确实需要纯执 行-但反之则不然。
然而需要惰性的是,Haskell语言1.0-1.2版本以及它的前身Miranda在没有使用单子的情况下模拟IO。由于没有任何显式的副作用排序的概念,可执行程序的类型是
main :: [Response] -> [Request]
针对一个简单的交互程序,它将按如下方式工作:main
初始时只是忽略输入列表。因此,由于惰性,那个列表中的值实际上不需要在那个阶段存在。在此期间,它将生成第一个Request
值,例如提示用户输入的终端提示符。然后,键入的内容会以Response
值的形式返回,这时才需要评估它,从而产生新的Request
等等。
https://www.haskell.org/definition/haskell-report-1.0.ps.gz
在1.3版本中,他们转向了我们今天都熟悉和热爱的基于单子的IO接口,此时惰性并不是非常必要了。但在此之前,普遍的观点是,如果没有惰性,与真实世界进行交互的唯一方法就是允许有副作用的函数,因此没有惰性,Haskell只会走上与Lisp和ML同样的道路。