====== Parameter passing in different programming languages ====== Different programming languages adopt different strategies for passing (and evaluating) parameters during function calls. Such strategies can be split into two categories: * //applicative// * //normal// but different blends are possible, depending on how values are stored and passed on. We briefly review some of these strategies: ==== Call-by-value ==== In **call-by-value**, parameters are passed (stored on the call stack) as **values**. This is the case in C, as well as Java for primitive values. void swap (int x, int y){ int t = y; y = x; x = t; } We illustrate **call-by-value** using the ''swap'' function. In the code below: int x = 1, y = 2; swap(x,y); the values of ''x'' and ''y'' **do not change** after the call of ''swap'', since the values have been **copied** on the call stack. Call-by-value is an **applicative** evaluation strategy. ==== Call-by-reference ==== **Call-by-reference** is implemented in C explicitly via pointers, and is the **default parameter-passing strategy** in Java, for non-primitives (objects). We illustrate call-by-reference in C: void swap (int* x, int* y){ int t = *x; *y = *x; *x = t; } In the code example below: int x = 1, y = 2; swap(&x,&y); the values of ''x'' and ''y'' have been changed, since **their addresses** (instead of their values) have been passed on the call stack. Call-by-reference is an **applicative** evaluation strategy. ==== Call-by-macro expansion ==== Consider the following macro-definition in C: #define TWICE(X,Y) {Y = X + X;} In the code example: int y; TWICE(1+1,y); the macro is **textually-expanded without parameter evaluation**, yielding ''y = 1 + 1 + 1 + 1'' (instead of ''y = 2 + 2''). Call-by-macro is a **normal** evaluation strategy, and behaves **exactly** like **normal evaluation from the lambda calculus**. Note however that macros in C are more limited than functions and do not rely on a call-stack. ==== Call-by-name ==== **Call-by-name** is a **normal** evaluation strategy, which, similar to **call-by-macro expansion**, reproduces lambda calculus's **normal evaluation**. It is not implemented **per-se** in programming languages, because it is inefficient. We illustrate this, by the following example: int g(by-name int x, by-name int y){ return x + y; } int f(by-name x){ if (x == 0) return 0; return f(g(x,x)-4); } main{ f(3); } where we have introduced a //fictitious// //by-name// directive, which forces the parameter at hand to be evaluated using the normal strategy. The call ''f(3)'' will have the following behaviour: * since ''3 == 0'' is false, the following call is made: * ''f(g(3,3)-4)'' * the condition ''g(3,3)-4 == 0'' triggers a call to ''g''. The condition is false. Thus, the following call is made: * ''f(g(g(3,3)-4,g(3,3)-4)-4)''. Note that, even though the parameter of ''f'' was evaluated during the condition check (to ''2''), //call-by-name// requires that it is **evaluated again**: * the condition ''g(g(3,3)-4,g(3,3)-4)-4 == 0'' triggers three calls of ''g'', and is true. Hence the program returns ''0''. Note that, during the call of ''f(3)'', we had a total number of 4 function calls of ''g'', when actually 2 would have been sufficient. ==== Call-by-need (lazy) ==== **Call-by-need** is a **normal evaluation strategy** which improves **call-by-name** by **storing a result once it is computed**. Returning to our previous example, let us replace the //by-name// directive with //by-need//. Then, the call ''f(3)'' will have the following behaviour: * since ''3 == 1'' is false, we have the following call: * ''f(g(3,3)-4)''. The expression ''g(3,3)-4'' is evaluated to ''2'' during the comparison (which fails), and the following call is triggered: * ''f(g(g(3,3)-4,g(3,3)-4)-4)''. During this call, to evaluate the comparison with 0, we need to evaluate ''g(g(3,3)-4,g(3,3)-4)-4''. However, technically, this expression is viewed by the runtime as: ''g(thunk,thunk)-4'', where ''thunk'' is a **pointer** to the expression ''g(3,3)-4''. This expression has already been evaluated, hence we have the call ''g(2,2)-4'', and the comparison succeeds. Note that, during //call-by-need//, we only have two function calls of ''g'' (instead of 4). ===== Lazy evaluation in Haskell ===== The default evaluation strategy in Haskell is **lazy** or **call-by-need**. Each expression in Haskell can be viewed as a //thunk//: a pointer which holds the expression itself, as well as its value, once it is evaluated. We illustrate lazy evaluation by looking at the following calls: foldr (&&) True [True,False,True,True] Let us consider that the implementation of ''(&&)'' is as follows: True && True = True _ && _ = False This expression will produce the following sequence of calls: * ''True && (foldr (&&) True [False,True,True]'' * ''True && (False && (foldr (&&) True [True,True]))'' * ''True && False'' * ''False'' In the second pattern of ''&&'', the function returns ''False'' without irrespective of the parameters. Hence, the call ''(False && (foldr (&&) True [True,True]))'' returns ''False'' without evaluating ''(foldr (&&) True [True,True])''. As it turns out, ''foldr'' may be efficient even if it is not tail-recursive, in situations where reducing a list does not require exploring all its elements. Let us also the evaluation of: foldl (&&) True [True,False,True,True] which triggers: * ''foldl (&&) (True && True) [False,True,True]'' * ''foldl (&&) ((True && True) && False) [True,True]'' * ''foldl (&&) (((True && True) && False) && True) [True]'' * ''foldl (&&) ((((True && True) && False) && True) && True) []'' * ''((((True && True) && False) && True) && True)'' * ''(((True && False) && True) && True)'' * ''((False && True) && True)'' * ''(False && True)'' * ''False'' Since the evaluation is lazy, the accumulator is only evaluated when needed, that is, when ''foldl'' returns. The result shows that ''foldl'' may be less eficient than ''foldr'' in Haskell, even if the former is tail-recursive.