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, which was the topic of Question 12 in the mini-project about the underlying determinism of OCaml:
(fun _ -> 42) (1 / 0)
What is the result of evaluating this expressions?
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 names 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, names denote values, i.e., the result of evaluating actual parameters (for functions) and definienses (for let-expressions).
Unfolding a function call in a call-by-value language requires the actual parameters to be evaluated first, as articulated in the section about The recipe to unfold the call to a function in Week 07. For example,
where the let-expression is strict (i.e., its definiens is evaluated before its body).
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, names denote suspended evaluations, i.e., the result of not evaluating actual parameters (for functions) and definienses (for let-expressions).
Each time a name is evaluated in the body of a function or of a let expression, the suspended evaluation is “thawed” and takes place.
A language which is call by name implements what is known as the copy rule in that when unfolding a function call, the actual parameters are substituted for the formal parameters (i.e., copied) in the body of the inlined function. For example,
In practice, the copy rule requires local renaming for names not to shadow other names. For example, (fun x y -> x + y) (y + 1) is unfolded into (fun y' -> (y + 1) + y'), where y has been renamed into y' in order not to shadow y in (y + 1).
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, no matter the argument of the function.
In a call-by-need language, names denote suspended evaluations, i.e., the result of not evaluating actual parameters (for functions) and definienses (for let-expressions).
The first time a name 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 name be evaluated again.
In practice, unfolding a call requires to be mindful of the context. For example:
Memoization has been achieved by changing the context.
The introductory example, i.e., (fun _ -> 42) (1 / 0), 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. Standard ML, for example, is such a programming language.
A programming language is right-to-left call by value if, in an application, the function is evaluated after the actual parameter. As investigated in Question 02 in the midterm mini-project, OCaml is such a programming language.
In Scheme, procedure arguments are evaluated before procedures are applied. As to whether sub-expressions in an application are evaluated from left to right, or from right to left, or in any given order, that is left unspecified.
Ditto in C for function arguments.
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.
Edsger Dijkstra (sententiously): Progress is possible only if we train ourselves to think about programs without thinking of them as pieces of executable code.
Rick Blaine: Not so fast, Louie.
Halcyon: Where is Mimer?Gad Elmaleh (dutifully): Mi-mer is in the kit-chen.
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 01.)
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.
Halcyon (candidly): In denotational semantics, is the evaluation order of the meta-language call by name or call by value?
{John Reynolds && Peter Landin} (simultaneously): Call by {name && value} of course.
A second of eternity passes, and the universe resumes its course.
Richard Bird (mildly): Call by name enables one to reason about one’s programs equationally.
Mitchell Wand (sotto voce): I prefer call by value because it is so much more predictable.