什么是按需呼叫?
我想知道什么是按需调用。
虽然我在维基百科中搜索并在这里找到它: 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
假设我们有这个函数
,并且想要计算
square(1+2)
。在按值调用中,我们执行
square(1+2)
square(3)
3*3
9
在call-by-name中,我们执行
square(1+2)
(1+2)*(1+2 )
3*(1+2)
3*3
9
请注意,由于我们使用该参数两次,因此我们对其求值两次。如果论证评估花费很长时间,那就太浪费了。这就是按需调用所解决的问题。
在按需调用中,我们执行如下操作:
square(1+2)
let x = 1+2 in x*x
let x = 3 in x*x
3*3
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
and we want to evaluate
square(1+2)
.In call-by-value, we do
square(1+2)
square(3)
3*3
9
In call-by-name, we do
square(1+2)
(1+2)*(1+2)
3*(1+2)
3*3
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:
square(1+2)
let x = 1+2 in x*x
let x = 3 in x*x
3*3
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 forx
. Only then do we substitute.BTW, if the argument expression produced something more complicated, like a closure, there might be more shuffling of
let
s 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.
想象一个函数:
然后我们调用它:
在按名称调用语言中,它将被评估为:
a = 3 * 2 = 6
b = 4 / 2 = 2
code>return a + b = 6 + 2 = 8
该函数将返回值
8
。在按需调用(也称为惰性语言)中,其计算方式如下:
a = 3 * 2
b = 4 / 2
return a + b = 3 * 2 + 4 / 2
该函数将返回表达式
3 * 2 + 4 / 2
。到目前为止,几乎没有花费任何计算资源。仅当需要整个表达式的值时才会计算整个表达式 - 假设我们想要打印结果。为什么这有用?有两个原因。首先,如果您不小心包含了死代码,它不会影响您的程序,因此效率会更高。其次,它允许做非常酷的事情,例如使用无限列表进行有效计算:
一种按名称调用的语言会挂在那里,尝试创建一个从 0 到无穷大的列表。惰性语言只会返回
[0,1,2]
。Imagine a function:
And then we call it:
In a call-by-name language this will be evaluated so:
a = 3 * 2 = 6
b = 4 / 2 = 2
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:
a = 3 * 2
b = 4 / 2
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:
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]
.按需调用使用惰性求值。一个简单但说明性的示例:
现在假设我们正在使用急切求值策略,那么它将急切地计算
7*0
和7/0
。如果它是惰性评估策略(按需调用),那么它只会发送表达式7*0
和7/0
直到函数而不评估它们。区别?你会期望执行
do_something(0)
因为第一个参数被使用,尽管它实际上取决于评估策略:如果语言急切地评估,那么它将如所述评估
7 *0
和7/0
首先,7/0
是什么?除零错误。但如果评估策略是惰性的,它会发现它不需要计算除法,它会像我们期望的那样调用
do_something(0)
,没有错误。在此示例中,惰性求值策略可以避免执行过程中产生错误。以类似的方式,它可以使执行免于执行它不会使用的不必要的评估(就像这里没有使用
7/0
一样)。Call-by-need uses lazy evaluation. A simple, yet illustrative example:
Now lets say we're using the eager evaluation strategy, then it would calculate both
7*0
and7/0
eagerly. If it is a lazy evaluated strategy (call-by-need), then it would just send the expressions7*0
and7/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
and7/0
first, and what's7/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).这是用 C 编写的一堆不同评估策略的具体示例。我将具体介绍按名称调用、按值调用和按需要调用之间的区别,这是一种组合前两个,正如 Ryan 的回答所建议的。
我们最感兴趣的部分是调用
foo
时使用k++
作为形式参数c
的实际参数。请注意 后缀如何运算符的工作原理是
k++
首先返回k
,然后将k
加1。即的结果>k++
是只是k
。 (但是,返回结果后,k
将增加 1。)我们可以忽略
foo
内的所有代码,直到行j = c + c
(第二部分)。以下是按值调用下这一行发生的情况:
j = c + c
行之前,因为我们正在执行按值调用,c
将具有计算k++
的值。由于计算k++
返回k
,并且k
为 0(从程序顶部开始),因此c
将是<代码>0。但是,我们确实计算了k++
一次,这会将k
设置为 1。j = 0 + 0
,其行为与正如您所期望的,将j
设置为 0,并将c
保留为 0。printf("k is %i\n", k );
我们明白了k
为 1,因为我们计算了k++
一次。以下是 call-by-name 下的行发生的情况:
c
并且我们正在使用 call-by-name,因此我们替换文本c
与实际参数的文本k++
。因此,该行变为j = (k++) + (k++)
。j = (k++) + (k++)
。其中一个(k++)
将首先被求值,返回0
并将k
设置为 1。然后,第二个(k++ )
将被求值,返回1
(因为k
在第一次求值k++
时被设置为 1),并设置 < code>k 到 2。因此,我们最终得到j = 0 + 1
和k
设置为 2。printf("k is %i\n", k);
,我们得到k
是 2,因为我们计算了k++
两次。最后,call-by-need 下的行会发生以下情况:
j = c + c;
时,我们认识到这是参数c
被评估。因此,我们需要评估其实际参数(一次)并将该值存储为c
的评估。因此,我们计算实际参数k++
,它将返回k
,即0,因此c
的计算将为0。然后,由于我们评估了k++
,k
将被设置为 1。然后我们使用这个存储的评估作为第二个c 的评估
。也就是说,与按名称调用不同,我们不会重新评估k++
。相反,我们重用之前评估的c
初始值,即 0。因此,我们得到j = 0 + 0;
就像c
一样> 是按值传递的。而且,由于我们仅计算k++
一次,因此k
为 1。j = c + c
为j = 0 + 0
在按需调用下,它的运行完全按照您的预期。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.
The part we're most interested in is the fact that
foo
is called withk++
as the actual parameter for the formal parameterc
.Note that how the
++
postfix operator works is thatk++
returnsk
at first, and then incrementsk
by 1. That is, the result ofk++
is justk
. (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 linej = c + c
(the second section).Here's what happens for this line under call-by-value:
j = c + c
, because we're doing call-by-value,c
will have the value of evaluatingk++
. Since evaluatingk++
returnsk
, andk
is 0 (from the top of the program),c
will be0
. However, we did evaluatek++
once, which will setk
to 1.j = 0 + 0
, which behaves exactly like how you'd expect, by settingj
to 0 and leavingc
at 0.printf("k is %i\n", k);
we get thatk
is 1, because we evaluatedk++
once.Here's what happens for the line under call-by-name:
c
and we're using call-by-name, we replace the textc
with the text of the actual argument,k++
. Thus, the line becomesj = (k++) + (k++)
.j = (k++) + (k++)
. One of the(k++)
s will be evaluated first, returning0
and settingk
to 1. Then, the second(k++)
will be evaluated, returning1
(becausek
was set to 1 by the first evaluation ofk++
), and settingk
to 2. Thus, we end up withj = 0 + 1
andk
set to 2.printf("k is %i\n", k);
, we get thatk
is 2 because we evaluatedk++
twice.Finally, here's what happens for the line under call-by-need:
j = c + c;
we recognize that this is the first time the parameterc
is evaluated. Thus we need to evaluate its actual argument (once) and store that value to be the evaluation ofc
. Thus, we evaluate the actual argumentk++
, which will returnk
, which is 0, and therefore the evaluation ofc
will be 0. Then, since we evaluatedk++
,k
will be set to 1. We then use this stored evaluation as the evaluation for the secondc
. That is, unlike call-by-name, we do not re-evaluatek++
. Instead, we reuse the previously evaluated initial value forc
, which is 0. Thus, we getj = 0 + 0;
just as ifc
was pass-by-value. And, since we only evaluatedk++
once,k
is 1.j = c + c
isj = 0 + 0
under call-by-need, and it runs exactly as you'd expect.printf("k is %i\n", k);
, we get thatk
is 1 because we only evaluatedk++
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: