Along with variables, functions are among the oldest and the most important programming constructs in imperative programming languages. Like variables, programmers name functions, and the compiler maps the function names to the function's memory address (i.e., the memory location of the function's machine code instructions). Function calls can pass zero or more arguments to functions, which operate on the data and return zero or one value to the call. While the returned value can be an aggregate type (an array or object), we still consider it a single value.
(a) | (b) |
Although functions impact programming in many ways, their most important contribution to software development is their influence on how developers conceptualize or think about problems and how they implement the software that solves them. Trying to understand a large and complex problem in its entirety is both overwhelming and counterproductive. Software developers typically decompose or break down the problem into successively smaller units until they reach a conceptual unit that is small enough and simple enough to understand. Once they understand the smaller units, they build increasingly complex units composed of the smaller, more simple units.
Similarly, software design based on the procedural programming paradigm proceeds by representing or mapping parts of the problem to corresponding functions. Software designers decompose or break functions into increasingly smaller and more simple functions. Unfortunately, there is no "magic" recipe for doing a functional decomposition, and there is no perfect size for the functions at which we stop the decomposition. We continue breaking the functions down until we reach a point at which we can understand and conveniently implement the individual functions.
Mapping parts of a problem to different functions implies that we can use functions to think about and represent abstract problems. As Jonassen suggests, we can then manipulate those functions as part of our problem-solving process. Finally, we implement or program those functions to realize a working solution to the original problem.
Software developers typically take one of two paths to implement the functions.
Bottom-up implementation begins with the leaf functions at the bottom of the tree. Once programmers implement the leaves of a sub-tree, they can implement the sub-tree itself. If the functions only rely on data passed in through the argument list, it is possible to validate these functions independently of the rest of the program. Programmers use a driver or set of driver programs to validate each function. A driver is simply a program that calls a function, sends appropriate test data as arguments, and in some way verifies that the returned values or effects of the function are correct. The bottom-up strategy is demonstrated by both the Time and the American example programs presented in the last chapter.
For contemporary software development, the bottom-up approach is appropriate for small programs or for implementing the core functions of larger, more complex programs. However, the bottom-up approach is incompatible with incremental delivery for large, dynamic programs. Software developers practice incremental delivery when they initially deliver a system with essential core features and incrementally add and deliver enhancements over time.
void function(int x, int y, int z) { . . . } int max(int x, int y) { return (x > y) ? x : y; } |
int main() { function(10, 20, 30); int a = max(100, 200); cout << a << endl; return 0; } |
(a) | (b) |
Top-down implementation begins with the high-level functions. Function stubs replace the smaller, lower sub-functions. A stub is nothing more than an empty function where the body contains just enough code to allow the function to compile without error. For example:
void print(int x, double y) { } |
double pow(double x, double y) { return 1.0; } |
void
as the return type) must include a "dummy" return statement (e.g., return 1.0;
) in the body for the program to compile. The bodies of stub functions are replaced by fully operational statements (and any dummy returns are discarded) as development continues downwards from the top of the decomposition tree.
Top-down implementation allows software developers to practice incremental delivery. Large software systems typically provide many related features. Programmers can implement a minimal core set of features that form a deliverable product capable of doing practical work. Programmers regularly enhance the product with additional features and incrementally deliver enhancements to customers and clients. Incremental delivery allows developers to gain market share and gather feedback about what is important to customers.