main :: [Response] -> [Request]
对于一个简单的交互式程序,它将会工作得像这样:main
一开始会忽略其输入列表。所以由于惰性求值,那个列表中的值实际上在那个时刻并不需要存在。同时,它会产生第一个 Request
值,例如要求用户输入的终端提示。然后键入的内容将会作为一个 Response
值返回,现在才需要对其进行评估,从而产生一个新的 Request
等等。
这条推文包含两个方面:首先,从技术角度来看,懒惰通常要求纯净;其次,实际上,严格要求可能仍然可以保证纯净,但在实践中通常无法做到(即,在严格性的情况下,纯度“荡然无存”)。
Simon Peyton-Jones在论文《Haskell的历史:具有类的懒惰》中解释了这两个方面。关于技术方面,在第3.2节“Haskell是纯净的”中,他写道(我加粗):
懒惰的一个直接结果是评估顺序是需求驱动的。因此,在函数调用的结果中执行input/output或其他副作用就变得多多少少不可靠。因此,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所指的不是惰性导致纯度的技术后果,而是严格性诱导语言设计者在“只有这一个特殊情况”之后放松纯度,之后纯度很快消失。
unsafePerformIO
允许在计算中进行不受限制的副作用 - 但是延迟计算让大家不敢真正利用它来构建这些不纯的程序,因为它们不能依赖于计算顺序。 - Bergilet f = E in
\x -> f x
不等同于
\x -> E x
如果前面的表达式被急切地求值并且 E
的求值发散,则会出现问题。
急切语言需要区分值和计算:变量只能替换为值,但表达式代表计算,这就是为什么上面的“显而易见”的 let
缩减不合法的原因。表达式超越其所指表示的值,这正是语言具有效果的含义。从这个非常技术性的意义上讲,像 Purescript 这样的急切语言(我所能想到的第一个例子)并不是纯粹的。
当我们忽略非终止和计算顺序时,Purescript 是纯粹的,这也是几乎所有程序员所做的,值得称赞。
相比之下,在懒惰语言中,值和计算之间的区别变得模糊起来。一切都代表值,即使是非终止的表达式,它们也是“底部”的。您可以随心所欲地替换而不必担心表达式的计算过程。在我看来,这正是纯粹的要点。
有人可能会争辩说,在急切语言中,一个发散的表达式实际上也表示为底部,只是 let
表示了一个严格函数。说实话,这可能是一种良好的后验解释,但除了 Haskeller 和编程语言恐怖分子外,没有人会这样想。
let f=E in \x->f x
中,E
只会被评估一次,而在\x->E x
中,它将在每个函数调用时被反复评估。这不像严格求值的情况下得到的 ⊥s 那么明显,但对于程序员来说仍然非常重要。实际上,对于每个出现的情况选择更好的编写方式并不是太大的问题。 - leftaroundaboutfun () ->
延迟计算是表达许多急切程序的必要条件。在 Haskell 中,是的,你必须意识到操作上的问题才能完全理解某些东西的运行方式,但它不会经常出现在语法中,这进一步鼓励了指称式思维。 - Li-yao Xia