什么是按需呼叫?

发布于 2024-10-30 15:19:53 字数 233 浏览 2 评论 0原文

我想知道什么是按需调用。

虽然我在维基百科中搜索并在这里找到它: http://en.wikipedia.org/wiki/Evaluation_strategy, 但无法正确理解。 如果有人可以用一个例子来解释并指出按值调用的区别,那将是一个很大的帮助。

I want to know what is call-by-need.

Though I searched in wikipedia and found it here: http://en.wikipedia.org/wiki/Evaluation_strategy,
but could not understand properly.
If anyone can explain with an example and point out the difference with call-by-value, it would be a great help.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

玩世 2024-11-06 15:19:53

假设我们有这个函数

square(x) = x * x

,并且想要计算 square(1+2)

按值调用中,我们执行

  1. square(1+2)
  2. square(3)
  3. 3*3
  4. 9

call-by-name中,我们执行

  1. square(1+2)
  2. (1+2)*(1+2 )
  3. 3*(1+2)
  4. 3*3
  5. 9

请注意,由于我们使用该参数两次,因此我们对其求值两次。如果论证评估花费很长时间,那就太浪费了。这就是按需调用所解决的问题。

按需调用中,我们执行如下操作:

  1. square(1+2)
  2. let x = 1+2 in x*x
  3. let x = 3 in x*x
  4. 3*3
  5. 9

在步骤 2 中,不要复制参数(如 call-by- name),我们给它一个名字。然后在第 3 步中,当我们注意到需要 x 的值时,我们会计算 x 的表达式。只有这样我们才可以替代。

顺便说一句,如果参数表达式产生更复杂的东西,比如闭包,可能会有更多的 let 乱序以消除复制的可能性。正式规则写起来有些复杂。

请注意,我们“需要”原始操作(例如 +*)的参数值,但对于其他函数,我们采用“命名、等待和查看”方法。我们会说原始算术运算是“严格的”。这取决于语言,但通常大多数原始操作都是严格的。

另请注意,“评估”仍然意味着减少到一个值。函数调用始终返回一个值,而不是表达式。 (其他答案之一犯了这个错误。)OTOH,惰性语言通常具有惰性数据构造函数,它可以具有按需评估的组件,即在提取时评估的组件。这就是您如何拥有“无限”列表的方法——您返回的值是一个惰性数据结构。 但是按需要调用与按值调用是与惰性数据结构与严格数据结构不同的问题。Scheme 具有惰性数据构造函数(流),尽管由于Scheme 是按值调用,因此构造函数是语法形式,而不是普通函数。 Haskell 是按需调用的,但它有定义严格数据类型的方法。

如果有助于思考实现,那么 call-by-name 的一种实现是将每个参数包装在一个 thunk 中;当需要参数时,您可以调用 thunk 并使用该值。 call-by-need 的一种实现是类似的,但是 thunk 是 memoizing;它只运行一次计算,然后保存它并返回保存的答案。

Suppose we have the function

square(x) = x * x

and we want to evaluate square(1+2).

In call-by-value, we do

  1. square(1+2)
  2. square(3)
  3. 3*3
  4. 9

In call-by-name, we do

  1. square(1+2)
  2. (1+2)*(1+2)
  3. 3*(1+2)
  4. 3*3
  5. 9

Notice that since we use the argument twice, we evaluate it twice. That would be wasteful if the argument evaluation took a long time. That's the issue that call-by-need fixes.

In call-by-need, we do something like the following:

  1. square(1+2)
  2. let x = 1+2 in x*x
  3. let x = 3 in x*x
  4. 3*3
  5. 9

In step 2, instead of copying the argument (like in call-by-name), we give it a name. Then in step 3, when we notice that we need the value of x, we evaluate the expression for x. Only then do we substitute.

BTW, if the argument expression produced something more complicated, like a closure, there might be more shuffling of lets around to eliminate the possibility of copying. The formal rules are somewhat complicated to write down.

Notice that we "need" values for the arguments to primitive operations like + and *, but for other functions we take the "name, wait, and see" approach. We would say that the primitive arithmetic operations are "strict". It depends on the language, but usually most primitive operations are strict.

Notice also that "evaluation" still means to reduce to a value. A function call always returns a value, not an expression. (One of the other answers got this wrong.) OTOH, lazy languages usually have lazy data constructors, which can have components that are evaluated on-need, ie, when extracted. That's how you can have an "infinite" list---the value you return is a lazy data structure. But call-by-need vs call-by-value is a separate issue from lazy vs strict data structures. Scheme has lazy data constructors (streams), although since Scheme is call-by-value, the constructors are syntactic forms, not ordinary functions. And Haskell is call-by-need but it has ways of defining strict data types.

If it helps to think about implementations, then one implementation of call-by-name is to wrap every argument in a thunk; when the argument is needed, you call the thunk and use the value. One implementation of call-by-need is similar, but the thunk is memoizing; it only runs the computation once, then it saves it and just returns the saved answer after that.

梦里的微风 2024-11-06 15:19:53

想象一个函数:

fun add(a, b) {
  return a + b
}

然后我们调用它:

 add(3 * 2, 4 / 2)

在按名称调用语言中,它将被评估为:

  1. a = 3 * 2 = 6
  2. b = 4 / 2 = 2 code>
  3. return a + b = 6 + 2 = 8

该函数将返回值8

在按需调用(也称为惰性语言)中,其计算方式如下:

  1. a = 3 * 2
  2. b = 4 / 2
  3. return a + b = 3 * 2 + 4 / 2

该函数将返回表达式 3 * 2 + 4 / 2。到目前为止,几乎没有花费任何计算资源。仅当需要整个表达式的值时才会计算整个表达式 - 假设我们想要打印结果。

为什么这有用?有两个原因。首先,如果您不小心包含了死代码,它不会影响您的程序,因此效率会更高。其次,它允许做非常酷的事情,例如使用无限列表进行有效计算:

fun takeFirstThree(list) {
  return [list[0], list[1], list[2]]
}

takeFirstThree([0 ... infinity])

一种按名称调用的语言会挂在那里,尝试创建一个从 0 到无穷大的列表。惰性语言只会返回[0,1,2]

Imagine a function:

fun add(a, b) {
  return a + b
}

And then we call it:

 add(3 * 2, 4 / 2)

In a call-by-name language this will be evaluated so:

  1. a = 3 * 2 = 6
  2. b = 4 / 2 = 2
  3. return a + b = 6 + 2 = 8

The function will return the value 8.

In a call-by-need (also called a lazy language) this is evaluated like so:

  1. a = 3 * 2
  2. b = 4 / 2
  3. return a + b = 3 * 2 + 4 / 2

The function will return the expression 3 * 2 + 4 / 2. So far almost no computational resources have been spent. The whole expression will be computed only if its value is needed - say we wanted to print the result.

Why is this useful? Two reasons. First if you accidentally include dead code it doesn't weigh your program down and thus can be a lot more efficient. Second it allows to do very cool things like efficiently calculating with infinite lists:

fun takeFirstThree(list) {
  return [list[0], list[1], list[2]]
}

takeFirstThree([0 ... infinity])

A call-by-name language would hang there trying to create a list from 0 to infinity. A lazy language will simply return [0,1,2].

三生池水覆流年 2024-11-06 15:19:53

按需调用使用惰性求值。一个简单但说明性的示例:

function choose(cond, arg1, arg2) {
   if (cond)
      do_something(arg1);
   else
      do_something(arg2);
}

choose(true, 7*0, 7/0);

现在假设我们正在使用急切求值策略,那么它将急切地计算 7*07/0。如果它是惰性评估策略(按需调用),那么它只会发送表达式 7*07/0直到函数而不评估它们。

区别?你会期望执行 do_something(0) 因为第一个参数被使用,尽管它实际上取决于评估策略:

如果语言急切地评估,那么它将如所述评估 7 *07/0 首先,7/0 是什么?除零错误。

但如果评估策略是惰性的,它会发现它不需要计算除法,它会像我们期望的那样调用 do_something(0) ,没有错误。

在此示例中,惰性求值策略可以避免执行过程中产生错误。以类似的方式,它可以使执行免于执行它不会使用的不必要的评估(就像这里没有使用 7/0 一样)。

Call-by-need uses lazy evaluation. A simple, yet illustrative example:

function choose(cond, arg1, arg2) {
   if (cond)
      do_something(arg1);
   else
      do_something(arg2);
}

choose(true, 7*0, 7/0);

Now lets say we're using the eager evaluation strategy, then it would calculate both 7*0 and 7/0 eagerly. If it is a lazy evaluated strategy (call-by-need), then it would just send the expressions 7*0 and 7/0 through to the function without evaluating them.

The difference? you would expect to execute do_something(0) because the first argument gets used, although it actually depends on the evaluation strategy:

If the language evaluates eagerly, then it will, as stated, evaluate 7*0 and 7/0 first, and what's 7/0? Divide-by-zero error.

But if the evaluation strategy is lazy, it will see that it doesn't need to calculate the division, it will call do_something(0) as we were expecting, with no errors.

In this example, the lazy evaluation strategy can save the execution from producing errors. In a similar manner, it can save the execution from performing unnecessary evaluation that it won't use (the same way it didn't use 7/0 here).

分分钟 2024-11-06 15:19:53

这是用 C 编写的一堆不同评估策略的具体示例。我将具体介绍按名称调用、按值调用和按需要调用之间的区别,这是一种组合前两个,正如 Ryan 的回答所建议的。

#include<stdio.h>
int x = 1;
int y[3]= {1, 2, 3};
int i = 0;
int k = 0;
int j = 0;

int foo(int a, int b, int c) {
    i = i + 1;
    // 2 for call-by-name
    // 1 for call-by-value, call-by-value-result, and call-by-reference
    // unsure what call-by-need will do here; will likely be 2, but could have evaluated earlier than needed
    printf("a is %i\n", a);
    b = 2;
    // 1 for call-by-value and call-by-value-result
    // 2 for call-by-reference, call-by-need, and call-by-name
    printf("x is %i\n", x);

    // this triggers multiple increments of k for call-by-name
    j = c + c;

    // we don't actually care what j is, we just don't want it to be optimized out by the compiler
    printf("j is %i\n", j);

    // 2 for call-by-name
    // 1 for call-by-need, call-by-value, call-by-value-result, and call-by-reference
    printf("k is %i\n", k);
}

int main() {
    int ans = foo(y[i], x, k++);
    // 2 for call-by-value-result, call-by-name, call-by-reference, and call-by-need
    // 1 for call-by-value
    printf("x is %i\n", x);
    return 0;
}

我们最感兴趣的部分是调用 foo 时使用 k++ 作为形式参数 c 的实际参数。

请注意 后缀如何运算符的工作原理k++首先返回k,然后将k加1。即的结果>k++ 是只是k。 (但是,返回结果后,k 将增加 1。)

我们可以忽略 foo 内的所有代码,直到行 j = c + c(第二部分)。

以下是按值调用下这一行发生的情况:

  1. 首次调用函数时,在遇到 j = c + c 行之前,因为我们正在执行按值调用,c 将具有计算 k++ 的值。由于计算 k++ 返回 k,并且 k 为 0(从程序顶部开始),因此 c 将是<代码>0。但是,我们确实计算了 k++ 一次,这会将 k 设置为 1。
  2. 该行变为 j = 0 + 0,其行为与正如您所期望的,将 j 设置为 0,并将 c 保留为 0。
  3. 然后,当我们运行 printf("k is %i\n", k ); 我们明白了k 为 1,因为我们计算了 k++ 一次。

以下是 call-by-name 下的行发生的情况:

  1. 由于该行包含 c 并且我们正在使用 call-by-name,因此我们替换文本 c 与实际参数的文本 k++。因此,该行变为j = (k++) + (k++)
  2. 然后我们运行j = (k++) + (k++)。其中一个 (k++) 将首先被求值,返回 0 并将 k 设置为 1。然后,第二个 (k++ ) 将被求值,返回 1 (因为 k 在第一次求值 k++ 时被设置为 1),并设置 < code>k 到 2。因此,我们最终得到j = 0 + 1k 设置为 2。
  3. 然后,当我们运行 printf("k is %i\n", k);,我们得到 k 是 2,因为我们计算了 k++ 两次。

最后,call-by-need 下的行会发生以下情况:

  1. 当我们遇到 j = c + c; 时,我们认识到这是参数 c 被评估。因此,我们需要评估其实际参数(一次)并将该值存储为 c 的评估。因此,我们计算实际参数k++,它将返回k,即0,因此c的计算将为0。然后,由于我们评估了 k++k 将被设置为 1。然后我们使用这个存储的评估作为第二个 c 的评估。也就是说,与按名称调用不同,我们不会重新评估 k++。相反,我们重用之前评估的 c 初始值,即 0。因此,我们得到 j = 0 + 0; 就像 c 一样> 是按值传递的。而且,由于我们仅计算 k++ 一次,因此 k 为 1。
  2. 如上一步所述,j = c + cj = 0 + 0 在按需调用下,它的运行完全按照您的预期。
  3. 当我们运行 printf("k is %i\n", k); 时,我们得到 k 为 1,因为我们只计算了 k++一次。

希望这有助于区分按值调用、按名称调用和按需要调用的工作方式。如果更清楚地区分按值调用和按需要调用有帮助,请在评论中告诉我,我将在 foo 中解释前面的代码及其工作原理就是这样。

我认为维基百科的这句话很好地总结了事情:

按需要调用是按名称调用的记忆变体,其中,如果对函数参数进行求值,则存储该值以供后续使用。如果参数是纯参数(即没有副作用),则这会产生与按名称调用相同的结果,从而节省了重新计算参数的成本。

Here's a concrete example for a bunch of different evaluation strategies written in C. I'll specifically go over the difference between call-by-name, call-by-value, and call-by-need, which is kind of a combination of the previous two, as suggested by Ryan's answer.

#include<stdio.h>
int x = 1;
int y[3]= {1, 2, 3};
int i = 0;
int k = 0;
int j = 0;

int foo(int a, int b, int c) {
    i = i + 1;
    // 2 for call-by-name
    // 1 for call-by-value, call-by-value-result, and call-by-reference
    // unsure what call-by-need will do here; will likely be 2, but could have evaluated earlier than needed
    printf("a is %i\n", a);
    b = 2;
    // 1 for call-by-value and call-by-value-result
    // 2 for call-by-reference, call-by-need, and call-by-name
    printf("x is %i\n", x);

    // this triggers multiple increments of k for call-by-name
    j = c + c;

    // we don't actually care what j is, we just don't want it to be optimized out by the compiler
    printf("j is %i\n", j);

    // 2 for call-by-name
    // 1 for call-by-need, call-by-value, call-by-value-result, and call-by-reference
    printf("k is %i\n", k);
}

int main() {
    int ans = foo(y[i], x, k++);
    // 2 for call-by-value-result, call-by-name, call-by-reference, and call-by-need
    // 1 for call-by-value
    printf("x is %i\n", x);
    return 0;
}

The part we're most interested in is the fact that foo is called with k++ as the actual parameter for the formal parameter c.

Note that how the ++ postfix operator works is that k++ returns k at first, and then increments k by 1. That is, the result of k++ is just k. (But, then after that result is returned, k will be incremented by 1.)

We can ignore all of the code inside foo up until the line j = c + c (the second section).

Here's what happens for this line under call-by-value:

  1. When the function is first called, before it encounters the line j = c + c, because we're doing call-by-value, c will have the value of evaluating k++. Since evaluating k++ returns k, and k is 0 (from the top of the program), c will be 0. However, we did evaluate k++ once, which will set k to 1.
  2. The line becomes j = 0 + 0, which behaves exactly like how you'd expect, by setting j to 0 and leaving c at 0.
  3. Then, when we run printf("k is %i\n", k); we get that k is 1, because we evaluated k++ once.

Here's what happens for the line under call-by-name:

  1. Since the line contains c and we're using call-by-name, we replace the text c with the text of the actual argument, k++. Thus, the line becomes j = (k++) + (k++).
  2. We then run j = (k++) + (k++). One of the (k++)s will be evaluated first, returning 0 and setting k to 1. Then, the second (k++) will be evaluated, returning 1 (because k was set to 1 by the first evaluation of k++), and setting k to 2. Thus, we end up with j = 0 + 1 and k set to 2.
  3. Then, when we run printf("k is %i\n", k);, we get that k is 2 because we evaluated k++ twice.

Finally, here's what happens for the line under call-by-need:

  1. When we encounter j = c + c; we recognize that this is the first time the parameter c is evaluated. Thus we need to evaluate its actual argument (once) and store that value to be the evaluation of c. Thus, we evaluate the actual argument k++, which will return k, which is 0, and therefore the evaluation of c will be 0. Then, since we evaluated k++, k will be set to 1. We then use this stored evaluation as the evaluation for the second c. That is, unlike call-by-name, we do not re-evaluate k++. Instead, we reuse the previously evaluated initial value for c, which is 0. Thus, we get j = 0 + 0; just as if c was pass-by-value. And, since we only evaluated k++ once, k is 1.
  2. As explained in the previous step, j = c + c is j = 0 + 0 under call-by-need, and it runs exactly as you'd expect.
  3. When we run printf("k is %i\n", k);, we get that k is 1 because we only evaluated k++ once.

Hopefully this helps to differentiate how call-by-value, call-by-name, and call-by-need work. If it would be helpful to differentiate call-by-value and call-by-need more clearly, let me know in a comment and I'll explain the code earlier on in foo and why it works the way it does.

I think this line from Wikipedia sums things up nicely:

Call by need is a memoized variant of call by name, where, if the function argument is evaluated, that value is stored for subsequent use. If the argument is pure (i.e., free of side effects), this produces the same results as call by name, saving the cost of recomputing the argument.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文