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]
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. [7]
Evaluation strategy is part of the semantics of the programming language definition. Some languages, such as PureScript, have variants with different evaluation strategies. Some declarative languages, such as Datalog, support multiple evaluation strategies. Some languages define a calling convention.[ clarification needed ]
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. [8] : 434
Evaluation strategy | Representative languages | Year first introduced |
---|---|---|
Call by reference | Fortran II, PL/I | 1958 |
Call by value | ALGOL, C, Scheme, MATLAB [9] | 1960 |
Call by name | ALGOL 60, Simula | 1960 |
Call by copy-restore | Fortran IV, Ada [10] | 1962 |
Call by unification | Prolog | 1965 [11] [12] |
Call by need | SASL, [13] Haskell, R [14] | 1971 [15] |
Call by sharing | CLU, Java, Python, Ruby, Julia | 1974 [16] |
Call by reference parameters | C++, PHP, [17] C#, [18] Visual Basic .NET [19] | 1985 [20] |
Call by reference to const | C++, C | 1985 [20] |
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
deff(x):print(x,end='')returnxprint(f(1)+f(2),end='')
outputs 123
due to Python's left-to-right evaluation order, but a similar program in OCaml:
letfx=print_intx;x;;print_int(f1+f2)
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 [8] : 240–241 and the C++17 standard has added constraints on the evaluation order. [21]
Applicative order is a family of evaluation orders in which a function's arguments are evaluated completely before the function is applied. [22] 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. [23] [24] 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. [25] Scheme requires the execution order to be the sequential execution of an unspecified permutation of the arguments. [26] OCaml similarly leaves the order unspecified, but in practice evaluates arguments right-to-left due to the design of its abstract machine. [27] All of these are strict evaluation.
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. [28] : 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. [29] Normal order evaluation has the property that it terminates without error whenever any other evaluation order would have terminated without error. [30] 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). [31] 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, [22] [32] or confuse non-strictness with lazy evaluation. [28] : 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. [32] Conditional expressions similarly use non-strict evaluation - only one of the branches is evaluated. [28]
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. [33] Normal order evaluation has historically had a lack of usable debugging tools due to its complexity. [34]
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: [35]
programMain;usescrt;procedurePrintArray(a:Arrayofinteger);vari:Integer;beginfori:=Low(a)toHigh(a)doWrite(a[i]);WriteLn();end;ProcedureModify(Row:Arrayofinteger);beginPrintArray(Row);// 123Row[1]:=4;PrintArray(Row);// 143end;VarA:Arrayofinteger;beginA:=[1,2,3];PrintArray(A);// 123Modify(A);PrintArray(A);// 123end.
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. [16] 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. [36] 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. [16]
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 (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. [36] For example in Fortran:
program Mainimplicit noneinteger::a=1integer::b=2call Swap(a,b)print*,a,b! 2 1contains subroutine Swap(a,b)integer,intent(inout)::a,binteger::temptemp=aa=bb=tempend subroutine Swapend 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:
classMain{staticclassBox{intvalue;publicBox(intvalue){this.value=value;}}staticvoidswap(Boxa,Boxb){inttemp=a.value;a.value=b.value;b.value=temp;}publicstaticvoidmain(String[]args){Boxa=newBox(1);Boxb=newBox(2);swap(a,b);System.out.println(String.format("%d %d",a.value,b.value));}}// output: 2 1
where an explicit Box
type must be used to introduce a handle. Java is call-by-sharing but not call-by-reference. [36]
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"). [37]
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. [38] For example, Ada specifies that the copy-out assignment for each in out
or out
parameter occurs in an arbitrary order. [39] From the following program (illegal in Ada 2012) [40] it can be seen that the behavior of GNAT is to copy in left-to-right order on return:
withAda.Text_IO;useAda.Text_IO;procedureTest_Copy_RestoreisprocedureModify(A,B: inoutInteger)isbeginA:=A+1;B:=B+2;endModify;X:Integer:=0;beginModify(X,X);Put_Line("X = "&Integer'Image(X));endTest_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, [41] as unlike call-by-reference it does not require frequent communication between threads of execution for variable access.
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". [16]
The technique was first noted by Barbara Liskov in 1974 for the CLU language. [16] It is used by many modern languages such as Python (the shared values being called "objects"), [42] Java (objects), Ruby (objects), JavaScript (objects), Scheme (data structures such as vectors), [43] 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). [44] 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. [36]
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. [45]
For example, in Python, lists are mutable and passed with call by sharing, so:
deff(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
:
deff(a_list):a_list=a_list+[1]print(a_list)# [1]m=[]f(m)print(m)# []
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: [46]
#include<stdio.h>voidswap(int*a,int*b){inttemp=*a;*a=*b;*b=temp;}intmain(){inta=1;intb=2;swap(&a,&b);printf("%d %d",a,b);// 2 1return0;}
Some authors treat &
as part of the syntax of calling swap
. Under this view, C supports the call-by-reference parameter passing strategy. [47] Other authors take a differing view that the presented implementation of swap
in C is only a simulation of call-by-reference using pointers. [48] 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, [49] 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>intread(int*p){return*p;}voidassign(int*p,intv){*p=v;}voidswap(int*a,int*b){inttemp_storage;int*temp=&temp_storage;assign(temp,read(a));assign(a,read(b));assign(b,read(temp));}intmain(){inta_storage;int*a=&a_storage;intb_storage;int*b=&b_storage;assign(a,1);assign(b,2);swap(a,b);printf("%d %d",read(a),read(b));// 2 1return0;}
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: [50]
voidswap(int&a,int&b){inttemp=a;a=b;b=temp;}intmain(){inta=1;intb=2;swap(a,b);std::cout<<a<<b<<std::endl;// 2 1return0;}
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.
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.
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. Seed7 provides call by name with function parameters. Java programs can accomplish similar lazy evaluation using lambda expressions and the java.util.function.Supplier<T>
interface.
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 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", also known as "parallel call by name" or "lenient evaluation", [51] 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. [52]
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
fx=1/xgy=1main=print(g(f0))
may either have g
finish before f
, and output 1, or may result in an error due to evaluating 1/0
. [28]
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. [52] However, call-by-future may perform unnecessary speculative work compared to call-by-need, such as deeply evaluating a lazy data structure. [28] This can be avoided by using lazy futures that do not start computation until it is certain the value is needed.
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. [53] This approach avoids some of call-by-need's runtime expenses while retaining desired termination characteristics.
In programming language theory, lazy evaluation, or call-by-need, is an evaluation strategy which delays the evaluation of an expression until its value is needed and which also avoids repeated evaluations.
In programming languages, a closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function together with an environment. The environment is a mapping associating each free variable of the function with the value or reference to which the name was bound when the closure was created. Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.
In object-oriented (OO) and functional programming, an immutable object is an object whose state cannot be modified after it is created. This is in contrast to a mutable object, which can be modified after it is created. In some cases, an object is considered immutable even if some internally used attributes change, but the object's state appears unchanging from an external point of view. For example, an object that uses memoization to cache the results of expensive computations could still be considered an immutable object.
In mathematics and computer science, a higher-order function (HOF) is a function that does at least one of the following:
In computer programming, a parameter or a formal argument is a special kind of variable used in a subroutine to refer to one of the pieces of data provided as input to the subroutine. These pieces of data are the values of the arguments with which the subroutine is going to be called/invoked. An ordered list of parameters is usually included in the definition of a subroutine, so that, each time the subroutine is called, its arguments for that call are evaluated, and the resulting values can be assigned to the corresponding parameters.
In computer programming, a default argument is an argument to a function that a programmer is not required to specify. In most programming languages, functions may take one or more arguments. Usually, each argument must be specified in full. Later languages allow the programmer to specify default arguments that always have a value, even if one is not specified when calling the function.
The syntax of the C programming language is the set of rules governing writing of software in C. It is designed to allow for programs that are extremely terse, have a close relationship with the resulting object code, and yet provide relatively high-level data abstraction. C was the first widely successful high-level language for portable operating-system development.
In computer science, a pointer is an object in many programming languages that stores a memory address. This can be that of another value located in computer memory, or in some cases, that of memory-mapped computer hardware. A pointer references a location in memory, and obtaining the value stored at that location is known as dereferencing the pointer. As an analogy, a page number in a book's index could be considered a pointer to the corresponding page; dereferencing such a pointer would be done by flipping to the page with the given page number and reading the text found on that page. The actual format and content of a pointer variable is dependent on the underlying computer architecture.
In computer programming, a function object is a construct allowing an object to be invoked or called as if it were an ordinary function, usually with the same syntax. In some languages, particularly C++, function objects are often called functors.
In computer programming, a thunk is a subroutine used to inject a calculation into another subroutine. Thunks are primarily used to delay a calculation until its result is needed, or to insert operations at the beginning or end of the other subroutine. They have many other applications in compiler code generation and modular programming.
In computer programming, a callback is a function that is stored as data and designed to be called by another function – often back to the original abstraction layer.
In class-based, object-oriented programming, a constructor is a special type of function called to create an object. It prepares the new object for use, often accepting arguments that the constructor uses to set required member variables.
In computer science, a programming language is said to have first-class functions if it treats functions as first-class citizens. This means the language supports passing functions as arguments to other functions, returning them as the values from other functions, and assigning them to variables or storing them in data structures. Some programming language theorists require support for anonymous functions as well. In languages with first-class functions, the names of functions do not have any special status; they are treated like ordinary variables with a function type. The term was coined by Christopher Strachey in the context of "functions as first-class citizens" in the mid-1960s.
In computer programming, a nested function is a named function that is defined within another, enclosing, block and is lexically scoped within the enclosing block – meaning it is only callable by name within the body of the enclosing block and can use identifiers declared in outer blocks, including outer functions. The enclosing block is typically, but not always, another function.
In some programming languages, const is a type qualifier that indicates that the data is read-only. While this can be used to declare constants, const in the C family of languages differs from similar constructs in other languages in that it is part of the type, and thus has complicated behavior when combined with pointers, references, composite data types, and type-checking. In other languages, the data is not in a single memory location, but copied at compile time for each use. Languages which use it include C, C++, D, JavaScript, Julia, and Rust.
The computer programming languages C and Pascal have similar times of origin, influences, and purposes. Both were used to design their own compilers early in their lifetimes. The original Pascal definition appeared in 1969 and a first compiler in 1970. The first version of C appeared in 1972.
In certain computer programming languages, data types are classified as either value types or reference types, where reference types are always implicitly accessed via references, whereas value type variables directly contain the values themselves.
In computer programming, a constant is a value that is not altered by the program during normal execution. When associated with an identifier, a constant is said to be "named," although the terms "constant" and "named constant" are often used interchangeably. This is contrasted with a variable, which is an identifier with a value that can be changed during normal execution. To simplify, constants' values remains, while the values of variables varies, hence both their names.
In computer programming, a function is a callable unit of software logic that has a well-defined interface and behavior and can be invoked multiple times.
This article includes a list of general references, but it lacks sufficient corresponding inline citations .(April 2012) |
Was probably the first language to systematically exploit the power of lazy evaluation.
{{cite book}}
: |work=
ignored (help)