|
Subprocedures: Better than Subroutines
by Ted Holt
Many RPG programmers no longer use conditioning or resulting indicators in calculation specs. Many no
longer use the cycle. Many no longer define variables in C-specs. Maybe it's time RPG programmers quit
using subroutines.
My RPG II teacher did not like subroutines and did not teach his students to use them. It wasn't until I
began writing production code that I learned the value of subroutines. But since then, I've found something
even better than subroutines: subprocedures.
Subprocedures have all the advantages of subroutines and then some. In this article, I point out the
advantages of using subprocedures rather than subroutines.
Subroutines Are Wonderful
I think the greatest benefit of using subroutines is intellectual manageability. That is, I can think of a
program as a group of related small tasks, all of which are simple enough for me to understand, rather than
as one gargantuan task that is more than I can handle. I believe--but cannot prove--that my programs have
fewer bugs because I tackle each task separately.
Debugging seems to be easier because I can often determine which subroutine most likely contains an
error. Finding a logic error in a program of subroutines is similar to determining why my car won't crank. I
don't check to see if there is air in the tires, because I know that a lack of air in the tires won't prevent my
car from cranking. I would look to see if there is gas in the tank or check whether the battery is dead.
Likewise, if there was a mistake in the discount a customer received, I would begin my search for the error
at the routine that calculates discounts.
Subroutines also promote the reusability of code. If I developed a subroutine to calculate a profit margin, I
would be able to copy it to other programs that needed to calculate profit margins.
However, subroutines have their limitations, two of which really bother me. The first is that subroutines use
global variables only. That is, any variable or constant or indicator that I use in a subroutine may be used
anywhere else in the program--in the main calculations, in any subroutine, in output specs. This can lead to
undesirable side effects. Changing the value of a variable in one part of the program causes something to go
wrong in another part of a program.
Global variables also work against the portability of code. For instance, I may have a dandy subroutine that
calculates a scheduling function of some sort, but copying it to another program may require me to rename
a lot of variables. For example, maybe the two programs use different input files, in which case field names
are different. Or maybe the work variables in the subroutine have already been used for other purposes in
the second program. In such cases, I'm less likely to reuse the subroutine.
The other thing that bothers me is that I can't define parameters to pass data to subroutines. A subroutine
that verifies a general ledger account number might need to verify several different account number
variables within one program, but there is no way to pass each different account number variable to the
subroutine.
Subprocedures Are Even More Wonderful
Subprocedures directly address these two problems. First, subprocedures allow you to define local
variables. A local variable is one that is understood only with within the subprocedure and cannot be
referenced outside of it.
A local variable can have the same name as a global variable. The two are separate variables, and the
compiler will not confuse them. The global and local variables of the same name do not have to be defined
identically. Do you understand the ramifications of this? You can create a work variable in a subprocedure
and not have to worry that there may be another variable of the same name in the main part of the program.
This increases my chances of porting a subprocedure from one program to another.
Parameters also make subprocedures more portable by providing internal names (i.e., internal to the
subroutine) for required data values. The subprocedure does not have to know the names of variables,
constants, and fields in database files or other parts of the program in order to do its assigned task.
An Example Would Be Good
If subprocedures are harder to code than subroutines, it's not by much. A subprocedure needs a procedure
prototype, but that's no big deal; I just copy the procedure interface from the subprocedure and change the
"pi" line to a "pr" line. The CALLP op code is no harder to code than EXSR.
To illustrate, here are two versions of a routine to calculate income tax in a mythical state. I've included
them inside part of a "gross-to-net" payroll program. The first is implemented as a subroutine, the second as
a subprocedure. If you compare them, you'll see there is little difference.
First, here's the subroutine. Notice the identifiers that begin with PW. These are fields in the payroll work
file, which this subroutine updates. The only work variable, TaxablePay, is defined in the D specs.
Fpaywork uf e disk
D TaxablePay s 7p 2
C read PayRec
C dow not %eof(PayWork)
C exsr CalcStateTax
C update PayRec
C read PayRec
C enddo
C*
C eval *inLR = *on
C CalcStateTax begsr
C*
C eval PWStateTax = *zero
C* calculate annual pay
C eval TaxablePay =
C (PWGross * PWNbrPer)
C* deduct allowance for employee & spouse
C if PWMarStat = 'M'
C eval TaxablePay =
C (TaxablePay - 10000)
C else
C eval TaxablePay =
C (TaxablePay - 5000)
C endif
C* if zero or less, no tax due
C if TaxablePay <= *zero
C leavesr
C endif
C* deduct allowance for dependents
C eval TaxablePay =
C (TaxablePay -
C (PWNbrDep * 2500))
C* if zero or less, no tax due
C if TaxablePay <= *zero
C leavesr
C endif
C* yearly tax is 3% of first $5,000
C* 4% of remainder
C if TaxablePay <= 5000
C eval PWStateTax =
C (TaxablePay * 0.03)
C else
C eval PWStateTax =
C (150 +
C (TaxablePay - 5000) * 0.04)
C endif
C* get this period's portion of yearly tax
C eval PWStateTax =
C (PWStateTax / PWNbrPer)
C endsr
Here's the subprocedure. There are no references to global variables. The TaxablePay variable is now
defined in the subprocedure. There may be other variables named TaxablePay in other tax routines, but
they will not conflict with or affect this one. The subprocedure does not directly reference the fields from
the payroll work file. Instead, all fields are passed to the subprocedure through parameters.
H dftactgrp(*no) actgrp('QILE')
Fpaywork uf e disk
* prototype for state tax routine
DCalcStateTax pr
D StateTax 7p 2
D Gross 7p 2 value
D NbrPer 3p 0 value
D MarStat 1 value
D NbrDep 3p 0 value
C read PayRec
C dow not %eof(PayWork)
C callp CalcStateTax
C (PWStateTax:
C PWGross:
C PWNbrPer:
C PWMarStat:
C PWNbrDep)
C update PayRec
C read PayRec
C enddo
C*
C eval *inLR = *on
PCalcStateTax b
* parameters
D pi
D StateTax 7p 2
D Gross 7p 2 value
D NbrPer 3p 0 value
D MarStat 1 value
D NbrDep 3p 0 value
* local variables and constants
D TaxablePay s 7p 2
C eval StateTax = *zero
C* calculate annual pay
C eval TaxablePay =
C (Gross * NbrPer)
C* deduct allowance for employee & spouse
C if MarStat = 'M'
C eval TaxablePay =
C (TaxablePay - 10000)
C else
C eval TaxablePay =
C (TaxablePay - 5000)
C endif
C* if zero or less, no tax due
C if TaxablePay <= *zero
C return
C endif
C* deduct allowance for dependents
C eval TaxablePay =
C (TaxablePay -
C (NbrDep * 2500))
C* if zero or less, no tax due
C if TaxablePay <= *zero
C return
C endif
C* yearly tax is 3% of first $5,000
C* 4% of remainder
C if TaxablePay <= 5000
C eval StateTax =
C (TaxablePay * 0.03)
C else
C eval StateTax =
C (150 +
C (TaxablePay - 5000) * 0.04)
C endif
C* get this period's portion of yearly tax
C eval StateTax =
C (StateTax / NbrPer)
C*
PCalcStateTax e
Let me point out one more thing. A program with a subprocedure will not run in the default activation
group, so I included an H-spec to force it to run in activation group QILE instead.
Returning a Value
Here's another thing subprocedures can do that subroutines can't: A subprocedure can return a value in the
same way that a built-in function returns a value. This means that you can reference a subprocedure name
in such operations as EVAL, IF, and DOx.
The previous example is a good illustration of this. The CalcStateTax procedure yields one value--the
amount of tax to withhold from a paycheck and send to the tax collector. Here is the subprocedure modified
to return a value:
H dftactgrp(*no) actgrp('QILE')
Fpaywork uf e disk
DCalcStateTax pr 7p 2
D Gross 7p 2 value
D NbrPer 3p 0 value
D MarStat 1 value
D NbrDep 3p 0 value
C read PayRec
C dow not %eof(PayWork)
C eval PWStateTax =
C CalcStateTax
C (PWGross:
C PWNbrPer:
C PWMarStat:
C PWNbrDep)
C update PayRec
C read PayRec
C enddo
C*
C eval *inLR = *on
* ========================================================
PCalcStateTax b
* parameters
D pi 7p 2
D Gross 7p 2 value
D NbrPer 3p 0 value
D MarStat 1 value
D NbrDep 3p 0 value
* local variables and constants
D TaxablePay s 7p 2
D StateTax s 7p 2
C* calculate annual pay
C eval TaxablePay =
C (Gross * NbrPer)
C* deduct allowance for employee & spouse
C if MarStat = 'M'
C eval TaxablePay =
C (TaxablePay - 10000)
C else
C eval TaxablePay =
C (TaxablePay - 5000)
C endif
C* if zero or less, no tax due
C if TaxablePay <= *zero
C return *zero
C endif
C* deduct allowance for dependents
C eval TaxablePay =
C (TaxablePay -
C (NbrDep * 2500))
C* if zero or less, no tax due
C if TaxablePay <= *zero
C return *zero
C endif
C* yearly tax is 3% of first $5,000
C* 4% of remainder
C if TaxablePay <= 5000
C eval StateTax =
C (TaxablePay * 0.03)
C else
C eval StateTax =
C (150 +
C (TaxablePay - 5000) * 0.04)
C endif
C* get this period's portion of yearly tax
C return (StateTax / NbrPer)
C*
PCalcStateTax e
Notice the differences. The "pi" procedure interface line now includes a data type and size to tell what type
of value is being returned. Each RETURN operation includes a value to be sent back to the caller.
A Few More Tips Would Be Nice
I hear that many programmers do not use subprocedures. If you're one of them, I hope I've given you some
good reasons to begin using them. And before I go, let me offer a few tips on using subprocedures.
First, subprocedures follow the O-specs, if you have any. Since most RPG programs these days don't have
O-specs, subprocedures usually follow the C-specs.
Second, don't start converting subroutines to subprocedures. I do not advocate changing working code
unless there is a clear benefit in doing so. You may benefit from converting subroutines that carry out
common tasks or tend to break and need a lot of fixing; but otherwise, leave your existing code alone.
Third, if a subroutine is useful throughout an application--if it can be used in more than one program--
consider putting it into a service program, to which other programs can bind at compile time, rather than at
the end of each program that needs it. Building service programs is a topic for another article.
Speaking of topics for other articles, subprocedures support recursion. That is, a subprocedure can call
itself. Try that with a subroutine sometime. This is not a terribly useful feature, but recursion does have its
applications, and maybe I will write about it someday.
There is one additional advantage to subroutines worth mentioning. Subroutines are faster than
subprocedures. I have never had to worry about the performance of subprocedures, but in a heavily used
program, performance may become an issue. Using subroutines instead of subprocedures may make a
difference.
I don't know what my RPG teacher's aversion to subroutines was. Maybe he didn't like having to code SR
in columns 7 and 8 of the C-specs, as the System/3 RPG II compiler required. Maybe he thought that using
subroutines added too many lines of code to programs. I just hope that you will not have such an aversion
to subprocedures.
Ted Holt is a consultant and an editor of Midrange Guru, OS/400 Edition. He
welcomes your comments at tholt@itjungle.com.
|