Recent from talks
Nothing was collected or created yet.
Evaluation strategy
View on Wikipedia
| Evaluation strategies |
|---|
In a programming language, an evaluation strategy is a set of rules for evaluating expressions.[1] The term is often used to refer to the more specific notion of a parameter-passing strategy[2] that defines the kind of value that is passed to the function for each parameter (the binding strategy)[3] and whether to evaluate the parameters of a function call, and if so in what order (the evaluation order).[4] The notion of reduction strategy is distinct,[5] although some authors conflate the two terms and the definition of each term is not widely agreed upon.[6] A programming language's evaluation strategy is part of its high-level semantics. Some languages, such as PureScript, have variants with different evaluation strategies. Some declarative languages, such as Datalog, support multiple evaluation strategies.
Just like in mathematics, evaluation is the process of finding the value corresponding to an expression.[7][8]
The calling convention consists of the low-level platform-specific details of parameter passing.
Example
[edit]To illustrate, executing a function call f(a, b) may first evaluate the arguments a and b, store the results in references or memory locations ref_a and ref_b, then evaluate the function's body with those references passed in. This gives the function the ability to look up the original argument values passed in through dereferencing the parameters (some languages use specific operators to perform this), to modify them via assignment as if they were local variables, and to return values via the references. This is the call-by-reference evaluation strategy.[9]
Table
[edit]This is a table of evaluation strategies and representative languages by year introduced. The representative languages are listed in chronological order, starting with the language(s) that introduced the strategy and followed by prominent languages that use the strategy.[10]: 434
| Evaluation strategy | Representative languages | Year first introduced |
|---|---|---|
| Call by reference | Fortran II, PL/I | 1958 |
| Call by value | ALGOL, C,[11] Scheme, MATLAB[12] | 1960 |
| Call by name | ALGOL 60, Simula | 1960 |
| Call by copy-restore | Fortran IV, Ada[13] | 1962 |
| Call by unification | Prolog | 1965[14][15] |
| Call by need | SASL,[16] Haskell, R[17] | 1971[18] |
| Call by sharing | CLU, Java, Python, Ruby, Julia | 1974[19] |
| Call by reference parameters | C++, PHP,[20] C#,[21] Visual Basic .NET[22] | 1985[23] |
| Call by reference to const | C++, C | 1985[23] |
Evaluation orders
[edit]While the order of operations defines the abstract syntax tree of the expression, the evaluation order defines the order in which expressions are evaluated. For example, the Python program
def f(x):
print(x, end="")
return x
print(f(1) + f(2))
outputs 123 due to Python's left-to-right evaluation order, but a similar program in OCaml:
let f x = print_int x; x ;;
print_int (f 1 + f 2)
outputs 213 due to OCaml's right-to-left evaluation order.
The evaluation order is mainly visible in code with side effects, but it also affects the performance of the code because a rigid order inhibits instruction scheduling. For this reason language standards such as C++ traditionally left the order unspecified, although languages such as Java and C# define the evaluation order as left-to-right[10]: 240–241 and the C++17 standard has added constraints on the evaluation order.[24]
Strict evaluation
[edit]Applicative order is a family of evaluation orders in which a function's arguments are evaluated completely before the function is applied. [25] This has the effect of making the function strict, i.e. the function's result is undefined if any of the arguments are undefined, so applicative order evaluation is more commonly called strict evaluation. Furthermore, a function call is performed as soon as it is encountered in a procedure, so it is also called eager evaluation or greedy evaluation.[26][27] Some authors refer to strict evaluation as "call by value" due to the call-by-value binding strategy requiring strict evaluation.[4]
Common Lisp, Eiffel and Java evaluate function arguments left-to-right. C leaves the order undefined.[28] Scheme requires the execution order to be the sequential execution of an unspecified permutation of the arguments.[29] OCaml similarly leaves the order unspecified, but in practice evaluates arguments right-to-left due to the design of its abstract machine.[30] All of these are strict evaluation.
Non-strict evaluation
[edit]A non-strict evaluation order is an evaluation order that is not strict, that is, a function may return a result before all of its arguments are fully evaluated.[31]: 46–47 The prototypical example is normal order evaluation, which does not evaluate any of the arguments until they are needed in the body of the function.[32] Normal order evaluation has the property that it terminates without error whenever any other evaluation order would have terminated without error.[33] The name "normal order" comes from the lambda calculus, where normal order reduction will find a normal form if there is one (it is a "normalizing" reduction strategy).[34] Lazy evaluation is classified in this article as a binding technique rather than an evaluation order. But this distinction is not always followed and some authors define lazy evaluation as normal order evaluation or vice-versa,[25][35] or confuse non-strictness with lazy evaluation.[31]: 43–44
Boolean expressions in many languages use a form of non-strict evaluation called short-circuit evaluation, where evaluation evaluates the left expression but may skip the right expression if the result can be determined—for example, in a disjunctive expression (OR) where true is encountered, or in a conjunctive expression (AND) where false is encountered, and so forth.[35] Conditional expressions similarly use non-strict evaluation - only one of the branches is evaluated.[31]
Comparison of applicative order and normal order evaluation
[edit]With normal order evaluation, expressions containing an expensive computation, an error, or an infinite loop will be ignored if not needed,[4] allowing the specification of user-defined control flow constructs, a facility not available with applicative order evaluation. Normal order evaluation uses complex structures such as thunks for unevaluated expressions, compared to the call stack used in applicative order evaluation.[36] Normal order evaluation has historically had a lack of usable debugging tools due to its complexity.[37]
Strict binding strategies
[edit]Call by value
[edit]In call by value (or pass by value), the evaluated value of the argument expression is bound to the corresponding variable in the function (frequently by copying the value into a new memory region). If the function or procedure is able to assign values to its parameters, only its local variable is assigned—that is, anything passed into a function call is unchanged in the caller's scope when the function returns. For example, in Pascal, passing an array by value will cause the entire array to be copied, and any mutations to this array will be invisible to the caller:[38]
program Main;
uses crt;
procedure PrintArray(a: Array of integer);
var
i: Integer;
begin
for i := Low(a) to High(a) do
Write(a[i]);
WriteLn();
end;
Procedure Modify(Row : Array of integer);
begin
PrintArray(Row); // 123
Row[1] := 4;
PrintArray(Row); // 143
end;
Var
A : Array of integer;
begin
A := [1,2,3];
PrintArray(A); // 123
Modify(A);
PrintArray(A); // 123
end.
Semantic drift
[edit]Strictly speaking, under call by value, no operations performed by the called routine can be visible to the caller, other than as part of the return value.[19] This implies a form of purely functional programming in the implementation semantics. However, the circumlocution "call by value where the value is a reference" has become common in some languages, for example, the Java community.[39] Compared to traditional pass by value, the value which is passed is not a value as understood by the ordinary meaning of value, such as an integer that can be written as a literal, but an implementation-internal reference handle. Mutations to this reference handle are visible in the caller. Due to the visible mutation, this form of "call by value" is more properly referred to as call by sharing.[19]
In purely functional languages, values and data structures are immutable, so there is no possibility for a function to modify any of its arguments. As such, there is typically no semantic difference between passing by value and passing by reference or a pointer to the data structure, and implementations frequently use call by reference internally for the efficiency benefits. Nonetheless, these languages are typically described as call by value languages.
Call by reference
[edit]Call by reference (or pass by reference) is an evaluation strategy where a parameter is bound to an implicit reference to the variable used as argument, rather than a copy of its value. This typically means that the function can modify (i.e., assign to) the variable used as argument—something that will be seen by its caller. Call by reference can therefore be used to provide an additional channel of communication between the called function and the calling function. Pass by reference can significantly improve performance: calling a function with a many-megabyte structure as an argument does not have to copy the large structure, only the reference to the structure (which is generally a machine word and only a few bytes). However, a call-by-reference language makes it more difficult for a programmer to track the effects of a function call, and may introduce subtle bugs.
Due to variation in syntax, the difference between call by reference (where the reference type is implicit) and call by sharing (where the reference type is explicit) is often unclear on first glance. A simple litmus test is if it's possible to write a traditional swap(a, b) function in the language.[39] For example in Fortran:
program Main
implicit none
integer :: a = 1
integer :: b = 2
call Swap(a, b)
print *, a, b ! 2 1
contains
subroutine Swap(a, b)
integer, intent(inout) :: a, b
integer :: temp
temp = a
a = b
b = temp
end subroutine Swap
end program Main
Therefore, Fortran's inout intent implements call-by-reference; any variable can be implicitly converted to a reference handle. In contrast the closest one can get in Java is:
class Main {
static class Box {
int value;
public Box(int value) {
this.value = value;
}
}
static void swap(Box a, Box b) {
int temp = a.value;
a.value = b.value;
b.value = temp;
}
public static void main(String[] args) {
Box a = new Box(1);
Box b = new Box(2);
swap(a, b);
System.out.printf("a = %d, b = %d%n", a.value, b.value);
// output: a = 2, b = 1
}
}
where an explicit Box type must be used to introduce a handle. Java is call-by-sharing but not call-by-reference.[39]
Call by copy-restore
[edit]Call by copy-restore—also known as "copy-in copy-out", "call by value result", "call by value return" (as termed in the Fortran community)—is a variation of call by reference. With call by copy-restore, the contents of the argument are copied to a new variable local to the call invocation. The function may then modify this variable, similarly to call by reference, but as the variable is local, the modifications are not visible outside of the call invocation during the call. When the function call returns, the updated contents of this variable are copied back to overwrite the original argument ("restored").[40]
The semantics of call by copy-restore is similar in many cases to call by reference, but differs when two or more function arguments alias one another (i.e., point to the same variable in the caller's environment). Under call by reference, writing to one argument will affect the other during the function's execution. Under call by copy-restore, writing to one argument will not affect the other during the function's execution, but at the end of the call, the values of the two arguments may differ, and it is unclear which argument is copied back first and therefore what value the caller's variable receives.[41] For example, Ada specifies that the copy-out assignment for each in out or out parameter occurs in an arbitrary order.[42] From the following program (illegal in Ada 2012)[43] it can be seen that the behavior of GNAT is to copy in left-to-right order on return:
with Ada.Text_IO; use Ada.Text_IO;
procedure Test_Copy_Restore is
procedure Modify (A, B : in out Integer) is
begin
A := A + 1;
B := B + 2;
end Modify;
X : Integer := 0;
begin
Modify(X, X);
Put_Line("X = " & Integer'Image(X));
end Test_Copy_Restore;
-- $ gnatmake -gnatd.E test_copy_restore.adb; ./test_copy_restore
-- test_copy_restore.adb:12:10: warning: writable actual for "A" overlaps with actual for "B" [-gnatw.i]
-- X = 2
If the program returned 1 it would be copying right-to-left, and under call by reference semantics the program would return 3.
When the reference is passed to the caller uninitialized (for example an out parameter in Ada as opposed to an in out parameter), this evaluation strategy may be called "call by result".
This strategy has gained attention in multiprocessing and remote procedure calls,[44] as unlike call-by-reference it does not require frequent communication between threads of execution for variable access.
Call by sharing
[edit]Call by sharing (also known as "pass by sharing", "call by object", or "call by object-sharing") is an evaluation strategy that is intermediate between call by value and call by reference. Rather than every variable being exposed as a reference, only a specific class of values, termed "references", "boxed types", or "objects", have reference semantics, and it is the addresses of these pointers that are passed into the function. Like call by value, the value of the address passed is a copy, and direct assignment to the parameter of the function overwrites the copy and is not visible to the calling function. Like call by reference, mutating the target of the pointer is visible to the calling function. Mutations of a mutable object within the function are visible to the caller because the object is not copied or cloned—it is shared, hence the name "call by sharing".[19]
The technique was first noted by Barbara Liskov in 1974 for the CLU language.[19] It is used by many modern languages such as Python (the shared values being called "objects"),[45] Java (objects), Ruby (objects), JavaScript (objects), Scheme (data structures such as vectors),[46] AppleScript (lists, records, dates, and script objects), OCaml and ML (references, records, arrays, objects, and other compound data types), Maple (rtables and tables), and Tcl (objects).[47] The term "call by sharing" as used in this article is not in common use; the terminology is inconsistent across different sources. For example, in the Java community, they say that Java is call by value.[39]
For immutable objects, there is no real difference between call by sharing and call by value, except if object identity is visible in the language. The use of call by sharing with mutable objects is an alternative to input/output parameters: the parameter is not assigned to (the argument is not overwritten and object identity is not changed), but the object (argument) is mutated.[48]
For example, in Python, lists are mutable and passed with call by sharing, so:
def f(a_list):
a_list.append(1)
m = []
f(m)
print(m)
outputs [1] because the append method modifies the object on which it is called.
In contrast, assignments within a function are not noticeable to the caller. For example, this code binds the formal argument to a new object, but it is not visible to the caller because it does not mutate a_list:
def f(a_list):
a_list = a_list + [1]
print(a_list) # [1]
m = []
f(m)
print(m) # []
Call by address
[edit]Call by address, pass by address, or call/pass by pointer is a parameter passing method where the address of the argument is passed as the formal parameter. Inside the function, the address (pointer) may be used to access or modify the value of the argument. For example, the swap operation can be implemented as follows in C:[49]
#include <stdio.h>
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int a = 1;
int b = 2;
swap(&a, &b);
printf("%d %d\n", a, b); // 2 1
return 0;
}
Some authors treat & as part of the syntax of calling swap. Under this view, C supports the call-by-reference parameter passing strategy.[50] Other authors take a differing view that the presented implementation of swap in C is only a simulation of call-by-reference using pointers.[51] Under this "simulation" view, mutable variables in C are not first-class (that is, l-values are not expressions), rather pointer types are. In this view, the presented swap program is syntactic sugar for a program that uses pointers throughout,[52] for example this program (read and assign have been added to highlight the similarities to the Java Box call-by-sharing program above):
#include <stdio.h>
int read(int* p) {
return *p;
}
void assign(int* p, int v) {
*p = v;
}
void swap(int* a, int* b) {
int temp_storage;
int* temp = &temp_storage;
assign(temp, read(a));
assign(a, read(b));
assign(b, read(temp));
}
int main() {
int a_storage;
int* a = &a_storage;
int b_storage;
int* b = &b_storage;
assign(a, 1);
assign(b, 2);
swap(a, b);
printf("%d %d\n", read(a), read(b)); // 2 1
return 0;
}
Because in this program, swap operates on pointers and cannot change the pointers themselves, but only the values the pointers point to, this view holds that C's main evaluation strategy is more similar to call-by-sharing.
C++ confuses the issue further by allowing swap to be declared and used with a very lightweight "reference" syntax:[53]
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int a = 1;
int b = 2;
swap(a, b);
std::println("{} {}", a, b); // 2 1
return 0;
}
Semantically, this is equivalent to the C examples. As such, many authors consider call-by-address to be a unique parameter passing strategy distinct from call-by-value, call-by-reference, and call-by-sharing.
Call by unification
[edit]In logic programming, the evaluation of an expression may simply correspond to the unification of the terms involved combined with the application of some form of resolution. Unification must be classified as a strict binding strategy because it is fully performed. However, unification can also be performed on unbounded variables, so calls may not necessarily commit to final values for all its variables.
Non-strict binding strategies
[edit]Call by name
[edit]Call by name is an evaluation strategy where the arguments to a function are not evaluated before the function is called—rather, they are substituted directly into the function body (using capture-avoiding substitution) and then left to be evaluated whenever they appear in the function. If an argument is not used in the function body, the argument is never evaluated; if it is used several times, it is re-evaluated each time it appears. (See Jensen's device for a programming technique that exploits this.)
Call-by-name evaluation is occasionally preferable to call-by-value evaluation. If a function's argument is not used in the function, call by name will save time by not evaluating the argument, whereas call by value will evaluate it regardless. If the argument is a non-terminating computation, the advantage is enormous. However, when the function argument is used, call by name is often slower, requiring a mechanism such as a thunk.
.NET languages can simulate call by name using delegates or Expression<T> parameters. The latter results in an abstract syntax tree being given to the function. Eiffel provides agents, which represent an operation to be evaluated when needed. Java programs can accomplish similar lazy evaluation using lambda expressions and the java.util.function.Supplier<T> interface.
Call by need
[edit]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.
Haskell is a well-known language that uses call-by-need evaluation. Because evaluation of expressions may happen arbitrarily far into a computation, Haskell supports only side effects (such as mutation) via the use of monads. This eliminates any unexpected behavior from variables whose values change prior to their delayed evaluation.
In R's implementation of call by need, all arguments are passed, meaning that R allows arbitrary side effects.
Lazy evaluation is the most common implementation of call-by-need semantics, but variations like optimistic evaluation exist. .NET languages implement call by need using the type Lazy<T>.
Graph reduction is an efficient implementation of lazy evaluation.
Call by macro expansion
[edit]Call by macro expansion is similar to call by name, but uses textual substitution rather than capture-avoiding substitution. Macro substitution may therefore result in variable capture, leading to mistakes and undesired behavior. Hygienic macros avoid this problem by checking for and replacing shadowed variables that are not parameters.
Call by future
[edit]"Call by future", also known as "parallel call by name" or "lenient evaluation",[54] is a concurrent evaluation strategy combining non-strict semantics with eager evaluation. The method requires fine-grained dynamic scheduling and synchronization but is suitable for massively parallel machines.
The strategy creates a future (promise) for the function's body and each of its arguments. These futures are computed concurrently with the flow of the rest of the program. When a future A requires the value of another future B that has not yet been computed, future A blocks until future B finishes computing and has a value. If future B has already finished computing the value is returned immediately. Conditionals block until their condition is evaluated, and lambdas do not create futures until they are fully applied.[55]
If implemented with processes or threads, creating a future will spawn one or more new processes or threads (for the promises), accessing the value will synchronize these with the main thread, and terminating the computation of the future corresponds to killing the promises computing its value. If implemented with a coroutine, as in .NET async/await, creating a future calls a coroutine (an async function), which may yield to the caller, and in turn be yielded back to when the value is used, cooperatively multitasking.
The strategy is non-deterministic, as the evaluation can occur at any time between creation of the future (i.e., when the expression is given) and use of the future's value. The strategy is non-strict because the function body may return a value before the arguments are evaluated. However, in most implementations, execution may still get stuck evaluating an unneeded argument. For example, the program
f x = 1/x
g y = 1
main = print (g (f 0))
may either have g finish before f, and output 1, or may result in an error due to evaluating 1/0.[31]
Call-by-future is similar to call by need in that values are computed only once. With careful handling of errors and nontermination, in particular terminating futures partway through if it is determined they will not be needed, call-by-future also has the same termination properties as call-by-need evaluation.[55] However, call-by-future may perform unnecessary speculative work compared to call-by-need, such as deeply evaluating a lazy data structure.[31] This can be avoided by using lazy futures that do not start computation until it is certain the value is needed.
Optimistic evaluation
[edit]Optimistic evaluation is a call-by-need variant where the function's argument is partly evaluated in a call-by-value style for some amount of time (which may be adjusted at runtime). After that time has passed, evaluation is aborted and the function is applied using call by need.[56] This approach avoids some of call-by-need's runtime expenses while retaining desired termination characteristics.
See also
[edit]References
[edit]This article includes a list of general references, but it lacks sufficient corresponding inline citations. (April 2012) |
- ^ Araki, Shota; Nishizaki, Shin-ya (November 2014). "Call-by-name evaluation of RPC and RMI calculi". Theory and Practice of Computation. p. 1. doi:10.1142/9789814612883_0001. ISBN 978-981-4612-87-6. Retrieved 21 August 2021.
- ^ Turbak, Franklyn; Gifford, David (18 July 2008). Design Concepts in Programming Languages. MIT Press. p. 309. ISBN 978-0-262-30315-6.
- ^ Crank, Erik; Felleisen, Matthias (1991). "Parameter-passing and the lambda calculus". Proceedings of the 18th ACM SIGPLAN-SIGACT symposium on Principles of programming languages - POPL '91. p. 2. CiteSeerX 10.1.1.23.4385. doi:10.1145/99583.99616. ISBN 0897914198. S2CID 5782416.
- ^ a b c Wilhelm, Reinhard; Seidl, Helmut (10 November 2010). Compiler Design: Virtual Machines. Springer Science & Business Media. p. 61. ISBN 978-3-642-14909-2.
- ^ Nita, Stefania Loredana; Mihailescu, Marius (2017). "Introduction". Practical Concurrent Haskell. p. 3. doi:10.1007/978-1-4842-2781-7_1. ISBN 978-1-4842-2780-0.
- ^ Pierce, Benjamin C. (2002). Types and Programming Languages. MIT Press. p. 56. ISBN 0-262-16209-1.
- ^ "Evaluate (v.), sense a". Oxford English Dictionary. 2023. doi:10.1093/OED/3423541985.
Mathematics. To work out the 'value' of (a quantitative expression); to find a numerical expression for (any quantitative fact or relation).
- ^ "Simplify (v.), sense 4.a". Oxford English Dictionary. 2023. doi:10.1093/OED/1018661347.
To express (an equation or other mathematical expression) in a form that is easier to understand, analyse, or work with, e.g. by collecting like terms or substituting variables.
- ^ Daniel P. Friedman; Mitchell Wand (2008). Essentials of Programming Languages (third ed.). Cambridge, MA: The MIT Press. ISBN 978-0262062794.
- ^ a b Scott, Michael Lee (2016). Programming language pragmatics (Fourth ed.). Waltham, MA: Elsevier. ISBN 9780124104778.
- ^ Kernighan, Brian W.; Ritchie, Dennis M. (1988). The C programming language (2nd ed.). Englewood Cliffs, N.J: Prentice Hall. p. 28. ISBN 978-0131103627.
- ^ "Avoid Unnecessary Copies of Data - MATLAB & Simulink". www.mathworks.com. Retrieved 2023-01-28.
- ^ Hasti, Rebecca. "Parameter Passing". CS 536: Introduction to Programming Languages and Compilers. University of Wisconsin. Retrieved 22 August 2021.
- ^ J.A. Robinson (Jan 1965). "A Machine-Oriented Logic Based on the Resolution Principle". Journal of the ACM. 12 (1): 23–41. doi:10.1145/321250.321253. S2CID 14389185.; Here: sect.5.8, p.32
- ^ J.A. Robinson (1971). "Computational logic: The unification computation". Machine Intelligence. 6: 63–72.
- ^ Bundy, Alan; Wallen, Lincoln (1984). "SASL". Catalogue of Artificial Intelligence Tools. p. 117. doi:10.1007/978-3-642-96868-6_222. ISBN 978-3-540-13938-6.
Was probably the first language to systematically exploit the power of lazy evaluation.
- ^ Fay, Colin (30 July 2018). "About lazy evaluation". R-bloggers. Retrieved 21 August 2021.
- ^ Wadsworth, Christopher P. (1971). Semantics and Pragmatics of the Lambda Calculus (PhD). Oxford University.
- ^ a b c d e Liskov, Barbara; Atkinson, Russ; Bloom, Toby; Moss, Eliot; Schaffert, Craig; Scheifler, Craig; Snyder, Alan (October 1979). "CLU Reference Manual" (PDF). Laboratory for Computer Science. Massachusetts Institute of Technology. pp. 14–15. Archived (PDF) from the original on 2006-09-22. Retrieved 2011-05-19.
- ^ "PHP: Passing by Reference - Manual". www.php.net. Retrieved 2021-07-04.
- ^ Wagner, Bill (12 April 2023). "Passing Parameters - C# Programming Guide". Microsoft Docs. Retrieved 2023-09-10.
- ^ Dollard, Kathleen (15 September 2021). "Passing Arguments by Value and by Reference - Visual Basic". Microsoft Docs. Retrieved 2023-09-10.
- ^ a b "History of C++". en.cppreference.com. Retrieved 11 June 2022.
- ^ Filipek, Bartlomiej (16 August 2021). "Stricter Expression Evaluation Order in C++17". C++ Stories. Retrieved 24 August 2021.
- ^ a b Abelson, Harold; Sussman, Gerald Jay (1996). "Normal Order and Applicative Order". Structure and interpretation of computer programs (2nd ed.). Cambridge, Massachusetts: MIT Press. ISBN 0-262-01153-0. Archived from the original on 2005-03-02. Retrieved 2006-03-06. See also footnote Temp 576.
- ^ Reese, Richard M. (14 October 2015). Learning Java Functional Programming. Packt Publishing Ltd. p. 106. ISBN 978-1-78528-935-4.
- ^ Antani, Ved; Timms, Simon; Mantyla, Dan (31 August 2016). JavaScript: Functional Programming for JavaScript Developers. Packt Publishing Ltd. p. 614. ISBN 978-1-78712-557-5.
- ^ Seacord, Robert C. "EXP30-C. Do not depend on the order of evaluation for side effects". SEI CERT C Coding Standard. Carnegie Mellon University. Retrieved 23 August 2021.
- ^ Anglade, S.; Lacrampe, J. J.; Queinnec, C. (October 1994). "Semantics of combinations in scheme" (PDF). ACM SIGPLAN Lisp Pointers. VII (4): 15–20. doi:10.1145/382109.382669. S2CID 2987427.
- ^ "Why are OCaml function arguments evaluated right-to-left?". OCaml. 30 November 2017.
- ^ a b c d e Tremblay, G. (April 2000). "Lenient evaluation is neither strict nor lazy". Computer Languages. 26 (1): 43–66. CiteSeerX 10.1.1.137.9885. doi:10.1016/S0096-0551(01)00006-6.
- ^ George, Lai (March 1987). Efficient evaluation of normal order through strictness information (MSc). University of Utah. p. 10.
- ^ Borning, Alan (Autumn 1999). "Applicative vs Normal Order Evaluation in Functional Languages" (PDF). CSE 505: Concepts of Programming Languages. University of Washington. Retrieved 23 August 2021.
- ^ Mazzola, Guerino; Milmeister, Gérard; Weissmann, Jody (21 October 2004). Comprehensive Mathematics for Computer Scientists 2. Springer Science & Business Media. p. 323. ISBN 978-3-540-20861-7.
- ^ a b Sturm, Oliver (11 April 2011). Functional Programming in C#: Classic Programming Techniques for Modern Projects. John Wiley and Sons. p. 91. ISBN 978-0-470-74458-1.
- ^ Marlow, Simon. "Why can't I get a stack trace?". Haskell Implementors Workshop 2012. Retrieved 25 August 2021.
- ^ Nilsson, Henrik (1999). "Tracing piece by piece: Affordable debugging for lazy functional languages". Proceedings of the fourth ACM SIGPLAN international conference on Functional programming. pp. 36–47. CiteSeerX 10.1.1.451.6513. doi:10.1145/317636.317782. ISBN 1581131119. S2CID 13954359.
- ^ "Open array parameters". www.freepascal.org. Retrieved 20 January 2024.
- ^ a b c d "Java is Pass-by-Value, Dammit!". 16 May 2001. Retrieved 2016-12-24.
- ^ Coenen, Frans. "PARAMETER PASSING". cgi.csc.liv.ac.uk. Retrieved 22 January 2024.
- ^ "Call by Reference, Aliasing Issues" (PDF). MPRI Course 2-36-1: Proof of Program (Lecture notes). p. 53. Archived from the original (PDF) on 25 June 2024.
- ^ Ada 2022 Language Reference Manual (PDF). 13 October 2023. p. 215.
- ^ Barnes, John (2013). Ada 2012 rationale: the language, the standard libraries (PDF). Heidelberg: Springer. pp. 15–16, 87–88. ISBN 978-3-642-45210-9.
- ^ Thurlow, Robert (May 2009). "RPC: Remote Procedure Call Protocol Specification Version 2". tools.ietf.org. IETF. Retrieved 7 April 2018.
- ^ Lundh, Fredrik. "Call by Object". Effbot.org. Archived from the original on 2011-05-19. Retrieved 2011-05-19.
- ^ Jones, Rhys Price (2010). "Is Scheme call-by-value?". CS 145 Programming Languages Lab 9: Parameter Passing. George Washington University. Archived from the original on 16 October 2014. Retrieved 20 January 2024.
- ^ "Tcl Library Procedures - Tcl_Obj manual page". www.tcl.tk.
- ^ "CA1021: Avoid out parameters". Microsoft. 15 November 2016.
- ^ Leo, Ray (November 1996). Little C++ (Made Easy). LeoSudo Inc. pp. 79–80. ISBN 978-0-9654634-1-6.
- ^ Dandamudi, Sivarama P. (15 July 2005). Guide to Assembly Language Programming in Linux. Springer Science & Business Media. p. 232. ISBN 978-0-387-25897-3.
- ^ Srivastava, S. K.; Srivastava, Deepali (6 June 2018). C in Depth. BPB Publications. p. 206. ISBN 978-93-87284-94-4.
- ^ "Mutable Variables and Reference Types". okmij.org. Retrieved 20 January 2024.
- ^ Vermeir, Dirk (28 June 2011). Multi-Paradigm Programming using C++. Springer Science & Business Media. pp. 10–11. ISBN 978-1-4471-0311-0.
- ^ McCollin, Thomas Gwynfryn; Morell, Tobias. "A Game of Paradigms: A Usability Study of Functional Idioms in Gameplay Programming" (PDF). Aalborg University. p. 6. Retrieved 11 January 2022.
- ^ a b Schauser, Klaus E.; Goldstein, Seth C. (1995). "How much non-strictness do lenient programs require?" (PDF). Proceedings of the seventh international conference on Functional programming languages and computer architecture - FPCA '95. pp. 216–225. doi:10.1145/224164.224208. ISBN 0897917197. S2CID 2045943. Retrieved 7 January 2022.
- ^ Ennals, Robert; Peyton Jones, Simon (August 2003). Optimistic Evaluation: a fast evaluation strategy for non-strict programs. International Conference on Functional Programming. ACM Press. Archived from the original on 22 August 2025.
Further reading
[edit]- Baker-Finch, Clem; King, David; Hall, Jon; Trinder, Phil (1999-03-10). "An Operational Semantics for Parallel Call-by-Need" (ps). Research Report. 99 (1). Faculty of Mathematics & Computing, The Open University.
- Ennals, Robert; Peyton Jones, Simon (August 2003). Optimistic Evaluation: a fast evaluation strategy for non-strict programs (PDF). International Conference on Functional Programming. ACM Press. Archived from the original (PDF) on 22 August 2025.
- Ludäscher, Bertram (2001-01-24). "CSE 130 lecture notes". CSE 130: Programming Languages: Principles & Paradigms.
- Pierce, Benjamin C. (2002). Types and Programming Languages. MIT Press. ISBN 0-262-16209-1.
- Sestoft, Peter (2002). "Demonstrating Lambda Calculus Reduction" (PDF). In Mogensen, T; Schmidt, D; Sudborough, I. H. (eds.). The Essence of Computation: Complexity, Analysis, Transformation. Essays Dedicated to Neil D. Jones. Lecture Notes in Computer Science. Vol. 2566. Springer-Verlag. pp. 420–435. ISBN 3-540-00326-6.
- "Call by Value and Call by Reference in C Programming". Call by Value and Call by Reference in C Programming explained. Archived from the original on 2013-01-21.
External links
[edit]- The interactive on-line Geometry of Interaction visualiser, implementing a graph-based machine for several common evaluation strategies.
Evaluation strategy
View on GrokipediaIntroduction
Definition and overview
In a programming language, an evaluation strategy specifies the rules governing the order and timing of expression evaluation, as well as the binding of arguments to formal parameters during computation.[5] This encompasses decisions on whether and when subexpressions are reduced to values before or during function application.[6] Evaluation strategies consist of two primary components: the evaluation order, which dictates when arguments to a function are computed (e.g., before or after entering the function body), and the binding mechanism, which determines how argument values or expressions are associated with and stored for parameters (e.g., by copying values or sharing references).[5] Key terminology includes eager evaluation (also called strict evaluation), where arguments are fully evaluated prior to function application, aligning with applicative order; and lazy evaluation (non-strict evaluation), where evaluation is postponed until the argument's value is actually required, corresponding to normal order.[7] Strict evaluation is the default in most imperative languages like C and Java, while non-strict evaluation appears in functional languages such as Haskell.[5] These strategies profoundly influence program behavior and performance. Eager evaluation can improve efficiency for computations where all arguments are needed but may lead to non-termination if an argument diverges (e.g., an infinite loop) even if unused, and it executes side effects immediately, which is critical in imperative languages for predictable sequencing.[7] Lazy evaluation enhances efficiency by skipping unneeded computations—useful in declarative languages for infinite data structures or conditional expressions—and can ensure termination in cases where eager evaluation fails, but it complicates side-effect management and debugging due to deferred execution.[5] Overall, the choice balances resource use, correctness amid side effects, and expressiveness between imperative and declarative paradigms. To illustrate evaluation points, consider this pseudocode for a simple function call:function add(x, y)
return x + y
result = add(computeExp1(), computeExp2())
function add(x, y)
return x + y
result = add(computeExp1(), computeExp2())
computeExp1() and computeExp2() are evaluated to values before the add body executes. In lazy (normal-order) evaluation, these expressions are bound unevaluated and computed only when referenced in the addition.[7]
Historical development
The foundations of evaluation strategies in programming languages trace back to Alonzo Church's development of lambda calculus in the 1930s, where normal order evaluation—reducing the leftmost outermost redex first—emerged as a core reduction strategy for computing functions on symbolic expressions. This approach influenced subsequent language designs by prioritizing substitution without premature argument evaluation, laying groundwork for non-strict paradigms. In the mid-20th century, early imperative languages adopted strict evaluation models to align with hardware constraints and predictable execution. Fortran, released in 1957, implemented strict eager evaluation through its call-by-reference mechanism, evaluating arguments fully before passing addresses, which facilitated efficient numerical computations but limited expressiveness for unevaluated forms.[8] Similarly, ALGOL 60 (1960) introduced applicative order evaluation (call-by-value) for functions, where arguments are reduced to values prior to application, alongside call-by-name as a non-strict alternative; ALGOL 58 (1958) had used a substitution model akin to call-by-name.[9] John McCarthy's seminal 1960 paper on Lisp established applicative order as the default evaluation strategy, diverging from lambda calculus's normal order by evaluating function arguments before application to enable practical implementation on early computers, though it acknowledged the theoretical appeal of normal order for avoiding redundant computations.[10] This choice influenced many subsequent languages, but terminology began to shift; McCarthy's precise distinction between applicative (eager) and normal (lazy-like) orders evolved in later designs, where "lazy evaluation" came to denote delayed computation with memoization, distinct from pure substitution. Key advancements in non-strict strategies appeared in the 1970s, with lazy evaluation proposed for Lisp dialects through techniques like streams and indefinite data structures, as explored in works by Friedman and Wise, enabling infinite lists without immediate computation.[11] Icon, developed in 1977, introduced call-by-need elements via its goal-directed evaluation and generators, which compute values on demand during backtracking searches, bridging imperative and functional styles.[12] The rise of functional programming paradigms in the 1980s amplified non-strict strategies for modularity and expressiveness. David Turner's Miranda (1985) adopted lazy evaluation as standard, treating functions as first-class citizens and delaying reductions until needed, which inspired broader adoption.[13] This culminated in the 1990s with Haskell's release (version 1.0 in 1990), which formalized non-strict semantics—implemented via lazy evaluation—to support pure functional programming, equational reasoning, and handling infinite data structures efficiently.[14]Illustrative Examples
Basic evaluation example
To illustrate the impact of evaluation strategies on program execution, consider a simple function that increments its argument by one, defined in abstract syntax as:inc(x) = x + 1
inc(x) = x + 1
inc(if cond then expensive() else 0)
inc(if cond then expensive() else 0)
cond evaluates to false, expensive() represents a computationally intensive operation (such as a recursive computation that could loop indefinitely if invoked), and the if construct selects between the branches based on cond.
Under strict evaluation (also known as applicative order), the argument to inc is fully evaluated before the function body is entered. The evaluation proceeds as follows:
- Evaluate the operator
inc, yielding the increment function. - Evaluate the argument
if cond then expensive() else 0: First, evaluatecond(false), but sinceifis treated as a regular function application in pure applicative order, both branches may be evaluated prior to selection in some models; however, the key point is that the else branch (0) is selected without needingexpensive(), yet in strict regimes without special-casing conditionals, unnecessary evaluation of the then branch could occur if not optimized. - To highlight the difference starkly, adapt to a test-like construct where the conditional is the function itself, as in the classic example: Define
test(x, y) = if (= x 0) then 0 else y, and calltest(0, expensive()). In applicative order, evaluatexto 0 andytoexpensive()before applyingtest, triggering the full computation ofexpensive()(potentially infinite loop) even though the result would be 0.
expensive() despite the conditional rendering it unnecessary.
In contrast, under non-strict evaluation (normal order), arguments are not evaluated until their values are required during function application (detailed further in the Evaluation Orders section). The trace is:
- Substitute the unevaluated argument into
inc:inc(if cond then expensive() else 0). - Evaluate the body of
inc: Compute(if cond then expensive() else 0) + 1. - Evaluate the conditional:
condis false, so select the else branch (0) without ever invokingexpensive(). - Compute
0 + 1, yielding 1.
Comparison table
| Strategy | Evaluation Timing | Memory Usage | Side-Effect Handling | Termination Guarantees | Example Languages |
|---|---|---|---|---|---|
| Call by Value | Eager evaluation of arguments before function call. | Copies the value, requiring additional space for the local copy. | No side effects on actual parameters; modifications affect only the local copy. | Strict: May fail to terminate if an argument expression diverges, even if the result would terminate without it. | C, Java (primitives) [15] [16] |
| Call by Reference | Arguments evaluated before call; passes location. | Shares memory location; no extra copy. | Allows modification of actual parameters through aliases. | Strict: Similar to call by value; evaluates arguments eagerly. | Fortran, C++ (references), Ada (in out) [15] [17] |
| Call by Copy-Restore | Copy in before call; copy out on return. | Temporary local copy during execution. | Updates actual on return; potential issues with aliases. | Strict: Evaluates arguments eagerly. | Ada (for some modes) [15] [17] |
| Call by Sharing | Passes reference to the object at call time. | Shares the reference; no deep copy for structures. | Modifications affect the shared object. | Strict: Evaluates arguments before passing reference. | Java (objects), Lisp [17] [18] |
| Call by Name | Lazy: reevaluates argument expression each use. | Thunk storage for delayed evaluation; potential multiple computations. | Side effects occur each reevaluation; can affect actuals. | Non-strict: Terminates whenever a terminating reduction strategy does; avoids evaluating unused args. | Algol 60 [15] [16] [17] |
| Call by Need | Lazy: evaluates argument once on first use, memos result. | Stores thunk and result after first evaluation; avoids recomputation. | Typically read-only after evaluation; no multiple side effects. | Non-strict: Like call by name, but more efficient; terminates if possible. | Haskell [15] [16] |
f(x) where x is an argument:
Call by Value (C-like):
int f(int x) { return x * 2; }
int main() { int a = 3; f(a); } // a remains 3
int f(int x) { return x * 2; }
int main() { int a = 3; f(a); } // a remains 3
procedure f(expr x); ... end;
f(a + b); // x reevaluated as a + b each time used
procedure f(expr x); ... end;
f(a + b); // x reevaluated as a + b each time used
f x = x * 2
let y = undefined in f y -- errors only if y used, but memoized if used once
```[](https://www.cs.fsu.edu/~lacher/courses/COP4020/fall10/lectures/Subroutines.pdf)[](https://www.nyu.edu/classes/jcf/CSCI-GA.2110-001/slides/session4/Subprograms.pdf)
## Evaluation Orders
### Strict evaluation
Strict evaluation, also known as applicative-order evaluation, is a [strategy](/page/Strategy) in which all arguments to a function are fully evaluated to their normal form before the function body is applied. This approach ensures that subexpressions are reduced to values prior to any combination or substitution in [lambda calculus](/page/Lambda_calculus) terms.[](https://www.macs.hw.ac.uk/~greg/books/gjm.lambook88.pdf) In programming languages, it aligns with eager evaluation, where computations occur immediately upon encountering an expression.
Mechanically, strict evaluation proceeds by selecting the leftmost innermost redex for reduction, evaluating [arguments](/page/Argument) from the inside out before applying the outer function. This outer-to-inner order contrasts with strategies that delay [argument](/page/Argument) computation, prioritizing complete resolution of inputs to avoid partial applications. In [lambda calculus](/page/Lambda_calculus), for an application $ M \, N $, $ N $ is reduced to a value before substituting into $ M $. A classic illustration is the identity function applied to a diverging term:
$$
(\lambda x. x) \, \Omega
$$
where $ \Omega = (\lambda x. x \, x) \, (\lambda x. x \, x) $. Under strict evaluation, $ \Omega $ is fully evaluated first, resulting in non-termination as it enters an infinite self-application loop.[](https://www.cs.cornell.edu/courses/cs6110/2016sp/lectures/lecture04.pdf)
One key advantage of strict evaluation is its predictability, particularly for side effects, as all computations occur in a defined sequence, facilitating debugging and reasoning about program behavior. It is the default in most imperative languages, such as C and Java, where immediate evaluation supports efficient resource management for terminating expressions.[](https://www.macs.hw.ac.uk/~greg/books/gjm.lambook88.pdf) However, a significant disadvantage arises with non-terminating arguments: even if the function does not require the argument's value, strict evaluation will attempt to compute it, potentially leading to infinite loops or errors in cases like conditionals with diverging false branches. This can cause programs that would terminate under delayed evaluation to diverge entirely. Strict evaluation is often realized through binding strategies like call by value, which enforce this eager semantics.
### Non-strict evaluation
Non-strict evaluation, also referred to as normal-order evaluation, is a reduction strategy in [lambda calculus](/page/Lambda_calculus) and [functional programming](/page/Functional_programming) where the leftmost outermost beta-redex (reducible expression) is reduced first, delaying the evaluation of arguments until they are actually needed.[](https://www.cs.cornell.edu/courses/cs6110/2018sp/lectures/lec04.pdf) This approach contrasts with strict strategies by prioritizing the overall term structure over immediate argument computation, ensuring that if [a normal form](/page/A-normal_form) exists, the reduction sequence will reach it due to the standardization theorem.[](https://www.cs.yale.edu/flint/trifonov/cs430/notes/17.pdf) In practice, arguments are represented as thunks—suspended computations that encapsulate unevaluated expressions—and are only forced when their values are demanded during the execution of the function body.[](https://www.brics.dk/RS/97/7/BRICS-RS-97-7.pdf)
The mechanics of non-strict evaluation focus on achieving head-normal form, where the term is reduced until the outermost [lambda](/page/Lambda) abstraction is exposed, with inner expressions evaluated on demand as they contribute to the result. This on-demand evaluation occurs lazily, meaning subexpressions not required for the final output are never computed, which is particularly useful in functional languages for constructing and manipulating infinite data structures like streams or lists without immediate termination.[](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/history.pdf) For instance, in [Haskell](/page/Haskell), this strategy enables concise definitions of algorithms that process only the necessary portions of potentially unbounded data.[](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/history.pdf)
One key advantage of non-strict [evaluation](/page/Evaluation) is its ability to avoid superfluous computations, thereby improving [efficiency](/page/Efficiency) in scenarios where arguments may not influence the outcome, such as conditional expressions or higher-order functions. It is foundational in purely functional languages, facilitating modular and composable code without the need for explicit control over evaluation timing.[](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/history.pdf) However, it introduces overhead from thunk creation and management, which can increase memory usage through accumulated suspensions and complicate performance prediction.[](https://www.brics.dk/RS/97/7/BRICS-RS-97-7.pdf) Additionally, if the language permits side effects, delayed evaluation can lead to non-intuitive ordering, making [debugging](/page/Debugging) more challenging.[](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/history.pdf)
A illustrative example highlights the terminating behavior of non-strict evaluation: consider the lambda term $(\lambda x.\ 42)\ \Omega$, where $\Omega = (\lambda x.\ x\ x)(\lambda x.\ x\ x)$ is a diverging expression that loops indefinitely under reduction. In non-strict evaluation, the outer redex reduces first to 42, bypassing the evaluation of $\Omega$ entirely, whereas a strict strategy would diverge by attempting to evaluate the argument beforehand.[](https://www.cs.tufts.edu/comp/150AVS/03-lambda.pdf) This demonstrates how non-strict evaluation can yield results in cases where strict evaluation fails. Non-strict evaluation is often implemented via binding strategies like call-by-name, which provide the necessary delay mechanism.[](https://www.brics.dk/RS/97/7/BRICS-RS-97-7.pdf)
### Comparison of applicative and normal order
Applicative-order evaluation, also known as strict evaluation, evaluates the arguments to a function before applying the function itself, focusing on the innermost redex (reducible expression) first.[](https://web.mit.edu/6.001/6.037/sicp.pdf) In contrast, [normal-order evaluation](/page/Normal_order), a non-strict approach, applies the function first and evaluates arguments only when they are actually needed, prioritizing the outermost (leftmost) redex.[](https://web.mit.edu/6.001/6.037/sicp.pdf) This difference in timing—pre-application for applicative order versus post-application for normal order—fundamentally affects how expressions are reduced in [lambda calculus](/page/Lambda_calculus) and [functional programming](/page/Functional_programming) languages.[](https://www.cs.cornell.edu/courses/cs6110/2016sp/lectures/lecture04.pdf)
Regarding termination, applicative order can fail to terminate if any argument diverges, as it evaluates all arguments regardless of whether they are used; for instance, applying a function to a non-terminating expression like `(define (p) (p))` will loop infinitely before the function body executes.[](https://web.mit.edu/6.001/6.037/sicp.pdf) [Normal order](/page/Normal_order), however, can achieve termination in such cases by deferring evaluation of unused arguments, ensuring convergence to [a normal form](/page/A-normal_form) if one exists.[](https://www.cs.cornell.edu/courses/cs6110/2016sp/lectures/lecture04.pdf) This property makes [normal order](/page/Normal_order) particularly advantageous for expressions involving conditionals where branches may not be taken.[](https://files.core.ac.uk/download/pdf/354675334.pdf)
Efficiency trade-offs arise from these strategies: applicative order avoids the overhead of creating thunks (delayed computations) and prevents redundant evaluations of the same argument, but it wastes effort on unused computations.[](https://people.csail.mit.edu/jastr/6001/fall07/r26.pdf) [Normal order](/page/Normal_order) saves work by skipping unnecessary evaluations but may recompute arguments multiple times without memoization, leading to higher computational cost in some scenarios; for example, computing `(gcd 206 40)` requires 18 remainder operations under [normal order](/page/Normal_order) but only 4 under applicative order.[](https://web.mit.edu/6.001/6.037/sicp.pdf)
Side effects further highlight the contrasts, as applicative order executes all argument side effects upfront, potentially performing more operations than needed, while [normal order](/page/Normal_order) defers them, which can alter the order or multiplicity of execution unpredictably.[](https://web.mit.edu/6.001/6.037/sicp.pdf) In languages with mutable state, this deferral in [normal order](/page/Normal_order) complicates reasoning about program behavior.[](https://files.core.ac.uk/download/pdf/354675334.pdf)
To illustrate, consider a conditional function `test` defined as `(define (test x y) (if (= x 0) 0 y))` applied to `(test 0 (display "side effect"))`, where the second argument produces a [side effect](/page/Side_effect) (printing). Under applicative order, the [side effect](/page/Side_effect) executes immediately before the conditional, printing regardless of the branch. Under [normal order](/page/Normal_order), the [side effect](/page/Side_effect) is deferred and skipped since the true branch is taken, avoiding the print.
**Applicative-order trace:**
1. Evaluate arguments: `x` to 0, `y` to (display "[side effect](/page/Side_effect)") — prints "[side effect](/page/Side_effect)".
2. Apply `if (= 0 0) 0 y` → returns 0.
**Normal-order trace:**
1. Substitute into body: `if (= 0 0) 0 (display "[side effect](/page/Side_effect)")`.
2. Evaluate condition: true.
3. Return 0 — no evaluation of `y`, so no print.
This example demonstrates how [normal order](/page/Normal_order) avoids unnecessary [side effect](/page/Side_effect)s, while applicative order incurs them.[](https://web.mit.edu/6.001/6.037/sicp.pdf)
## Strict Binding Strategies
### Call by value
In call by value, each argument expression is fully evaluated to produce a value, which is then copied and bound to the corresponding formal [parameter](/page/Parameter) as a [local variable](/page/Local_variable) before execution of the procedure or function body begins. This mechanism treats the parameter as an independent entity initialized with the argument's value, ensuring that the binding is strict and occurs prior to any side effects within the called procedure.[](https://www.softwarepreservation.org/projects/ALGOL/report/Algol60_report_CACM_1960_June.pdf)[](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf)
A key property of call by value is the absence of [aliasing](/page/Aliasing) for mutable state, as modifications to the [parameter](/page/Parameter) do not propagate back to the original argument or its associated data. Any side effects arising from the [evaluation](/page/Evaluation) of the argument expression, such as function calls or increments, are realized before the procedure body executes, promoting predictable behavior in strict [evaluation](/page/Evaluation) contexts.[](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf)[](https://www.cs.tufts.edu/~nr/cs257/archive/john-mccarthy/recursive.pdf)
This strategy is prevalent in imperative languages like C and Java. In C, arguments are passed by value, with the order of evaluation unspecified but a sequence point ensuring completion before the function body starts; Java similarly passes copies of primitive values or object references by value, where reassigning the parameter does not alter the caller's reference.[](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf)[](https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html)
The term "call by value" originated in the [ALGOL 60](/page/ALGOL_60) report, where it specified explicit value assignment to formal parameters declared with the `value` keyword. In contrast, John McCarthy's 1960 Lisp design used applicative-order evaluation—evaluating arguments to normal form before [function application](/page/Function_application), effectively passing evaluated expressions or closures in a functional sense—rather than the imperative notion of value copying that became standard in later languages like ALGOL 60 derivatives. This evolution shifted the semantics from reduction strategies in expression-based systems to parameter binding via duplication in block-structured procedural languages.[](https://www.pls-lab.org/Call-by-value)[](https://www.cs.tufts.edu/~nr/cs257/archive/john-mccarthy/recursive.pdf)[](https://www.softwarepreservation.org/projects/ALGOL/report/Algol60_report_CACM_1960_June.pdf)
The following C example illustrates that mutation of the parameter does not affect the original argument:
```c
#include <stdio.h>
void modify(int param) {
param = 10; // Modifies local copy only
}
int main() {
int arg = 5;
modify(arg);
printf("arg = %d\n", arg); // Outputs: arg = 5
return 0;
}
f x = x * 2
let y = undefined in f y -- errors only if y used, but memoized if used once
```[](https://www.cs.fsu.edu/~lacher/courses/COP4020/fall10/lectures/Subroutines.pdf)[](https://www.nyu.edu/classes/jcf/CSCI-GA.2110-001/slides/session4/Subprograms.pdf)
## Evaluation Orders
### Strict evaluation
Strict evaluation, also known as applicative-order evaluation, is a [strategy](/page/Strategy) in which all arguments to a function are fully evaluated to their normal form before the function body is applied. This approach ensures that subexpressions are reduced to values prior to any combination or substitution in [lambda calculus](/page/Lambda_calculus) terms.[](https://www.macs.hw.ac.uk/~greg/books/gjm.lambook88.pdf) In programming languages, it aligns with eager evaluation, where computations occur immediately upon encountering an expression.
Mechanically, strict evaluation proceeds by selecting the leftmost innermost redex for reduction, evaluating [arguments](/page/Argument) from the inside out before applying the outer function. This outer-to-inner order contrasts with strategies that delay [argument](/page/Argument) computation, prioritizing complete resolution of inputs to avoid partial applications. In [lambda calculus](/page/Lambda_calculus), for an application $ M \, N $, $ N $ is reduced to a value before substituting into $ M $. A classic illustration is the identity function applied to a diverging term:
$$
(\lambda x. x) \, \Omega
$$
where $ \Omega = (\lambda x. x \, x) \, (\lambda x. x \, x) $. Under strict evaluation, $ \Omega $ is fully evaluated first, resulting in non-termination as it enters an infinite self-application loop.[](https://www.cs.cornell.edu/courses/cs6110/2016sp/lectures/lecture04.pdf)
One key advantage of strict evaluation is its predictability, particularly for side effects, as all computations occur in a defined sequence, facilitating debugging and reasoning about program behavior. It is the default in most imperative languages, such as C and Java, where immediate evaluation supports efficient resource management for terminating expressions.[](https://www.macs.hw.ac.uk/~greg/books/gjm.lambook88.pdf) However, a significant disadvantage arises with non-terminating arguments: even if the function does not require the argument's value, strict evaluation will attempt to compute it, potentially leading to infinite loops or errors in cases like conditionals with diverging false branches. This can cause programs that would terminate under delayed evaluation to diverge entirely. Strict evaluation is often realized through binding strategies like call by value, which enforce this eager semantics.
### Non-strict evaluation
Non-strict evaluation, also referred to as normal-order evaluation, is a reduction strategy in [lambda calculus](/page/Lambda_calculus) and [functional programming](/page/Functional_programming) where the leftmost outermost beta-redex (reducible expression) is reduced first, delaying the evaluation of arguments until they are actually needed.[](https://www.cs.cornell.edu/courses/cs6110/2018sp/lectures/lec04.pdf) This approach contrasts with strict strategies by prioritizing the overall term structure over immediate argument computation, ensuring that if [a normal form](/page/A-normal_form) exists, the reduction sequence will reach it due to the standardization theorem.[](https://www.cs.yale.edu/flint/trifonov/cs430/notes/17.pdf) In practice, arguments are represented as thunks—suspended computations that encapsulate unevaluated expressions—and are only forced when their values are demanded during the execution of the function body.[](https://www.brics.dk/RS/97/7/BRICS-RS-97-7.pdf)
The mechanics of non-strict evaluation focus on achieving head-normal form, where the term is reduced until the outermost [lambda](/page/Lambda) abstraction is exposed, with inner expressions evaluated on demand as they contribute to the result. This on-demand evaluation occurs lazily, meaning subexpressions not required for the final output are never computed, which is particularly useful in functional languages for constructing and manipulating infinite data structures like streams or lists without immediate termination.[](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/history.pdf) For instance, in [Haskell](/page/Haskell), this strategy enables concise definitions of algorithms that process only the necessary portions of potentially unbounded data.[](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/history.pdf)
One key advantage of non-strict [evaluation](/page/Evaluation) is its ability to avoid superfluous computations, thereby improving [efficiency](/page/Efficiency) in scenarios where arguments may not influence the outcome, such as conditional expressions or higher-order functions. It is foundational in purely functional languages, facilitating modular and composable code without the need for explicit control over evaluation timing.[](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/history.pdf) However, it introduces overhead from thunk creation and management, which can increase memory usage through accumulated suspensions and complicate performance prediction.[](https://www.brics.dk/RS/97/7/BRICS-RS-97-7.pdf) Additionally, if the language permits side effects, delayed evaluation can lead to non-intuitive ordering, making [debugging](/page/Debugging) more challenging.[](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/07/history.pdf)
A illustrative example highlights the terminating behavior of non-strict evaluation: consider the lambda term $(\lambda x.\ 42)\ \Omega$, where $\Omega = (\lambda x.\ x\ x)(\lambda x.\ x\ x)$ is a diverging expression that loops indefinitely under reduction. In non-strict evaluation, the outer redex reduces first to 42, bypassing the evaluation of $\Omega$ entirely, whereas a strict strategy would diverge by attempting to evaluate the argument beforehand.[](https://www.cs.tufts.edu/comp/150AVS/03-lambda.pdf) This demonstrates how non-strict evaluation can yield results in cases where strict evaluation fails. Non-strict evaluation is often implemented via binding strategies like call-by-name, which provide the necessary delay mechanism.[](https://www.brics.dk/RS/97/7/BRICS-RS-97-7.pdf)
### Comparison of applicative and normal order
Applicative-order evaluation, also known as strict evaluation, evaluates the arguments to a function before applying the function itself, focusing on the innermost redex (reducible expression) first.[](https://web.mit.edu/6.001/6.037/sicp.pdf) In contrast, [normal-order evaluation](/page/Normal_order), a non-strict approach, applies the function first and evaluates arguments only when they are actually needed, prioritizing the outermost (leftmost) redex.[](https://web.mit.edu/6.001/6.037/sicp.pdf) This difference in timing—pre-application for applicative order versus post-application for normal order—fundamentally affects how expressions are reduced in [lambda calculus](/page/Lambda_calculus) and [functional programming](/page/Functional_programming) languages.[](https://www.cs.cornell.edu/courses/cs6110/2016sp/lectures/lecture04.pdf)
Regarding termination, applicative order can fail to terminate if any argument diverges, as it evaluates all arguments regardless of whether they are used; for instance, applying a function to a non-terminating expression like `(define (p) (p))` will loop infinitely before the function body executes.[](https://web.mit.edu/6.001/6.037/sicp.pdf) [Normal order](/page/Normal_order), however, can achieve termination in such cases by deferring evaluation of unused arguments, ensuring convergence to [a normal form](/page/A-normal_form) if one exists.[](https://www.cs.cornell.edu/courses/cs6110/2016sp/lectures/lecture04.pdf) This property makes [normal order](/page/Normal_order) particularly advantageous for expressions involving conditionals where branches may not be taken.[](https://files.core.ac.uk/download/pdf/354675334.pdf)
Efficiency trade-offs arise from these strategies: applicative order avoids the overhead of creating thunks (delayed computations) and prevents redundant evaluations of the same argument, but it wastes effort on unused computations.[](https://people.csail.mit.edu/jastr/6001/fall07/r26.pdf) [Normal order](/page/Normal_order) saves work by skipping unnecessary evaluations but may recompute arguments multiple times without memoization, leading to higher computational cost in some scenarios; for example, computing `(gcd 206 40)` requires 18 remainder operations under [normal order](/page/Normal_order) but only 4 under applicative order.[](https://web.mit.edu/6.001/6.037/sicp.pdf)
Side effects further highlight the contrasts, as applicative order executes all argument side effects upfront, potentially performing more operations than needed, while [normal order](/page/Normal_order) defers them, which can alter the order or multiplicity of execution unpredictably.[](https://web.mit.edu/6.001/6.037/sicp.pdf) In languages with mutable state, this deferral in [normal order](/page/Normal_order) complicates reasoning about program behavior.[](https://files.core.ac.uk/download/pdf/354675334.pdf)
To illustrate, consider a conditional function `test` defined as `(define (test x y) (if (= x 0) 0 y))` applied to `(test 0 (display "side effect"))`, where the second argument produces a [side effect](/page/Side_effect) (printing). Under applicative order, the [side effect](/page/Side_effect) executes immediately before the conditional, printing regardless of the branch. Under [normal order](/page/Normal_order), the [side effect](/page/Side_effect) is deferred and skipped since the true branch is taken, avoiding the print.
**Applicative-order trace:**
1. Evaluate arguments: `x` to 0, `y` to (display "[side effect](/page/Side_effect)") — prints "[side effect](/page/Side_effect)".
2. Apply `if (= 0 0) 0 y` → returns 0.
**Normal-order trace:**
1. Substitute into body: `if (= 0 0) 0 (display "[side effect](/page/Side_effect)")`.
2. Evaluate condition: true.
3. Return 0 — no evaluation of `y`, so no print.
This example demonstrates how [normal order](/page/Normal_order) avoids unnecessary [side effect](/page/Side_effect)s, while applicative order incurs them.[](https://web.mit.edu/6.001/6.037/sicp.pdf)
## Strict Binding Strategies
### Call by value
In call by value, each argument expression is fully evaluated to produce a value, which is then copied and bound to the corresponding formal [parameter](/page/Parameter) as a [local variable](/page/Local_variable) before execution of the procedure or function body begins. This mechanism treats the parameter as an independent entity initialized with the argument's value, ensuring that the binding is strict and occurs prior to any side effects within the called procedure.[](https://www.softwarepreservation.org/projects/ALGOL/report/Algol60_report_CACM_1960_June.pdf)[](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf)
A key property of call by value is the absence of [aliasing](/page/Aliasing) for mutable state, as modifications to the [parameter](/page/Parameter) do not propagate back to the original argument or its associated data. Any side effects arising from the [evaluation](/page/Evaluation) of the argument expression, such as function calls or increments, are realized before the procedure body executes, promoting predictable behavior in strict [evaluation](/page/Evaluation) contexts.[](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf)[](https://www.cs.tufts.edu/~nr/cs257/archive/john-mccarthy/recursive.pdf)
This strategy is prevalent in imperative languages like C and Java. In C, arguments are passed by value, with the order of evaluation unspecified but a sequence point ensuring completion before the function body starts; Java similarly passes copies of primitive values or object references by value, where reassigning the parameter does not alter the caller's reference.[](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf)[](https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html)
The term "call by value" originated in the [ALGOL 60](/page/ALGOL_60) report, where it specified explicit value assignment to formal parameters declared with the `value` keyword. In contrast, John McCarthy's 1960 Lisp design used applicative-order evaluation—evaluating arguments to normal form before [function application](/page/Function_application), effectively passing evaluated expressions or closures in a functional sense—rather than the imperative notion of value copying that became standard in later languages like ALGOL 60 derivatives. This evolution shifted the semantics from reduction strategies in expression-based systems to parameter binding via duplication in block-structured procedural languages.[](https://www.pls-lab.org/Call-by-value)[](https://www.cs.tufts.edu/~nr/cs257/archive/john-mccarthy/recursive.pdf)[](https://www.softwarepreservation.org/projects/ALGOL/report/Algol60_report_CACM_1960_June.pdf)
The following C example illustrates that mutation of the parameter does not affect the original argument:
```c
#include <stdio.h>
void modify(int param) {
param = 10; // Modifies local copy only
}
int main() {
int arg = 5;
modify(arg);
printf("arg = %d\n", arg); // Outputs: arg = 5
return 0;
}
param receives a copy of arg's value, and changes remain isolated.[19]
Call by reference
Call by reference is a parameter-passing mechanism in which the formal parameter is bound to the memory address (L-value) of the actual argument, allowing the subroutine to access and modify the original data directly without copying.[20][21] This approach is typically used in strict evaluation contexts, where arguments are fully evaluated prior to the binding.[22] One key advantage of call by reference is its efficiency when passing large data structures, such as arrays or objects, since only the address is transmitted rather than duplicating the entire content.[23] However, this shared access introduces aliasing, where multiple identifiers refer to the same memory location, potentially enabling side effects that alter the caller's data unexpectedly during execution.[22] Such properties make it suitable for operations requiring in-place modifications but demand careful management to avoid non-local effects. In languages like Pascal, call by reference is explicitly invoked using thevar keyword for formal parameters, ensuring the address of the argument is passed and changes propagate back to the caller.[24][25] Similarly, C++ supports this through reference types, declared with the & symbol, which act as aliases to the original variables and differ from pointers by being non-nullable and non-resignable, promoting safer usage without explicit dereferencing.[26]
Despite these benefits, call by reference can lead to issues such as unintended modifications via aliasing, which complicate program reasoning and debugging, as changes in one part of the code may unexpectedly affect distant variables.[27] Scoping problems may also arise if the lifetime of the referenced data ends before the subroutine completes, potentially causing dangling references or undefined behavior.[22]
For illustration, consider this C++ example where a function increments a variable passed by reference:
#include <iostream>
void increment(int& x) {
x += 1;
}
int main() {
int value = 5;
increment(value);
std::cout << value << std::endl; // Outputs: 6
return 0;
}
#include <iostream>
void increment(int& x) {
x += 1;
}
int main() {
int value = 5;
increment(value);
std::cout << value << std::endl; // Outputs: 6
return 0;
}
value in main because x aliases its address. A similar effect occurs in Pascal with var:
program Example;
var
value: integer;
procedure Increment(var x: integer);
begin
x := x + 1;
end;
begin
value := 5;
Increment(value);
writeln(value); // Outputs: 6
end.
program Example;
var
value: integer;
procedure Increment(var x: integer);
begin
x := x + 1;
end;
begin
value := 5;
Increment(value);
writeln(value); // Outputs: 6
end.
Call by copy-restore
Call by copy-restore, also known as copy-in copy-out or value-result, is a strict parameter-passing mechanism in which the value of an argument is copied into a local parameter upon entry to the subroutine, and any modifications to that parameter are then copied back to the original argument upon exit.[28] This process occurs as part of eager evaluation, ensuring arguments are fully evaluated before the subroutine executes.[23] The mechanism simulates pass-by-result semantics, allowing subroutines to update caller variables without direct sharing during execution, which can be useful in languages lacking native reference passing to mimic modifiable outputs.[28] It has been used in languages like Ada forin out parameters, though early Fortran versions primarily employed call-by-reference; value-result became more prominent in later standards and other languages.[23]
A key advantage of call by copy-restore is that it prevents aliasing issues within the subroutine, as the parameter operates on an independent copy, avoiding unintended side effects from simultaneous modifications—unlike bidirectional sharing—while still enabling one-way updates to the original argument on return.[28]
For example, consider a pseudocode subroutine that updates a specific element in an array:
SUBROUTINE UpdateArray(A, index, newValue)
// Copy-in: Create local copy of A
LOCAL_ARRAY = COPY(A)
// Modify the local copy
LOCAL_ARRAY[index] = newValue
// Copy-out: Restore changes to original A
A = COPY(LOCAL_ARRAY)
END SUBROUTINE
SUBROUTINE UpdateArray(A, index, newValue)
// Copy-in: Create local copy of A
LOCAL_ARRAY = COPY(A)
// Modify the local copy
LOCAL_ARRAY[index] = newValue
// Copy-out: Restore changes to original A
A = COPY(LOCAL_ARRAY)
END SUBROUTINE
UpdateArray(myArray, 5, 42) evaluates myArray and copies it locally before modification, then copies the updated local array back to myArray upon completion, ensuring the caller's array reflects the change without exposing the original to aliasing during execution.[28]
Call by sharing
Call by sharing is an evaluation strategy where the caller and the called procedure share access to the same argument object by passing a reference to it, without duplicating the object's contents. This approach was introduced in the CLU programming language to enable efficient passing of complex data structures while maintaining object integrity.[29] In practice, the argument is fully evaluated prior to the call, and a reference to the resulting object—typically treated as a value itself—is bound to the formal parameter, often via a shallow copy of the reference for efficiency.[29] This strategy strikes a balance between call by value and call by reference by passing the reference by value, which prevents rebinding the parameter to a different object but allows in-place mutations to propagate back to the caller.[30] It is particularly prevalent in object-oriented languages for handling mutable objects, such as lists or arrays in Python and Ruby, where the object's mutability enables shared modifications without the overhead of deep copying.[31] In Python, for instance, all arguments are passed this way, as the language treats everything as an object and passes references to those objects. Modern Fortran (since Fortran 90) supports call-by-value via the VALUE attribute for scalar arguments, providing a strict alternative to its default reference passing.[31][32] The shared access in call by sharing can introduce side effects, as mutations to the object within the procedure affect the original argument in the caller's scope, potentially leading to unintended aliasing behaviors if programmers assume independent copies.[31] To illustrate, consider the following Python example where a list is passed to a function that appends an element:def modify_list(lst):
lst.append(42) # Modifies the shared object
my_list = [1, 2, 3]
modify_list(my_list)
print(my_list) # Outputs: [1, 2, 3, 42]
def modify_list(lst):
lst.append(42) # Modifies the shared object
my_list = [1, 2, 3]
modify_list(my_list)
print(my_list) # Outputs: [1, 2, 3, 42]
lst and the argument my_list reference the same object.[31] Similar behavior occurs in Ruby for mutable objects like arrays, where passing an array allows the callee to alter its contents, affecting the caller's view.
Call by address
Call by address is a strict parameter-passing mechanism where the explicit memory address of the argument is passed to the function parameter, typically as a pointer value, requiring dereferencing within the function to access or modify the original data.[33] This approach evaluates the argument to obtain its address prior to the call, aligning with strict evaluation semantics. In practice, the function receives a copy of the address (consistent with call-by-value for the pointer itself), but dereferencing allows indirect mutation of the caller's data.[34] This mechanism provides low-level control over memory locations, enabling operations like dynamic allocation via functions such asmalloc in C, where pointers facilitate heap management without unnecessary data copying. However, it is inherently error-prone, as improper dereferencing can lead to segmentation faults, dangling pointers, or buffer overflows, demanding careful programmer oversight. Languages like C implement this through explicit pointer declarations (e.g., int *p), drawing from assembly language influences where direct address manipulation is routine for performance-critical code. PL/I also supports call by address as a default linkage convention, passing parameters by reference to their storage locations unless specified otherwise.[35]
The primary advantage of call by address lies in its efficiency for systems programming, where it minimizes overhead for large or complex data structures by avoiding full copies and supporting in-place modifications. For instance, pointer arithmetic allows traversing and altering arrays efficiently, which is essential for low-level tasks like operating system development or embedded systems. A representative example is a swap function in C that exchanges two integers without returning values:
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
swap(&x, &y);, where & yields the addresses of x and y, enabling the function to dereference and update them directly. Such patterns underscore its utility in resource-constrained environments, though they require robust error handling to mitigate risks.
Call by unification
Call by unification is a strict evaluation strategy employed in logic programming languages, where the arguments of a predicate are matched and bound to variables through a process of unification before the body of the predicate is executed.[36] This approach ensures that the terms are made identical via substitutions, enabling declarative specification of computations based on logical inference rather than imperative assignment.[37] The mechanics of call by unification involve applying an algorithm to find a most general unifier (mgu), which is a substitution that renders two terms syntactically identical while preserving their structure.[37] Unification occurs eagerly upon predicate invocation: the call's arguments are unified with the head of a clause, and if successful, the resulting substitution is extended to the clause's body for further evaluation. This substitution binds variables to terms, such as constants, variables, or complex structures, and is composed incrementally during resolution. The process supports the resolution principle, where unification facilitates matching between goals and facts or rules.[38] A key property of call by unification is its integration with backtracking, allowing the system to retract bindings and explore alternative clauses upon failure, which is essential for search-based problem solving in nondeterministic domains.[39] Variables are bound to ground terms or other variables, enabling flexible pattern matching without explicit control flow for binding propagation. This strategy is central to Prolog, where it forms the core mechanism for SLD-resolution (Selective Linear Definite clause resolution), driving the language's execution model since its inception in the early 1970s.[39] One advantage of call by unification is its support for declarative pattern matching, allowing programmers to specify what relations hold rather than how to compute them, which simplifies expressing logical queries and rules.[36] For example, in Prolog, the goalunify(X, [a|X], [a,b]). succeeds by unifying the second and third arguments, binding X to [b], as the list structures match under the substitution {X ← [b]}.[38]
Non-strict Binding Strategies
Call by name
Call by name is a non-strict evaluation strategy in which the argument expression passed to a function is not evaluated at the call site but is instead textually substituted for the corresponding formal parameter within the function body, with the substituted expression being evaluated anew each time it is referenced during execution.[40] This substitution occurs dynamically at runtime, treating the argument as unevaluated text that is inserted wherever the parameter appears, often enclosed in parentheses to preserve syntactic validity.[40] As a result, the argument's side effects, if any, may occur multiple times if the parameter is used more than once in the body.[41] The core property of call by name lies in its pure textual substitution semantics, which avoids the creation or storage of thunks—delayed computation wrappers—relying instead on direct re-substitution and re-evaluation without intermediate caching.[41] This approach aligns with normal-order reduction in lambda calculus, where outermost redexes are reduced first, but it implements this through syntactic replacement rather than graph-based optimization.[41] Call by name was formally introduced in the Revised Report on the Algorithmic Language ALGOL 60, where it is described as replacing "any formal parameter not quoted in the value list... throughout the procedure body, by the corresponding actual parameter."[40] It influenced the design of early Lisp implementations, where McCarthy incorporated a flexible variant resembling ALGOL's call by name through explicit evaluation control via theeval function, enabling delayed argument handling in symbolic expressions.[42]
A significant disadvantage of call by name is its potential for inefficient repeated evaluation, which can lead to exponential time complexity in cases where an expensive argument is referenced multiple times within the function body.[43] For instance, consider an ALGOL 60-style procedure that computes both the square and double of its parameter:
procedure square_plus_double(p);
begin
integer result;
result := p * p + 2 * p
end;
procedure square_plus_double(p);
begin
integer result;
result := p * p + 2 * p
end;
square_plus_double(fib(30)), where fib(n) is a naive recursive Fibonacci function requiring exponential time itself, the argument fib(30) would be fully recomputed three times—twice for p * p and once for 2 * p—resulting in approximately three times the work of a single evaluation, amplifying the overall cost exponentially with deeper recursion or more references.[44] This inefficiency arises because each substitution triggers independent evaluation without memoization, making call by name unsuitable for performance-critical applications involving costly computations.[45]
Call by need
Call by need is a non-strict evaluation strategy that delays the evaluation of function arguments until their values are required, while ensuring that each argument is computed at most once through memoization. In this approach, arguments are represented as thunks—suspended computations—that are evaluated on first demand and then cached, with the result shared across all subsequent uses to avoid redundant work. This mechanism combines the benefits of call by name's laziness with improved efficiency by preventing repeated evaluations of the same expression.[46] The strategy operates by substituting arguments with let-bindings in the lambda calculus, where evaluation occurs only in contexts that necessitate the value, such as primitive operations or case expressions. Once evaluated, the thunk is updated to hold the value, enabling sharing via graph structures in implementations.[46] Call by need exhibits lazy properties, meaning unevaluated arguments do not cause divergence unless demanded, while its caching provides efficiency gains over pure call by name. In practice, it is often realized through graph reduction techniques, where expressions form a directed acyclic graph (DAG) that is rewritten incrementally, preserving sharing and minimizing duplication during reduction.[46] Haskell implements call by need as its default evaluation strategy, where lazy evaluation is achieved through this memoized demand-driven approach.[47] One key advantage is the ability to handle infinite data structures without non-termination, as only the portions needed for the computation are evaluated.[47] For example, consider defining an infinite list of natural numbers in Haskell:naturals = 0 : map (+1) naturals. Accessing the first few elements, such as take 5 naturals, forces evaluation of only the initial segment [0,1,2,3,4], while the tail remains a thunk until further demand, preventing infinite computation.[47]
Call by macro expansion
Call by macro expansion is a non-strict evaluation strategy in which argument expressions are textually substituted into the body of a macro or function definition at compile time, with evaluation deferred until the expanded code is executed as part of the program. This static process resembles textual replacement, enabling code transformation without runtime overhead or dynamic thunk creation. As such, it achieves delayed evaluation through compile-time binding rather than runtime mechanisms. In Lisp, macros facilitate this strategy via definitions likedefmacro, where the macro receives unevaluated argument forms and generates a replacement expression that is inserted during compilation. The substitution occurs structurally on s-expressions, allowing arbitrary code generation while preserving the language's homoiconic nature. Lisp macros originated in early dialects like Lisp 1.5 and evolved significantly in MacLisp and Common Lisp, becoming essential for extending the language's syntax and creating domain-specific abstractions.[48] Lisp macros are generally non-hygienic, meaning they can suffer from variable capture issues where identifiers in the macro body might shadow or be shadowed by those in the calling context; programmers must handle this manually, often by generating fresh symbols with functions like gensym. For example, a macro defined as (defmacro when (condition &body body) (if ,condition (progn ,@body)))expands a call like(when (> x 0) (print x))to(if (> x 0) (progn (print x)))` at compile time, inlining the conditional logic and avoiding any function call overhead, but care must be taken to avoid capture in more complex cases.
In contrast, languages like Scheme implement hygienic macros, which automatically address variable capture by renaming identifiers in the macro body to fresh names during expansion and tracking syntactic environments to ensure local bindings do not accidentally shadow or be shadowed by those in the calling environment; this is achieved through algorithms that perform linear-time rewriting.[49]
The C preprocessor implements call by macro expansion through function-like macros, processed during translation phase 4, where the preprocessor scans for invocations, collects arguments as token sequences within matching parentheses, fully expands those arguments for further macros, and substitutes them textually into the replacement list without evaluating them. This purely lexical replacement treats parameters as placeholders, inserting the argument tokens verbatim while preserving source structure like whitespace and comments, followed by rescanning the result for additional expansions. An example is #define SQUARE(x) ((x) * (x)), which expands SQUARE(a + b) to ((a + b) * (a + b)), potentially recomputing a + b twice in the compiled code but resolving all substitutions statically. This approach, standardized in C since its early specifications, promotes efficiency by enabling inline code generation and eliminating indirect calls for simple operations.[50]
The primary advantages of call by macro expansion include enhanced code generation efficiency, as the compiler can optimize the fully expanded form, and reduced runtime costs from avoiding argument passing and evaluation delays. In both Lisp and C contexts, it supports powerful metaprogramming while remaining confined to compile-time processing.[48][50]
Call by future
Call by future is a non-strict evaluation strategy that integrates concurrency into argument passing by delegating the evaluation of each function argument to a separate future, or promise, which computes the value asynchronously in parallel with the function body. Upon invocation, the function receives placeholders for these futures rather than waiting for the arguments to evaluate, allowing the caller to proceed immediately without blocking. When the function body requires an argument's value, it accesses the future, which blocks the accessing thread only if the computation is incomplete; otherwise, the result is returned transparently. This mechanism, first formalized in Multilisp, ensures that futures replace themselves with their computed values upon completion, enabling seamless integration into sequential code while exposing parallelism.[51] The strategy's key properties include its support for concurrency through speculative, eager evaluation of arguments in parallel, combined with lazy demand-driven access that avoids unnecessary computations if values go unused. Unlike purely sequential laziness, call by future overlaps the evaluation of independent arguments, potentially reducing overall execution time in parallel environments, though it introduces non-determinism due to scheduling and potential blocking. It maintains referential transparency in functional settings by treating futures as first-class values, but requires careful handling of side effects to avoid races. This approach has been implemented in languages such as Multilisp, where thefuture construct spawns dedicated processes for each argument, and Alice ML, which uses futures for data-driven synchronization in a typed concurrent functional setting.[51][52]
A primary advantage of call by future is its ability to automatically overlap independent computations, maximizing processor utilization without explicit programmer intervention for parallelism. For instance, in Multilisp, implementations demonstrated near-linear speedup for tasks like Quicksort on multiprocessors, where futures for recursive sublist processing run concurrently across multiple levels of recursion.[51] In a parallel map operation, such as applying a function to each element of a list, call by future binds each application to a separate future, allowing all elements to compute simultaneously; the mapping thread then touches these futures as needed, blocking only for unfinished ones while proceeding with completed results. This is particularly effective for data-parallel workloads, as seen in Alice ML's thread-spawning primitives that create futures for concurrent list processing.[52] As a non-strict binding, it ensures arguments do not block the caller, deferring evaluation until demanded within the function.[51]
Optimistic evaluation
Optimistic evaluation is an adaptive strategy designed for non-strict functional programming languages, where expressions are speculatively evaluated eagerly at runtime, assuming their results will be needed, but with a mechanism to abort and rollback if the computation proves too expensive or unnecessary. This approach complements traditional lazy evaluation (such as call-by-need) by dynamically switching between lazy and eager modes for individual thunks—unevaluated expressions—based on runtime observations from an online profiler. The profiler monitors execution and adjusts speculation decisions, typically initializing with an eager bias and refining it to avoid wasteful computations.[53] In terms of mechanics, optimistic evaluation operates on let expressions, such aslet x = <rhs> in <body>, by introducing a runtime switch that determines whether to evaluate the right-hand side (<rhs>) speculatively using call-by-value semantics or to defer it lazily as a thunk. If speculation succeeds—meaning the value is used and the cost is low—the computation commits seamlessly, avoiding the overhead of thunk creation and blackholing associated with pure laziness. Upon detecting excessive cost (e.g., via time or space thresholds), the system aborts the speculative work by capturing the computation state into a continuation thunk, resuming the lazy strategy only if the value is later demanded. This rollback ensures semantic equivalence to standard non-strict evaluation while enabling eager optimizations. For infinite data structures, a variant called chunky evaluation speculatively computes fixed-size chunks (e.g., 10 elements) of lists, balancing speculation with controlled laziness to prevent unbounded eager expansion.[53][54]
The properties of optimistic evaluation center on its speculative nature, where commit occurs implicitly on successful use and abort handles failures gracefully, maintaining the non-strict guarantees of the language without introducing strictness errors. It introduces minimal overhead for speculation decisions, typically via lightweight runtime checks, and preserves referential transparency. This strategy is particularly effective in low-contention scenarios, such as programs where most thunks are eventually forced or are inexpensive to evaluate, yielding high throughput by reducing thunk allocation and forcing costs—key bottlenecks in lazy evaluation. Empirical results from implementations show average speedups of 5-25%, with up to 50% improvements in space-leak prone programs, and no program slowing by more than 15%.[53][55]
Optimistic evaluation was implemented experimentally in an early version of the Glasgow Haskell Compiler (GHC) for the Haskell language in 2003, requiring modifications to the compiler and runtime system to support speculation switches and abortion mechanisms. For example, consider a program that repeatedly sums elements from a large generated list; under pure lazy evaluation, thunks accumulate, risking space leaks, but optimistic evaluation speculatively computes list elements as needed, running in near-constant space by committing only useful results and aborting unused speculations. This makes it suitable for performance-critical non-strict code, adapting to workload characteristics without static analysis alone.[53]
Modern Applications and Variations
Evaluation in functional languages
In functional programming languages, evaluation strategies emphasize non-strict semantics to support higher-order functions, referential transparency, and composability, with lazy evaluation—specifically call-by-need—serving as the default in influential languages like Haskell.[56] This approach delays computation until a value is required, enabling the construction of infinite data structures and avoiding unnecessary work, while sharing computed results across multiple uses to optimize performance.[14] However, functional languages also provide mechanisms for strict evaluation to mitigate potential inefficiencies, such as Haskell'sseq function, which forces an argument to weak head normal form before proceeding, and bang patterns (!), which enforce strictness in pattern bindings.[57]
Implementations of these strategies in functional languages often rely on graph reduction techniques, where expressions are represented as directed graphs that are incrementally reduced to normal form. In the Glasgow Haskell Compiler (GHC), the dominant Haskell implementation, code is compiled to the Spineless Tagless G-machine (STG), a virtual machine that performs lazy graph reduction without explicit pointers, facilitating efficient handling of higher-order functions through closure representation and updateable references. This model supports lazy evaluation by constructing thunks—unevaluated expressions—that are shared and updated in place upon demand, reducing redundant computations in polymorphic and higher-order contexts.[14]
Key challenges in these implementations include space leaks arising from unevaluated thunks that accumulate in memory without being garbage-collected, potentially leading to excessive heap usage in long-running programs.[58] To address this, strictness analysis techniques determine whether a function requires its arguments to produce a defined result, allowing compilers to optimize by evaluating strict arguments eagerly and enabling parallelization or specialization.[59] Seminal work by Wright and Felleisen formalized strictness via abstract interpretation and type-based inference, providing a foundation for precise annotations that balance laziness with efficiency.
A representative example is the handling of infinite streams: in lazy languages like Haskell, streams can be defined recursively without termination issues, as in naturals = 0 : map (+1) naturals, where only demanded elements are computed, supporting applications like symbolic computation or event streams. In contrast, strict functional languages like Clean require explicit forcing for laziness via annotations or uniqueness typing, evaluating arguments eagerly by default to ensure predictable resource use but necessitating manual intervention for infinite structures.[60]
The evolution of evaluation strategies in functional languages traces from David Turner's Miranda in the 1980s, which popularized lazy evaluation through non-strict semantics and polymorphic types, influencing subsequent designs by enabling concise expressions for complex algorithms.[13] This laziness became central to Haskell's 1990 specification, prioritizing expressiveness over strictness. Modern languages like F#, however, adopt strict-by-default (eager) evaluation to align with imperative ecosystems, using explicit lazy keywords for deferred computation while maintaining functional purity.[61]
