多么懒惰的评价迫使Haskell变得纯粹

时间:2015-07-17 13:33:13

标签: haskell lazy-evaluation purely-functional

我记得看到一个演示文稿,其中SPJ说懒惰评估迫使他们保持Haskell纯粹(或沿着那条线)。我经常看到许多Haskeller说同样的话。

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

4 个答案:

答案 0 :(得分:9)

并不是说懒惰的评价导致纯洁; Haskell一开始就是纯粹的。相反,懒惰的评估迫使语言的设计者保持语言纯粹。

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

  

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

     

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

     

纯语言(具有monadic效果)是否最终是编写程序的最佳方式仍然是一个悬而未决的问题,但它肯定是对编程挑战的激进和优雅的攻击,它是权力与美的结合这激励了设计师。 因此,回想起来,也许懒惰的最大单一好处不是懒惰本身,而是懒惰使我们保持纯洁,从而激发了对monad和封装状态的大量生产性工作。

(我的重点)

我也邀请你听18' 30'' Software Engineering Radio podcast #108由该人自己解释。这是SPJ在Peter Seibel's Coders at Work采访中的一段较长但相关的段落:

  

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

     

[...]如果你有一个懒惰的评估者,那么很难准确预测何时要评估一个表达式。所以这意味着如果你想在屏幕上打印一些内容,每个按值调用的语言,评估的顺序是完全明确的,那就是通过一个不纯的“功能” - 我在它周围加上引号,因为它现在根本不是一个函数 - 类型为字符串到单位。你调用这个函数,作为一个副作用,它会在屏幕上放置一些东西。这就是Lisp中发生的事情;它也发生在ML。它主要发生在每种按值调用的语言中。

     

现在用纯语言,如果你有一个从字符串到单元的函数,你就永远不需要调用它,因为你知道它只是给出了答案单元。这就是功能所能做到的,就是给你答案。你知道答案是什么。但是当然如果它有副作用,那么你做它的调用非常重要。在一种懒惰的语言中,如果你说“f应用于打印"你好"”,那么问题就在于f是否评估它的第一个参数对函数的调用者来说是不明显的。这与函数的内部有关。如果你传递了两个参数,那就是print" hello"并打印"再见",然后您可以按顺序打印其中一个或两个,或两者都不打印。所以,不知何故,对于懒惰的评估,通过副作用进行输入/输出是不可行的。你无法用这种方式编写合理,可靠,可预测的程序。所以,我们不得不忍受这一点。真的有点尴尬,因为你无法真正做任何输入/输出。所以很长一段时间我们基本上都有程序可以把一个字符串带到一个字符串。这就是整个计划的作用。输入字符串是输入,结果字符串是输出,这是所有程序真正可以做的。

     

通过使输出字符串编码某些外部解释器解释的输出命令,您可能会有点聪明。所以输出字符串可能会说:“在屏幕上打印;把它放在磁盘上。“翻译实际上可以做到这一点。因此,您可以想象功能程序是完美的和纯粹的,并且有一种这种邪恶的解释器可以解释一串命令。但是,当然,如果您阅读文件,如何将输入重新输入程序?嗯,这不是问题,因为您可以输出由邪恶解释器解释并使用延迟评估的一串命令,它可以将结果转储回程序的输入。因此,该程序现在对一组请求进行响应。请求流流向邪恶的翻译,向世界做事。每个请求都会生成一个响应,然后反馈给输入。而且由于评估是懒惰的,程序及时发出响应,使其绕过循环并作为输入使用。但它有点脆弱,因为如果你过于急切地消耗你的反应,那么你会遇到某种僵局。因为你要问的是你还没有从后端吐出问题的答案。

     

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

(我的重点)

答案 1 :(得分:7)

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

必须经历这个" monad"有时候生意真的很不方便。关于Stack Overflow的大量问题询问"我如何删除这个IO的东西?"事实证明,有时候,你真的真的想要在这里打印出这个值 - 通常是为了弄清楚实际的****是什么!

用一种急切的语言,开始添加神奇不纯的功能,让你直接输入东西,就像在其他语言中一样,这将是非常诱人的。毫无疑问,你首先要从小事做起,但慢慢地你会滑下这个滑坡,在你知道它之前,效果已经到处都是。

在像哈斯克尔这样的懒惰语言中,这种诱惑仍然存在。很多时候能够在这里或那里快速潜入这一点效果非常有帮助。除了因为懒惰之外,添加效果几乎完全没用。发生任何事情时,您无法控制。即使只是Debug.trace也会产生完全不可理解的结果。

简而言之,如果你正在设计一个 lazy 语言,那么你真的强制想出一个关于你如何处理的连贯故事效果。你不能只是去,我们假装这个功能只是魔术而且#34 ;;如果没有更准确地控制效果的能力,你将立即陷入可怕的混乱中!

TL; DR 用热切的语言,你可以逃脱作弊。在一种懒惰的语言中,你真的来正确地做事,或者它只是简单地无法工作。

是我们雇用Alex的原因 - 等待,错误的窗口......

答案 2 :(得分:2)

这取决于你的意思"纯粹"在这种情况下。

  • 如果对于 pure 你的意思是在纯粹的功能,那么@MathematicalOrchid是真的:懒惰的评价你不知道在哪个序列执行不纯的行为,因此你根本无法编写有意义的程序而且你被迫变得更纯粹(使用IO monad)。

    但是我发现在这种情况下这并不令人满意。一个真正的函数式语言已经分离了纯粹的和不纯的代码,所以即使是严格的 也应该有某种IO

  • 然而,该陈述可能更广泛,并且指的是纯粹的,因为您能够以更具声明性,更具成熟性且更有效的方式表达代码。

查看this答案,该答案正是您引用的语句,它链接到Hughes的文章Why Functional Programming Matters,这可能是您正在讨论的内容。

本文介绍高阶函数延迟评估如何编写更多模块化代码。请注意,对纯粹的功能等做出任何说明。它所做的是关于更加模块化和更具声明性,而不会降低效率。

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

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

在功能上与Haskell一起思考 Richard Bird恰恰提到了这一点。如果我们看第2章,练习D:

  海狸是一个热心的评估者,而苏珊是一个懒惰的人。

     

[...]

     

Beaver对head . filter p . map f更喜欢哪种替代方案?

答案是:

  

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

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

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

答案 3 :(得分:1)

严格来说,这个说法并不正确,因为Haskell有unsafePerformIO,这是语言功能纯洁的一个大漏洞。 (它利用GHC Haskell的功能纯度中的漏洞,最终回到决定通过向语言添加严格的片段来实现未装箱的算术)。 unsafePerformIO之所以存在,是因为诱惑说“好吧,我会在内部使用副作用实现这一功能”#34;对大多数程序员来说是不可抗拒的。但是,如果你看一下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可以用类似的方式解决它(无论如何,单态限制几乎是一个解决方案,你只需要禁止人们解决它通过使用类型签名,就像我上面所做的那样。)