每种函数式语言都可以是惰性的吗?

Can every functional language be lazy?

在函数式语言中,函数首先是 class 公民,因此 调用 它们并不是我唯一能做的事情。我也可以将它们存放起来。

现在,当我拥有一种默认情况下是严格的语言时,我仍然不会被迫评估函数调用。我可以选择存储函数及其参数,例如在元组中供以后评估。

所以不用

x = f a b c

我做了类似的事情

x = (f,a,b,c)

然后,我可以用类似

的东西来评估这个东西
eval (f,a,b,c) = f a b c

好吧,可能还有更多,因为我只想对每个未评估的函数调用求值一次,但在我看来,这也可以用一个比简单的更高级的数据结构来解决元组。

反之亦然,因为例如在 Haskell 中,这是默认的惰性我可以使用 seqBangPatterns.

强制评估

所以说每一种函数式语言都有惰性的潜力是正确的,但大多数函数式语言在默认情况下并不惰性,因此需要额外的编程工作懒惰地调用函数,而 haskell 默认情况下是 惰性的,需要额外的编程工作才能以严格的方式调用函数?

如果是这样的话,程序员更难的是:用严格的语言编写惰性函数调用还是用惰性语言编写严格的函数调用?

旁注:Simon P. Jone 说 "the next version of haskell will be strict" 时是认真的吗?我起初以为这是个玩笑。但现在我认为 默认严格 并不是那么糟糕,只要你可以在需要时偷懒。

你的提议会奏效。在 Scheme 中执行此操作的详细信息可以是 found in SICP。可以设想一个 Haskell 的严格版本,其中有一个 "lazy" 函数,其作用与 "seq" 在 Haskell 中的作用相反。然而,将其添加到严格的 Haskell 类语言中需要编译器魔法,否则 thunk 在传递给 "lazy".

之前会被强制执行

但是,如果您的语言具有不受控制的效果,那么这可能会变得棘手,因为每当对其封闭函数进行评估时就会发生效果,并且在懒惰的语言中弄清楚何时会发生这种情况是很困难的。这就是为什么 Haskell 有 IO monad。

懒惰求值,在低层次上,是由一个叫做 thunk 的概念实现的,它包括两件事:

  1. 计算延迟计算值的闭包
  2. 用于存储结果的 set-once 可变存储单元。

第一部分,即闭包,可以用比带有函数及其参数的元组更简单的方式建模。您可以只使用接受单位或不接受参数的函数(取决于您的语言的工作方式),并在其 body 中将函数应用于参数。要计算结果,您只需调用该函数即可。

Paul Johnson 提到了 Scheme,这是证明这一点的完美语言。作为 Scheme 宏(伪代码,未经测试):

(define-syntax delay
  (syntax-rules ()
    ((delay expr ...)
     ;; `(delay expr)` evaluates to a lambda that closes over
     ;; two variables—one to store the result, one to record
     ;; whether the thunk has been forced.
     (let ((value #f)
           (done? #f))
       (lambda ()
         (unless done?
           (set! value (begin expr ...))
           (set! done? #t))
         value)))))

(define (force thunk)
  ;; Thunks are procedures, so to force them you just invoke them.
  (thunk))

但是回到问题的标题:这是否意味着每一种函数式语言都可以是惰性的?答案是没有。 Eager 语言可以实现 thunking 并使用它来提供 opt-in 延迟评估 在 user-selected 点 ,但这与像 Haskell 实现提供。

答案是肯定的。您认为惰性可以用严格的语言实现的直觉是正确的,其中函数是 first-class 对象。但深入细节会发现一些微妙之处。

让我们使用函数式语言(我指的是一种函数可以作为第一个 class 对象构造和操作的语言,就像在 lambda 演算中一样),其中函数应用是严格的(即函数¹及其参数在应用函数之前被完全评估)。我将使用标准 ML 的语法,因为这是一种流行且历史上很重要的严格函数式语言。严格应用<em>F</em><em>A</em>(其中<em>F</em><em>A</em> 是两个表达式)可以通过将其编码为

来延迟
Thunk (F, A)

此对象包含一个函数和一个称为 thunk 的参数。我们可以定义一种类型的 thunks:

datatype ('a, 'b) thunk = Thunk of ('a -> 'b) * 'a;

和一个评估 thunk 的函数:

 fun evaluate (Thunk (f, x)) = f x;

到目前为止还不错,也很简单。但事实上,我们还没有实施惰性求值!我们实现的是正常顺序求值,也称为 call-by-name. The difference is that if the value of the thunk is used more than once, it is calculated every time. Lazy evaluation (also known as call-by-need) 需要最多对表达式求值一次。

在纯粹、严格的语言中,惰性求值实际上是不可能实现的。原因是评估 thunk 会修改系统的状态:它从未评估变为已评估。实现惰性求值需要一种改变系统状态的方法。

这里有一个微妙之处:如果语言的语义纯粹根据表达式的终止状态和终止表达式的值来定义,那么,在纯语言中,call-by-need 和 call-by - 名字是无法区分的。按值调用(即严格求值)是可区分的,因为更少的表达式终止——按需调用和按名称调用隐藏了在从未被求值的 thunk 中发生的任何非终止。 call-by-need 和 call-by-name 的等价性允许惰性评估被认为是正常顺序评估的优化(具有很好的理论特性)。但是在许多程序中,使用按名称调用而不是按值调用会一遍又一遍地计算相同表达式的值,从而耗尽 运行 时间。

在具有可变状态的语言中,惰性求值可以通过在计算时将值存储到 thunk 中来表达。

datatype ('a, 'b) lazy_state = Lazy of ('a -> 'b) * 'a | Value of 'a;
type ('a, 'b) lazy_state = ('a, 'b) lazy_state ref;
let lazy (f, x) = ref (Lazy (f, x));
fun force r =
  case !r of Value y => y
           | Lazy (f, x) => let val y = f x in r := Value y; y end;

这段代码不是很复杂,所以即使在提供惰性求值作为库特性(可能带有语法糖)的 ML 方言中,它在实践中也不会经常使用——通常,需要的值是程序中的已知位置,程序员只需使用一个函数并在该位置将其参数传递给它。

虽然这进入了主观领域,但我要说的是,像这样实现惰性求值比使用 Haskell 这样的语言实现严格求值要容易得多。在 Haskell 中强制进行严格的评估基本上是不可能的(除非将所有内容都包装到一个状态 monad 中并本质上用 Haskell 语法编写 ML 代码)。当然,严格的评估不会改变程序计算的值,但它会对性能产生重大影响(而且,特别是,它有时会受到赞赏,因为它使性能更可预测——预测一个程序的性能Haskell程序可以很辛苦)。

这种评估和存储机制实际上是 Haskell 编译器在幕后所做工作的核心。 Haskell 是纯粹的²,所以你不能用语言本身来实现它!然而,编译器在幕后执行它是合理的,因为这种特殊的副作用使用不会破坏语言的纯度,所以它不会使任何程序转换无效。存储 thunk 的值是合理的原因是它将按名称调用评估转换为按需要调用,正如我们在上面看到的,这既不会改变终止表达式的值,也不会改变终止表达式的值。

这种方法在将纯功能性本地评估与多线程环境和线程间消息传递相结合的语言中可能会有些问题。 (这尤其是 Erlang 的模型。)如果一个线程开始评​​估一个 thunk,而另一个线程恰好需要它的值,将会发生什么?如果不采取任何预防措施,那么两个线程都会计算该值并将其存储。在纯语言中,这在两个线程无论如何都会计算相同值的意义上是无害的³。但是,这可能会损害性能。为确保一个 thunk 只被评估一次,值的计算必须被包裹在一个锁中;这有助于执行多次的长计算,但会损害只执行一次的短计算,因为获取和释放锁需要一些时间。

¹ 函数,当然不是函数体。
² 或者更确切地说,不使用副作用 monad 的 Haskell 片段是纯的。
³ 延迟 thunk 和计算值之间的转换必须是原子的——并发线程必须能够读取惰性值并获得有效的延迟 thunk 或有效的计算值,而不是某种混合这两个不是有效对象。在处理器级别,从延迟 thunk 到计算值的转换通常是指针分配,幸运的是,在大多数体系结构上它是原子的。