This is the archived website of SI 413 from the Fall 2012 semester. Feel free to browse around; you may also find more recent offerings at my teaching page.
Required reading:
Recommended reading:
This unit is all about function calls. Hopefully your work of implementing function calls in the last two labs has opened your eyes to what's really going on with a function call, but there are even more options than what we have been able to explore in SPL.
Functions fundamentally consist of two parts: the function definition and the call site (wherever the function is actually called). Of course there may be many call sites for a single function definition as well. Making a function call is like saying, "Go back and execute that code I already wrote down."
We are going to examine the different methods of communication between the call site and the function definition, which mostly means talking about how arguments work. Then we'll look at some more advanced uses of functions and other related concepts.
Readings for this section: PLP, Section 8.3 intro through 8.3.2 (required), all of 8.3 (recommended)
It is possible for a function to just do the same thing every time, like print some fixed message to the screen. But this is unlikely. Usually, there is some communication between the call site and the function definition. For example, you might write a very simple function that just takes its argument (a number), multiplies it by two, and returns the result. In this scenario, the call site passes in the value of the original number, and the function passes back the value of the result, so there is two-way communication going on.
Function parameters and return values are the primary way in which a program communicates between the function call site (wherever the function is actually called) and the function definition. The other option of course is to use commonly-accessible variables such as global variables: these are set before the function call to pass information into the function, and read after the function call to get information out.
Here is a simple C++ program that shows three ways of writing the "multiply by two" function and printing the result.
int foo1_global; void foo1() { foo1_global = foo1_global * 2; } int foo2(int in) { return in * 2; } void foo3(int& inout) { inout = inout * 2; } int main() { int x=1, y=2, z=3; foo1_global = x; foo1(); cout << foo1_global << endl; cout << foo2(y) << endl; foo3(z); cout << z << endl; return 0; }
Notice the three basic ways of function communication:
foo1
uses the global variable foo1_global
to communicate in both directions. This idea will work with any kind of function, but it is usually not the best idea. The main reason is scoping. For instance, if there happens to be a local variable with the same name at the call site, we will get different behavior in lexical or dynamic scoping. Even more troubling, because this method is side-stepping the normal nice things that scoping gives us, recursion becomes impossible! So while using a common (global) variable for function communication might be useful sometimes, it's rarely the best option.
foo2
does what we probably expect to see in C++: It takes the input as one argument, and returns the result separately. This is called "pass by value", and it means that the function gets a copy of the argument, so the argument is a one-way communication from call site to function body. Similarly, the return value is one-way communication back the other way. This works great in this situation, but can lead to difficulties when the arguments are large data structures (so we don't want to copy them), and when we want to return more than one value from a single function.
foo3
uses one little &
character to indicate that its argument is to be passed by reference. What this means is that the function parameter inout
is actually referring to exactly the same thing as the variable z
that gets passed in from main. In this way, the parameter is used for two-way communication between the call site and the function body. However, this option also has disadvantages: it constrains us to having only certain things (like variable names) as arguments, and it can lead to bugs as whoever called that function might not expect the argument to change.
The difference between foo2
and foo3
is called the "parameter passing mode*, which means how the argument is passed from call site to function body.
The most typical option is pass by value; this is what happens by default in C++ and in our SPL language too: the function receives a copy of the argument. Any changes the function makes to the argument are not seen by the call site, because those changes are on a copy that only the function gets to use. This allows any arbitrary expression to be used as the argument, since it will simply be evaluated and then this value is bound to the function's parameter name. The advantages of pass by value are that it is relatively simple to implement, and it clearly defines the communication from call site to function body.
The second primary option for parameter passing is pass by reference, which is supported in C++ by using reference parameters and the &
specifier. Some languages, including Perl and Visual Basic, actually use this mode by default. In pass by reference, the function parameter becomes an alias for the argument; any changes the function makes are immediately reflected at the call site. This overcomes some disadvantages of pass by value, namely by avoiding the need to copy large data structures as arguments, and by allowing a function to return more than one thing.
But pass by reference can also lead to programming errors, because it blurs the lines of communication between call site and function body. Is this argument for communicating from call site to function call, or from function call back to call site, or both? We simply can't know without looking at how the function actually works. Different languages provide some compromises for getting the nice aspects of pass by reference without all of the dangers:
In C++, we can use the const
keyword to specify that an argument will not be modified by the function, like dancpp vector<string> rev(const vector<string> &A) { ... }
This avoids the need to copy a large data structure, but also makes it clear that we still have one-way communication, since the function can't modify what's there.
In Ada, the parameter passing mode for each argument is indicated as either in
, out
, or in out
. Arguments specified as in
are set by the call site and cannot be modified by the function; they represent one-way communication. Arguments specified as out
are not set by the caller, but are set by the function; they represent one-way communication in the other direction. Arguments specified as in out
are a little funny: they are not passed by reference, but in a different way called pass by value/result. What this means is: the argument is passed in as a copy (by value), but then when the function returns, whatever value is in that copy is copied back to the original parameter. This usually gives the same behavior as pass by reference, except that we still have to do the copying.
In Java, primitive types like int
are passed by value, just like in C++. But objects in Java are different; calling a function on a Java object is sort of like passing a pointer to that object. If the function modifies the object, then the original one gets modified too (like pass by reference). But if the function reassigns the argument to something else, the original isn't changed at all (this is like pass by value). What is going on?
Well, you can read more details in your book or by looking at any Java reference, but this idea is called pass by sharing. The idea is that there is some data (in this case, the object itself) that is "shared" between the call site and the function body. It's sort of like pass-by-value, but with a "shallow copy" instead of a full copy. This can be really nice because it avoids the overhead of copying large data structures, while also giving some of the advantages of pass by value, but this "hybrid" approach to parameter passing is also notoriously confusing for Java programmers: see here for some evidence of the confusion.
In summary, you should know how pass by value, pass by reference, and pass by value/result work, and where we might want to use each one. You should also know that pass by sharing is another option that is used in some languages, and what pass by sharing means.
Readings for this section: PLP, Section 6.6.2 (required), Section 10.4 (recommended)
Now we know how the value of an argument gets passed between the call site and the function body. But what about when the argument is an expression like (x + 2)
? When exactly does this get evaluated? Of course there are multiple options! Here are the two basic choices:
Applicative order. This is what you have come to expect from most programming languages you have used (C, Java, Scheme, SPL, ...). The argument expression is evaluated exactly once, just before the function is called. Then the resulting value is used in the function according to the parameter passing scheme (see above!).
Call by name. This is the other extreme, and it exists in macro languages such as the C preprocessor (we'll discuss this below) and ALGOL. Instead of evaluating the argument at the beginning, that expression is only evaluated whenever the argument is used.
The potential advantage here is that, if we don't use some particular argument, then we never have to waste the time evaluating it! The downside is basically the same thing: if you use an argument more than once, then you have to evaluate it more than once too.
The other option is called normal order evaluation, wherein the arguments are not evaluated until they are needed. A related concept (which for the purposes of this course will be the same concept) is called lazy evaluation, which means every argument is only evaluated at most once, but might not be evaluated at all if it's not needed. So lazy evaluation sort of gives the best of both worlds from above - unused arguments are never evaluated, but frequently-used arguments are only evaluated once!
So why not just use lazy evaluation everywhere? Why doesn't C++ use it? The issue comes when expressions have side-effects. For example, in C++ we can have a function call like foo(++x)
. The clear intent of the programmer in this case is that x should be incremented exactly once when this function call is made. But if we used lazy evaluation, x might not get incremented at all, depending on whether foo
actually uses it! This is why lazy evaluation is supported by pure functional languages such as Haskell and ML, where referential transparency is strictly enforced. When there are no side effects, we can always do lazy evaluation. Hooray!
Readings for this section: PLP, Sections 3.5.2, 3.5.3, and 9.4 intro through 9.4.3 (required), all of 3.5, 3.7, and 9.4 (recommended)
Readings for this section: PLP, Section 3.7 (required)