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:
- 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.
- The line becomes
j = 0 + 0
, which behaves exactly like how you'd expect, by setting j
to 0 and leaving c
at 0.
- 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:
- 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++)
.
- 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.
- 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:
- 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.
- 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.
- 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.