【问题标题】:Understanding Lazy Evaluation in Haskell了解 Haskell 中的惰性求值
【发布时间】:2011-10-14 23:06:43
【问题描述】:

我正在努力学习Haskell,但我一直在理解lazy evaluation。 有人可以详细解释我的懒惰评估以及以下 2 个案例的输出[带解释] 与以下给出的相关

伪代码:

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

案例 1:静态绑定,惰性求值

案例 2:动态绑定,惰性求值。

我需要知道最后一行 (echo y) 将打印什么...在上述 2 种情况下。

【问题讨论】:

  • 不,不是......我正在尝试从link自己学习haskell......所以我很难弄清楚懒惰的评估

标签: functional-programming lazy-evaluation haskell


【解决方案1】:

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

恐怕答案在很大程度上取决于单词的含义......

首先,这是 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))

评估就像f 被评估之前 g (h x),即评估 去“外面->里面”。实际上这意味着如果f 看起来像

f x = 2

ie 只是扔掉它的论点,g (h x) 永远不会被评估。

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

  • + 总是计算它的参数! + 是一样的,不管你是用懒惰的 评估与否。

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

Haskell 人通常不会称其为“惰性评估”——他们会称其为 它懒惰(或延迟)执行

那么延迟执行对您的问题意味着什么?这将意味着 action 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)不是 检查直到变量被评估。

我的猜测是,这就是您的问题中“动态绑定”的含义。去 再次使用 dynamic 绑定(意义 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...但我 真的认为这就是问题所希望发生的事情!

【讨论】:

  • 欧文:但是当我们执行 x = 键盘输入(2)而不是回显 y..这里我们使用 y 而不是表达式 (x+3)..所以现在我们必须评估这个表达式,我们有 x==2 所以它不应该在这里打印 5 。 B'coz 我对惰性评估的理解是:delays the evaluation of an expression until its value is actually required。所以这里实际上需要 x 的值来评估 x+3
  • @RanRag:正如 Owen 所指出的,您将延迟 evaluation 与延迟 execution 混淆了。正如introduction of Learn You a Haskell 所说:“您也不能将变量设置为某个值,然后再将其设置为其他值。如果您说a 是5,那么您以后不能说它是其他值,因为您只是说是5。你算什么,骗子?” -- 重点是,你说x = keyboard input -- 意思是,“这个键盘输入发生在这里”。它不会变成“稍后发生的一些稍后的键盘输入”——那是在撒谎!
  • @RanRag:如果你看看 Owen 写的 Haskell 例子,也许另一个线索是。注意&lt;-=之间的区别,你可能会有所启发。
  • @RanRag:实际上,&lt;- 是在应用当前 Monad 之后为下一行函数创建参数的语法糖。在 IO Monad 的 do 块中,每一行基本上意味着“好的,这是您在 IO 交互完成后应该调用的下一个函数”,而 &lt;- 只允许您捕获因此而发生的事情IO交互。我也不会打电话给= assignment。真的,= 只是定义了一个方程式:也就是说,它表示它的左侧由其右侧定义
  • 无论如何,我(否则是错误的)答案中的关键点主要是在这里提出的。基本上,在没有副作用的情况下,表达式的求值顺序不会改变最终结果——尽管它可以确保你得到一个结果,否则你会得到一个无限循环、被零除或其他什么。在 Haskell 中,即使是基于 IO monad 的“受控副作用”代码也不会影响这一点 - 在 order 很重要的地方,无论如何它都是由 monadic 形式强制执行的。 IIRC,只有 unsafe-perform-IO 可能对此有问题。我感到困惑的是 - 无论如何都定义了评估顺序。
【解决方案2】:

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

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

因此,从某个角度来看,如果惰性评估给您带来麻烦,您还没有了解它。当你在学习 Haskell 时,尤其是如果它是你的第一个函数式和纯语言,那么考虑用这种方式表达自己就更重要了。我还认为训练自己习惯于阅读 Haskell(通常非常密集)的语法比完全“摸索”惰性评估更重要。所以如果这个概念给你带来困难,不要太担心。

也就是说,我的解释如下。我没有使用您的示例,因为它们并没有真正受到延迟评估的影响,而且 Owen 比我更清楚地谈到了动态绑定和延迟执行您的示例。


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

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

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

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

但要记住的关键是生成该列表所需的一切在生成占位符时已经确定和修复。它不会受到该计划之后发生的任何其他事情的影响。如果不是这样,那么 Haskell 就不是了。反之亦然;不纯的语言不能在幕后具有 Haskell 风格的完全惰性,因为您将获得的结果可能会根据将来何时需要结果而发生巨大变化。相反,支持惰性求值的不纯语言往往只对程序员明确声明的某些事物使用它,手册中的警告说“不要对依赖于副作用的事物使用惰性”。


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

【讨论】:

    【解决方案3】:

    Haskell 中的惰性求值:最左外层 + 图缩减

    平方 x = x * x

    广场(42广场)

    (Square 42) * (Square 42) -> 由于 Graph Reduction,Square 42 将只计算一次

    (42 * 42) * (42 平方)

    (1764) * (Square 42) -> 接下来是图缩减

    1764 * 1764
    =3111696

    最左-内(Java、C++)

    广场(42广场)

    正方形(42 * 42)

    正方形(1764)

    1764 * 1764 =3111696

    【讨论】:

      猜你喜欢
      • 2014-11-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-04-05
      • 1970-01-01
      • 2014-11-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多