懒惰和纯洁有什么关系?

What is the connection between laziness and purity?

Laziness is what kept Haskell pure. If it had been strict, purity would soon go out the window.

我看不出一个语言的评价策略和它的纯度之间的联系。考虑到推文作者的声誉,我肯定忽略了一些事情。也许有人可以解释一下。

你是对的,从现代的 POV 来看,这确实没有意义。 的是 lazy-by-default 会使 side-effectful 代码的推理成为一场噩梦,因此懒惰确实需要纯洁性——但反之则不然。

但真正需要惰性的是 Haskell 版本 1.0–1.2 中的方式,继其前身 Miranda 之后,没有 monad 的模拟 IO。缺少 side-effect 排序的任何明确概念,可执行程序的类型是

main :: [Response] -> [Request]

对于一个简单的交互式程序,它会像这样工作: main 首先会忽略它的输入列表。所以多亏了懒惰,那个列表中的值实际上不需要存在,在这一点上。同时它会产生第一个 Request 值,例如提示用户输入内容的终端提示。键入的内容将作为 Response 值返回,现在才真正需要对其进行评估,从而产生新的 Request,等等。

https://www.haskell.org/definition/haskell-report-1.0.ps.gz

在 1.3 版中,他们切换到我们今天都知道和喜爱的 monadic-IO 界面,那时懒惰不再是真正必要的了。但在此之前,普遍的看法是没有惰性与现实世界交互的唯一方法是允许 side-effectful 函数,因此没有惰性的说法,Haskell 只会走同样的路之前的 Lisp 和 ML。

这条推文有两个方面:首先,从技术角度来看,懒惰通常要求纯洁;其次,从实际的角度来看,严格性 可以 仍然允许纯度,但在实践中通常不允许(即,严格性,纯度“超出 window").

Simon Peyton-Jones 在论文 "A History of Haskell: Being Lazy With Class" 中解释了这两个方面。关于技术方面,在 3.2 Haskell is Pure 部分,他写道(我 粗体 强调):

An immediate consequence of laziness is that evaluation order is demand-driven. As a result, it becomes more or less impossible to reliably perform input/output or other side effects as the result of a function call. Haskell is, therefore, a pure language.

如果您不明白为什么懒惰会使不纯的效果变得不可靠,我敢肯定那是因为您 over-thinking 它。这是一个说明问题的简单示例。考虑一个假设的非纯函数,它从配置文件中读取一些信息,即一些“基本”配置和一些“扩展”配置,其格式取决于 header:

中的配置文件版本信息
getConfig :: Handle -> Config
getConfig h =
  let header = readHeader h
      basic = readBasicConfig h
      extended = readExtendedConfig (headerVersion header) h
  in Config basic extended

其中 readHeaderreadBasicConfigreadExtendedConfig 都是从文件中顺序读取字节的不纯函数(即,使用典型的文件 pointer-based 顺序读取)并将它们解析为适当的数据结构。

在惰性语言中,此功能可能无法按预期工作。如果 headerbasicextended 变量值都是延迟计算的,那么如果调用者强制 basic 先跟 extended,效果将是按顺序调用 readBasicreadHeaderreadExtendedConfig;而如果调用者强制 extended 先是 basic,效果将按 readHeaderreadExtendedConfigreadBasic 的顺序调用。在任何一种情况下,旨在由一个函数解析的字节将由另一个函数解析。

并且,这些评估顺序过于简单化,假设 sub-functions 的效果是“原子的”,并且 readExtendedConfig 可靠地强制版本参数以访问 extended .如果不是,根据 basicextended 部分 被强制执行,readBasic、[=13] 中(子)效果的顺序=],并且 readHeader 可以重新排序 and/or 混合。

您可以通过禁止顺序文件访问来解决此特定限制(尽管这会带来一些显着的成本!),但类似的不可预测的 out-of-order 效果执行会导致其他 I/O 操作出现问题(我们如何确保文件更新函数在截断文件以进行更新之前读取旧内容?),可变变量(当 恰好 时该锁定变量会递增吗?),等等

关于实际方面(再次强调我的 大胆 ),SPJ 写道:

Once we were committed to a lazy language, a pure one was inescapable. The converse is not true, but it is notable that in practice most pure programming languages are also lazy. Why? Because in a call-by-value language, whether functional or not, the temptation to allow unrestricted side effects inside a "function" is almost irresistible.

...

In retrospect, therefore, perhaps the biggest single benefit of laziness is not laziness per se, but rather that laziness kept us pure, and thereby motivated a great deal of productive work on monads and encapsulated state.

在他的推文中,我相信 Hutton 指的不是懒惰导致纯洁性的技术后果,而是严格诱使语言设计者放松纯洁性的实际后果“仅在这种特殊情况下”,之后window.

的纯度很快就消失了

其他答案给出的历史背景很可能是该评论所指的内容。我认为连接更深。

Eager 语言,即使是那些自称为“纯”的语言,也没有像 Haskell:

中那样强烈的指称透明性
let f = E in
\x -> f x

不等于

\x -> E x

如果前一个表达式急切求值而 E 的求值发散。

Eager 语言需要区分值和计算:变量仅用值代替,而表达式代表计算,这就是为什么上面 let 的“明显”减少无效的原因。一个表达式不仅仅是它所表示的值,这正是它对一种语言有效的意义。在这个非常技术性的意义上,像 Purescript(我能想到的第一个例子 space)这样的热切语言并不纯粹。

当我们忽略 non-termination 和计算顺序时,Purescript 是纯粹的,这几乎是每个程序员都会做的,因此值得信赖。

相比之下,在惰性语言中,值和计算之间的区别是模糊的。一切都表示一个值,甚至 non-terminating 表达式,它们都是“底部”。 你可以替换所有你想要的,而不用担心表达式 do。在我看来,这就是纯度。

有人可能会争辩说,实际上,我们也可以说急切语言中的发散表达式表示 bottom 而只是 let 表示严格函数。老实说,这可能是一个很好的后验解释,但除了 Haskell 使用者和编程语言恐怖分子之外,没有人这么想。