The goal of this lecture note is to investigate some more the notion of when and in which order a computation takes place. Intuitively, all the traced functions we have used so far helped us visualize the order in which calls, returns, and tail calls are taking place, but still, intuition is not reason. Consider, for example, the following expression:
(fun _ -> 42) (1 / 0)
What is the result of evaluating it?
Analysis:
Possible answers:
The issue is that of evaluation order, and each programming language has a specific one. This issue determines how function applications are evaluated and what variables denote. Many evaluation orders exist: here, we concentrate on call by value, call by name, and call by need.
A programming language is call by value (or again strict or eager) if function arguments are evaluated before functions are applied.
OCaml, for example, is call by value, and so in the example above, the exception Division_by_zero is raised.
In a call-by-value language, variables denote values, i.e., the result of evaluating actual parameters (for functions) and definienses (for let-expressions).
A programming language is call by name (or again non strict or lazy) if function arguments are not evaluated before functions are applied, but only when they are needed as the body of the function is being evaluated.
ALGOL 60, for example, is call by name, and so in the example above (rewritten in a suitable syntax), the answer is 42.
In a call-by-name language, variables denote suspended evaluations, i.e., the result of not evaluating actual parameters (for functions) and definienses (for let-expressions).
Each time a variable is evaluated in the body of a function or of a let expression, the suspended evaluation is “thawed” and takes place.
A programming language is call by need (or again non strict or fully lazy) if function arguments are not evaluated before functions are applied, but only when they are needed as the body of the function is being evaluated. Furthermore, the first time a function argument is evaluated, the result of this evaluation is memoized to be subsequently reused, should it be needed again.
Haskell, for example, is call by need, and so in the example above (rewritten in a suitable syntax), the answer is 42.
In a call-by-need language, variables denote suspended evaluations, i.e., the result of not evaluating actual parameters (for functions) and definienses (for let-expressions).
The first time a variable is evaluated in the body of a function or of a let expression, the suspended evaluation is “thawed” and takes place. The result is then memoized and subsequently reused, should this variable be evaluated again.
The introductory example was that of a non-strict function, i.e., a function whose result does not need its argument to be evaluated:
So the example makes it possible to distinguish call by value from call by name and call by need, but not call by name from call by need.
A linear function uses its argument exactly once. Like a non-strict function, it makes it possible to distinguish call by value from call by name and call by need (since in call by value, the argument is evaluated before the function is called, whereas in call by name and call by need, it is evaluated after the function is called), but not call by name from call by need.
A non-linear function uses its argument more than once.
Consider the following function application:
(fun n -> n + n) (let () = Printf.printf "hello\n" in 10)
Analysis:
So:
A programming language is left-to-right call by value if, in an application, the function is evaluated before the actual parameter.
A programming language is right-to-left call by value if, in an application, the function is evaluated after the actual parameter.
As we have seen, that is the case for OCaml.
Consider the following function declaration in the syntax of OCaml:
let chirpy b x y =
if b
then x
else y + y;;
The goal of this exercise is to determine what happens when the following two expressions are evaluated in a variety of evaluation orders:
chirpy true
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
chirpy false
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
Assuming left-to-right call by value, what happens when the first expression (the one with true) is evaluated?
Justify your answer.
Assuming right-to-left call by value, what happens when the first expression (the one with true) is evaluated?
Justify your answer.
Assuming call by name and that the arguments of + are evaluated from left to right, what happens when the first expression (the one with true) is evaluated?
Justify your answer.
Assuming call by name and that the arguments of + are evaluated from right to left, what happens when the first expression (the one with true) is evaluated?
Justify your answer.
Assuming call by need and that the arguments of + are evaluated from left to right, what happens when the first expression (the one with true) is evaluated?
Justify your answer.
Assuming call by need and that the arguments of + are evaluated from right to left, what happens when the first expression (the one with true) is evaluated?
Justify your answer.
Assuming left-to-right call by value, what happens when the second expression (the one with false) is evaluated?
Justify your answer.
Assuming right-to-left call by value, what happens when the second expression (the one with false) is evaluated?
Justify your answer.
Assuming call by name and that the arguments of + are evaluated from left to right, what happens when the second expression (the one with false) is evaluated?
Justify your answer.
Assuming call by name and that the arguments of + are evaluated from right to left, what happens when the second expression (the one with false) is evaluated?
Justify your answer.
Assuming call by need and that the arguments of + are evaluated from left to right, what happens when the second expression (the one with false) is evaluated?
Justify your answer.
Assuming call by need and that the arguments of + are evaluated from right to left, what happens when the second expression (the one with false) is evaluated?
Justify your answer.
Assuming left-to-right call by value, here is what happens when evaluating:
chirpy true
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
The result is 1.
So all in all, hello is printed, world is printed, and 1 is returned.
Assuming right-to-left call by value, here is what happens when evaluating:
chirpy true
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
The result is 1.
So all in all, world is printed, hello is printed, and 1 is returned.
Assuming call by name and that the arguments of + are evaluated from left to right, here is what happens when evaluating:
chirpy true
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
The result is 1.
So all in all, hello is printed and 1 is returned.
Assuming call by name and that the arguments of + are evaluated from right to left, here is what happens when evaluating:
chirpy true
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
The same thing happens as in c., since + is not applied.
Assuming call by need and that the arguments of + are evaluated from left to right, here is what happens when evaluating:
chirpy true
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
The result is 1.
So all in all, hello is printed and 1 is returned.
Assuming call by need and that the arguments of + are evaluated from right to left, here is what happens when evaluating:
chirpy true
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
The same thing happens as in e., since + is not applied.
Assuming left-to-right call by value, here is what happens when evaluating:
chirpy false
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
The result is 20.
So all in all, hello is printed, world is printed, and 20 is returned.
Assuming right-to-left call by value, here is what happens when evaluating:
chirpy false
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
The result is 20.
So all in all, world is printed, hello is printed, and 20 is returned.
Assuming call by name and that the arguments of + are evaluated from left to right, here is what happens when evaluating:
chirpy false
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
The result is 20.
So all in all, world is printed twice and 20 is returned.
Assuming call by name and that the arguments of + are evaluated from right to left, here is what happens when evaluating:
chirpy false
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
The result is 20.
So all in all, world is printed twice and 20 is returned.
Assuming call by need and that the arguments of + are evaluated from left to right, here is what happens when evaluating:
chirpy false
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
The result is 20.
So all in all, world is printed once and 20 is returned.
Assuming call by need and that the arguments of + are evaluated from right to left, here is what happens when evaluating:
chirpy false
(let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
The result is 20.
So all in all, world is printed once and 20 is returned.
Consider the following function declaration in the syntax of OCaml:
let jolly x y =
if x >= 0
then x
else x + y;;
The goal of this exercise is to determine what happens when the following two expressions are evaluated in a variety of evaluation orders:
jolly (let () = Printf.printf "hello\n" in 1)
(let () = Printf.printf "world\n" in 10)
jolly (let () = Printf.printf "hello\n" in -1)
(let () = Printf.printf "world\n" in 10)
(Hint: look at the solution for Exercise 1.)
Assuming left-to-right call by value, what happens when the first expression (the one with 1) is evaluated?
Justify your answer.
Assuming right-to-left call by value, what happens when the first expression (the one with 1) is evaluated?
Justify your answer.
Assuming call by name and that the arguments of + are evaluated from left to right, what happens when the first expression (the one with 1) is evaluated?
Justify your answer.
Assuming call by name and that the arguments of + are evaluated from right to left, what happens when the first expression (the one with 1) is evaluated?
Justify your answer.
Assuming call by need and that the arguments of + are evaluated from left to right, what happens when the first expression (the one with 1) is evaluated?
Justify your answer.
Assuming call by need and that the arguments of + are evaluated from right to left, what happens when the first expression (the one with 1) is evaluated?
Justify your answer.
Assuming left-to-right call by value, what happens when the second expression (the one with -1) is evaluated?
Justify your answer.
Assuming right-to-left call by value, what happens when the second expression (the one with -1) is evaluated?
Justify your answer.
Assuming call by name and that the arguments of + are evaluated from left to right, what happens when the second expression (the one with -1) is evaluated?
Justify your answer.
Assuming call by name and that the arguments of + are evaluated from right to left, what happens when the second expression (the one with -1) is evaluated?
Justify your answer.
Assuming call by need and that the arguments of + are evaluated from left to right, what happens when the second expression (the one with -1) is evaluated?
Justify your answer.
Assuming call by need and that the arguments of + are evaluated from right to left, what happens when the second expression (the one with -1) is evaluated?
Justify your answer.
Fixed a typo in the narrative, thanks to Yunjeong Lee’s eagle eye [11 Jun 2019]
Created [28 Mar 2019]