Table of Contents

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:

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:

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:

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:

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:

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.