Subroutines are important because they help to organize big and complex programs as smaller units of code.
A subroutine is a piece of pseudocode. However, in additional to the pseudocode (that performs operations), a subroutine is reusable. A subroutine can be invoked from any place in the overall pseudocode (program). An example is illustrated in algorithm 1.
In this code, there is a subroutine called “abc”. The definition of a subroutine starts with the words “define sub”, and ends with the words “end define sub”. A subroutine is essentially a piece of pseudocode with a name (in this case, “abc” is the name). However, code in a subroutine does not execute unless the subroutine is invoked. To invoke a subroutine is also known as to call a subroutine.
In this program, the first to execute is line 4. This is because this line is the first line that is outside a subroutine definition. The next line that executes is line 5. This is where things get interesting. As subroutine “abc” is called, control is passed to the subroutine.
However, before passing control we need to remember the statement immediately following the invocation. In this case, it is line 6. After this line number is remmebered, control is transferred to the first line of code in the subroutine. As a result, after line 5, the next line to execute is the first line of code in the subroutine “abc”, line 2.
Line 2 is the first and last statement in the subroutine. After its completion, we can imagine that we execute line 3. Normally, this is just considered a marker to end the definition of a subroutine. However, when a subroutine is finished, we need to perform a special operation.
When a subroutine completes its code, it needs to “return” to the code that invoked it in the first place. However, it does not return to the line that contains the invocation. Instead, it returns to the line following the invocation. This is because if we return to the line of invocation, then the subroutine is called again. This leads to the subroutine being called indefinitely.
In our example, we remember the line following the subroutine is line 6. This is why execution continues on line 6 after line 3.
Note that lines 4 to 6 are collectively the invoker (caller) of the subroutine “abc”. Subroutine “abc” is the invoked (called) subroutine.
Tracing the execution of a subroutine requires some effort. This is because we need to indicate the line to return to as a column that belongs to the subroutine. This column does not exist until the subroutine is called. The trace of algorithm 1 is in table 1.
line # | comments |
|
4 | first line out of a subroutine | |
5 | abc | subroutine “abc” stores the line # to return to |
return line # |
|
|
6 |
|
|
2 | the only line in the subroutine to execute |
|
3 | return | we are ready to return, use the stored return line # |
6 | now we are back to the invoker’s (caller’s) code |
Let us formalize subroutines now. The definition of a subroutine, at least in this module, consists of the following:
When a subroutine is invoked using a invoke statement, the following steps take place in this order:
When a subroutine reaches it end define sub line, the following happens in this sequence:
The most crucial part is that the column labeled “return line #” does not exist until the invoke statement. This column also disappears immediately after the end define sub statement. In other words, as a subroutine is called, at least one column is created. As a subroutine returns (when it reaches end define sub), all the column(s) created for the subroutine disappear.
In this module, each subroutine only has one dedicated column. However, when we get into parameters and variables, we’ll see that many columns can be created when a subroutine is invoked.
In general, a defined subroutine can be invoked from anywhere in a program, even from within the subroutine itself!. In other words, we can invoke “abc” from within the code of “abc”! This is called recursion, and it requires some special care.
At any rate, one important aspect of a subroutine is that it can be invoked from different places in a program. This is better than copying and pasting a block of code everywhere. The copy-and-paste approach has several problems compared to invoking subroutines.
First of all, if the original code (that we copy from) has a defect, then the defect is multiplied when the code is pasted elsewhere in the program. This means to fix the original code involves locating all the copies, and fixing all those copies, too. It is easy for a programming to miss one or more of these copies! By comparison, the subroutine approach only has one copy of the defective code. Once the definition of the subroutine is fixed, then all the invocations will be using the corrected code.
Secondly, the copy-and-paste approach also create bloat programs. Each pasted copy uses up program space. On the other hand, the subroutine approach only has one copy of the code. The code to invoke a subroutine is, generally speaking, much smaller than the code of a subroutine. Consequently, the use of subroutine helps to make programs smaller.
Let us consider algorithm 2. This code is not much different from that of algorithm 1. However, it does help to illustrate the flexibility of subroutine invocation. Here, we invoke subroutine “abc” twice.
The trace of this algorithm is in table 2.
line # | comments |
|
4 | first line out of a subroutine | |
5 | abc | subroutine “abc” stores the line # to return to |
return line # |
|
|
6 | the line to return to is 6 | |
2 | the only line in the subroutine to execute |
|
3 | return | we are ready to return, use the stored return line # |
6 | abc | subroutine “abc” is now invoked a second time. The column that stores the return line number is reconstructed |
return line # |
|
|
7 | this time, the line to return to is 7 |
|
2 |
|
|
3 | return | we are ready to return, use the stored return line # |
7 | now we are back to the invoker’s (caller’s) code |
Most programs have subroutines calling other subroutines. There is nothing mysterious about nested invocations. Just follow the same steps as a single invocation.
Algorithm 3 is an example containing nested invocataions.
The trace of this algorithm is in table 3.
line # | comments |
||
8 | first line out of a subroutine | ||
9 | abc | subroutine “abc” stores the line # to return to |
|
return line # |
|
||
10 |
| ||
2 | the first line in the subroutine to execute |
||
3 | def | we invoke “def” from “abc” | |
return line # | this is the return line # that belongs to “def”, it is independent to the return line # of “abc” |
||
4 | this is the line immediately following the invocation |
||
6 | this is the only line to execute in “def” |
||
7 | return | now we return from “def” to “abc”, note which column we use for the return line #, this “return line #” column is destroyed after this row in the trace |
|
4 | return | we are ready to return from “abc”, use the stored return line #. Note, again, which column we use to return |
|
10 | now we are back to the “main program” |