了解Haskell中的惰性求值

时间:2011-10-14 23:06:44

标签: functional-programming lazy-evaluation haskell

我正在努力学习Haskell,但我坚持理解lazy evaluation。 有人可以详细解释我的懒惰评价和以下两个案例的输出[与解释]有关以下给出

伪代码:

x = keyboard input (5)
y = x + 3 (=8)
echo y (8)
x = keyboard input (2)
echo y

案例1:静态绑定,懒惰评估

案例2:动态绑定,懒惰评估。

在上述两种情况下,我需要知道最后一行(echo y)打印的内容......

3 个答案:

答案 0 :(得分:10)

对不起,这太长了,但是......

我担心答案很大程度上取决于词语的含义......

首先,这是Haskell中的代码(使用静态绑定和延迟评估):

readInt :: String -> Int
readInt = read

main = do
    x <- fmap readInt getLine
    let y = x + 3
    print y
    x <- fmap readInt getLine
    print y

它会打印88

现在这里是R中的代码,它使用了懒惰的评估和一些人所说的 动态绑定:

delayedAssign('x', as.numeric(readLines(n=1)))
delayedAssign('y', x + 3)
print(y)

delayedAssign('x', as.numeric(readLines(n=1)))
print(y)

它会打印88。没那么不同!

现在在C ++中,它使用严格的评估和静态绑定:

#include <iostream>

int main() {
    int x;
    std::cin >> x;
    int y = x + 3;
    std::cout << y << "\n";
    std::cin >> x;
    std::cout << y << "\n";
}

它会打印88

现在让我告诉你我认为问题的实际意义是什么;)

“懒惰的评价”可能意味着许多不同的东西。在Haskell它有一个非常 特殊含义,即嵌套表达式中的含义:

f (g (h x))

评估的工作方式就像fg (h x)之前评估一样,即评估 去“外面 - &gt; in”。实际上这意味着如果f看起来像

f x = 2

即只是抛弃它的论点,g (h x)永远不会被评估。

但我认为那是而不是问题与“懒惰”有关 评估“。我认为这是因为:

  • +总是评估其参数!无论你是否使用懒惰,+都是一样的 评估与否。

  • 唯一可以延迟的计算是keyboard input - 并且这不是真正的计算,因为它会导致动作发生; 也就是说,它从用户那里读取。

Haskell人通常不会称之为“懒惰的评价” - 他们会打电话 它是懒惰的(或推迟的)执行

那么懒惰的执行对你的问题意味着什么呢?这意味着 操作 keyboard input被延迟......直到值x确实存在 需要。在我看来,这就发生在这里:

echo y

因为在那时你必须向用户显示一个值,所以你必须知道什么 x是!那么懒惰的执行和静态绑定会发生什么?

x = keyboard input     # nothing happens
y = x + 3              # still nothing happens!
echo y (8)             # y becomes 8. 8 gets printed.
x = keyboard input (2) # nothing happens
echo y                 # y is still 8. 8 gets printed.

现在关于这个词“动态绑定”。它可能意味着不同的东西:

  1. 可变范围和生命周期在运行时决定。这是什么语言 像R那样不会声明变量。

  2. 计算公式(如y的公式为x + 3)不是 检查,直到评估变量。

  3. 我的猜测是,“动态绑定”在你的问题中意味着什么。展望 使用动态绑定(感知2)和延迟执行再次使用代码:

    x = keyboard input     # nothing happens
    y = x + 3              # still nothing happens!
    echo y (8)             # y becomes 8. 8 gets printed.
    x = keyboard input (2) # nothing happens
    echo y                 # y is already evaluated, 
                           # so it uses the stored value and prints 8
    

    我知道没有语言可以在最后一行打印7 ......但是我 真的认为这就是希望会发生的问题!

答案 1 :(得分:2)

关于Haskell中延迟评估的关键在于它根本不会影响程序的输出。您可以阅读它,就好像所有内容都是在定义后立即进行评估一样,您仍然会得到相同的结果。

延迟评估只是一个策略,用于计算程序中表达式的值。有很多可能,它们都给出相同的结果[1];任何改变程序含义的评估策略都不是一个有效的策略!

因此,从某个角度来看,如果它给你带来麻烦,你不会 来理解延迟评估。当你学习Haskell时,特别是如果它是你的第一个功能性和纯粹的语言,那么考虑用这种方式表达自己就更重要了。我还要对自己进行训练,以便习惯于阅读Haskell(通常非常密集)语法,这比完全“grokking”懒惰评估更重要。所以如果这个概念给你带来困难,不要太担心。

那就是说,我的解释是在下面。我没有使用过你的例子,因为他们并没有真正受到懒惰评估的影响,而Owen在动态绑定和延迟执行方面已经比你的例子说得更清楚了。


(有效)评估策略之间最重要的区别在于,某些策略可能无法在其他策略可能成功的情况下返回结果。懒惰评估具有特定属性,即如果任何(有效)评估策略可以找到结果,则懒惰评估将找到它。特别是,生成无限数据结构然后仅使用有限数量的数据的程序可以通过延迟评估终止。在您可能习惯的严格评估中,程序必须先完成生成无限数据结构,然后才能继续使用它的一部分,当然它会。

懒惰评估实现这一目标的方法是仅在需要时评估某些内容,以确定下一步该做什么。当您调用返回列表的函数时,它会立即“返回”并为您提供列表的占位符。该占位符可以传递给其他函数,存储在其他数据结构中,任何东西。只有当程序需要知道关于列表的某些内容时,才会对其进行实际评估,并且只在需要时进行评估。

如果列表为空,则表示程序现在将执行不同的操作。最初返回占位符的函数调用进一步评估,以查看它是返回空列表还是带有head元素的列表。然后评估再次停止,因为程序现在知道要走哪条路。如果从不需要列表的其余部分,则永远不会对其进行评估。

但它的评估时间也不是所需的。如果占位符被传递给多个函数(因此它现在涉及其他尚未评估的函数调用),或存储到几个不同的数据结构中,Haskell仍然“知道”它们是完全相同的,并安排它们所有人都要“看到”任何进一步评估占位符的影响。最终,如果某个地方需要所有列表,那么它们都将指向一个普通的完全评估的数据结构,并且懒惰不会产生进一步的影响。

但要记住的关键是,生成占位符时已经确定并修复了生成该列表所需的所有不能受到程序中发生的任何其他事件的影响。如果不是这样,那么Haskell就不会纯粹。反之亦然;不纯的语言不能在幕后拥有Haskell风格的完全懒惰,因为你得到的结果可能会发生巨大变化,具体取决于在将来时需要的结果。相反,支持延迟评估的不纯语言倾向于仅将其用于程序员明确声明的某些事物,手册中的警告说“不要对依赖于副作用的事物使用懒惰”。


[1]我在这里撒谎。继续阅读下面的内容以了解原因。

答案 2 :(得分:1)

Haskell中的懒惰评估:最左边 - 最外层+图形缩减

方形x = x * x

广场(42号广场)

(方形42)*(方形42) - &gt;由于图形缩减

,方形42将仅计算一次

(42 * 42)*(Square 42)

(1764)*(Square 42) - &gt;接下来是图表缩减

1764年* 1764年 = 3111696

最左边(Java,C ++)

广场(42号广场)

方形(42 * 42)

square(1764)

1764 * 1764 = 3111696