Encapsulation, Inheritance and Polymorphism Top Overriding FunctionsParameter Passing Contents

Parameter Passing

There are (at least) six historical and current methods of passing arguments to a function when a function call is made. They are:

Let's examine these six methods in turn. After which, we will investigate variadic functions in Scam.

Call-by-value

This method is the only method of parameter passing allowed by C, Java, Scam, and Scheme. In this method, the formal parameters are set up as local variables that contain the value of the expressions that were passed as arguments to the function. Changes to local variables are not reflected in the actual arguments. For example, an attempt to define a function for exchanging the values of two variables passed to it might look like:

    (define (swap a b)
        (define temp a)
        (set! a b)
        (set! b temp)
        )

Consider this code which uses swap:

    (define x 3)
    (define y 4)

    (swap x y)

    (inspect x)
    (inspect y)

Under call-by-value, this function would not yield the intended semantics. The output of the above code is:

    x is 3
    y is 4

This is because only the values of the local variables a and b were swapped; the variables x and y remain unchanged as only their values were passed to the swapping function. In general, one cannot get a swap routine to work under call-by-value unless the addresses of the variables are somehow sent. One way of using addresses is to pass an array (in C and Scam, when an array name is used as an argument, the address of the first element is sent. In Java, the address of the array object is sent). For example, the code fragment:

    (define x (array 1))
    (define y (array 0))

    (swap x y)  ;address of beginning element is sent

    (println "x[0] is " (getElement x 0) " and y[0] is " (getElement y 0))

with swap defined as...

    (define (swap a b)
        (define temp (getElement a 0))
        (setElement a 0 (getElement b 0))
        (setElement b 0 temp)
        )

would print out:

    x[0] is 0 and y[0] is 1

In this case, the addresses of arrays x and y are stored in the local variables a and b, respectively. This is still call-by-value since even if the address stored in a, for example, is modified, x still "points" to the same array as before. Here is an example:

    (define (change a)
       (assign a (array 13))
       )

    (define x (array 42))

    (inspect (getElement x 0))

yields:

    (getElement x 0) is 42)

Note that C has an operator that extracts the address of a variable, the & operator. By using &, one can write a swap in C that does not depend on arrays:

    void swap(int *a,int *b)
        {
        int temp;
        temp = *a;
        *a = *b;
        *b = temp;
        }

The call to swap would look like:

    int x = 3;
    int y = 4;

    swap(&x,&y);

    printf("x is %d and y is %d\n",x,y);

with output:

    x is 4 and y is 3

as desired.

Note that this is still call-by-value since the value of the address of x (and y) is being passed to the swapping function.

Call-by-reference

This second method differs from the first in that changes to the formal parameters during execution of the function body are immediately reflected in actual arguments. Both C++ and Pascal allow for call-by-reference. Normally, this is accomplished, under the hood, by passing the address of the actual argument (assuming it has an address) rather than the value of the actual argument. References to the analogous formal parameter are translated to references to the memory location stored in the formal parameter. In C++, swap could be defined as:

    void swap(int &a, int &b) // keyword & signifies
        {                     // call-by-reference
        int temp = a;
        a = b;
        b = temp;
        }

Now consider the code fragment:

    var x = 3;  //assume x at memory location 1000
    var y = 4;  //assume y at memory location 1008

    //location 1000 holds a 3
    //location 1008 holds a 4

    swap(x,y);
    cout << "x is " << x << " and y is " << y << "\n";

When the swapping function starts executing, the value 1000 is stored in the local variable a and 1008 is stored in local variable b. The line:

    temp = a;

is translated, not into store the value of a (which is 1000) in variable temp, but rather store the value at memory location 1000 (which is 3) in variable temp. Similar translations are made for the remaining statements in the function body. Thus, the code fragment prints out:

    x is 4 and y is 3

The swap works! When trying to figure out what happens under call-by-reference, it is often useful to draw pictures of the various variables and their values and locations, then update them as the function body executes.

One can simulate call-by-reference in Scam by delaying the arguments and capturing the calling environment. To obtain the calling environment in Scam, one simply adds a formal parameter with the name #. The calling environment is then passed silently to the function when a function call is made. This means that the # formal parameter is not matched to any actual argument and can appear anywhere in the parameter list (except after the variadic parameters @ and $ - more on them later).

In addition to grabbing a handle to the calling environment, a swapping function also needs to delay the evaluation of the variables passed in. One delays the evaluation of an argument by naming the formal parameter matched to the argument in a special way. If the formal parameter name begins with a $, then the corresponding argument is delayed.

With the ability to grab the calling environment, delay the evaluation of arguments, and access the bindings in an environment (see the chapter on Objects), we can now define a swap function that works as intended.

    (define (swap # $a $b)
        (define temp (get $a #))
        (set $a (get $b #) #)
        (set $b temp #)
        )

The get function takes a symbol and an environment and retrieves the value of the symbol as found in the given environment. The set function is analogous, but changes the value of a symbol found in the given environment. Both get and set are true functions, so the values of $a and $b are used in manipulating the given environment. Note that this version of swap only works if variable names (i.e. symbols) are passed to swap.

Call-by-value-result

This method is a combination of the first two. Execution of the function body proceeds as in call-by-value. That is, no updates of the actual arguments are made. However, after execution of the body, but just before the function returns, the actual arguments are updated with the final values of their associated formal parameters. This method of parameter passing is used for Ada in-out parameters.

We can achieve the affect of call-by-value-result in Scam by using a scheme similar to the simulation of call-by-reference. The modification is to assigning the values of the passed symbols to local variables at the start of the function:

    (define (swap # $a $b)
        ; pre
        (define a (get $a #))
        (define b (get $b #))

        ; body
        (define temp a)
        (set! a b)
        (set! b temp)

        ;post
        (set $a a #)
        (set $b b #)
        )

The section marked pre sets up the locals while the section marked body implements the swap on the locals. The section marked post updates the variables that were passed to swap.

Usually, one cannot tell whether a language implements call-by-reference or call-bay-value-result; the resulting values are the same. One situation where the two methods of parameter passing can generate different results is if the body portion of the function references a global variable and that global variable is passed as an argument as well. This second reference to the global is known as an alias. Now there are two references to the same variable through two different names. Unlike call-by-value-result, updates to the alias in the body section are immediately reflected in the value of the global variable under call-by-reference.

Call-by-name

Call-by-name was used in Algol implementations. In essence, functions are treated as macros. Under call-by-name, the fragment:

    (define (swap a b)
        (define temp a)
        (set! a b)
        (set! b temp)
        )

    (define x 3)
    (define y 4)

    (swap x y)

    (println "x is "  x " and y is " y)

...would be translated into:

    (define (swap a b)
        (define temp a)
        (set! a b)
        (set! b temp)
        )

    (define x 3)
    (define y 4)

    ;substitute the body of the function for the call, 
    ;renaming the references to formal parameters with the names of 
    ;the actual args

    (scope
        (define temp x)
        (assign x y)
        (assign y temp)
        )

    (println "x is "  x " and y is " y)

Under call-by-name, the swap works as desired, so why is call-by-name a method that has fallen into relative disuse? One reason is complexity. What happens if a local parameter happens to have the same name as one of the actual args. Suppose swap had been written as:

    (define (swap a b)
        (define x a) 
        (set! a b)
        (set! b x)
        )

Then a naive substitution and renaming would have produced:

    (scope
        (define x x)
        (assign x y)
        (assign y x)
        )

which is surely incorrect. Further problems occur if the body of the function references globals which have been shadowed in the calling function. This requires a complicated renaming scheme. Finally, call-by-name makes treating functions as first-class objects problematic (being difficult to recover the static environment of the called function). Call-by-name exists today in C++, where it is possible to inline function calls for performance reasons, and in macro processors.

Call-by-need

In call-by-value, the arguments in a function call are evaluated and the results are bound to the formal parameters of the function. In call-by-need, the arguments themselves are literally bound to the formal parameters, as in call-by-name. A major difference is that the calling environment is also bound to the formal parameters as well. This bundle of literal argument and evaluation environment is known as a thunk. The actual values of the arguments are determined only when such values are needed; when such a need occurs, the thunk is evaluated, causing the literal argument in the thunk to be evaluated in the stored (calling) environment. For example, consider this code:

    (define (f x)
        (define y x)  ;x needed! x is fixed to its current value
        (set! z (* z 2))
        (+ x y)       ;x needed! x was already evaluated under call-by-need
        )
    (define z 5)
    (f (+ z 3)) 

Under call-by-name, the return value is 21, but under call-by-need, the return value is 16. This is because the value of z changed after the point when the value of x (really (+ z 3) was needed and the value of x was fixed from the prior evaluation of x. Under call-by-name, the second reference to x causes a fresh, new evaluation of z, the yielding the result of 21.

Call-by-need is exactly the method used to implement streams in the textbook The Structure and Interpretation of Computer Programs. It is important to remember that the evaluation of a call-by-need argument is done only once, with the result stored for future requests.

One can simulate call-by-need in Scam by delaying the evaluation of the arguments and capturing the calling environment. The combination of delayed argument and captured environment comprise a thunk in Scam. To memoize the thunk, we assign the evaluation of the delayed argument to a local variable. Here is a Scam's version of function f:

    (define (f # $x)
        (define x (eval $x #))
        (set! z (* z 2))
        (+ x y)
        )

The built-in eval function is used to evaluate the thunk comprised of $x and #.

Call-by-name-with-thunks

Call-by-name-with-thunks has roughly the same semantics as call-by-name, the difference being that thunks are used instead of textual substitutions. in the body of the function. Call-by-name-with-thunks differs from call-by-need in that the result of evaluating a thunk is not memoized (i.e. stored for future retrieval). Instead, repeated calls for the the value of the formal parameter result in the expression in the thunk being evaluated again and again. Differences between call-by-need and call-by-name-with-thunks arise when the thunk's expression causes a change of state.

    function f(x)
        {
        var y = x;    //x needed! Evaluate the arg in the calling env
        z = z * 2;
        return x + y; //x needed! Evaluate the arg in the calling env
        }

    var z = 5;
    f(z + 3);

Like call-by-name, this function call also returns 21. One can simulate call-by-name-with-thunks in Scam easily:

    (define (f # $x)
        (define y (eval $x #))
        (set! z (* z 2))
        (+ (eval $x #) y)
        )

Here, any time the value of the delayed argument is needed, a fresh evaluation is invoked.

Variadic functions

A variadic function is a function that can take a different number of arguments from call to call. Scam allows this via two special formal parameter names. They are @ and $. If the last formal parameter is @, then all remaining (evaluated) arguments not matched to any previous formal parameters are gathered up in a list and the variable @ is bound to this list. For example, consider these definitions:

    (define (variadic first @)
        (println "the first argument is " first)
        (println "the remaining arguments are:")
        (while (valid? @)
            (println "    " (car @))
            (set! @ (cdr @))
            )
        )
    (define x 1)
    (define y 2)

The call (variadic x) produces:

    the first argument is 1
    the remaining arguments are:

while the call (variadic x y (+ x y)) produces:

    the first argument is 1
    the remaining arguments are:
         2
         3

Similar to @, the formal parameter $ is expected to be the last formal parameter. The difference is that the arguments bundled up into a list are delayed. Suppose one replaced all occurrences of @ with $ in the definition of variadic. Then, the call (variadic x y (+ x y)) would produce:

    the first argument is x
    the remaining arguments are:
        y
        (+ x y)

lusth@cs.ua.edu


Encapsulation, Inheritance and Polymorphism Top Overriding FunctionsParameter Passing Contents