With the use of free-format (aka /Free) RPG logic becoming more and more popular, many shops are beginning to utilize prototypes for their program calls because the CALL operation code is one of those that doesn't work in free-form. This is a movement to which I say, "It's about time!" For far too long, many shops have used prototypes only with subprocedures. Even with this upswing in the use of prototypes, however, I've found that many programmers are still not taking advantage of all that prototypes have to offer. Too many shops do the bare minimum necessary to get their program calls to function in /Free and are not leveraging all that this great language feature offers. Prototyping has been with us for a long time, since V3R2 in fact. So in this article, let's take a look at some of the under-utilized features of prototypes.
Prototype Basics
First, let's look at the basics of prototyped calls for those who haven't yet made the switch. Let's compare a program call with CALL to one with CALLP. Of course, we can call subprocedures with prototypes as well, but people using subprocedures are already familiar with the use of prototypes for that, so we'll concentrate here on using prototypes for program calls.
Take a look at the following code that calls a program to calculate payroll taxes:
C Eval PayAmt = GrossPay + Bonus
C Call 'PA0132'
C Parm PayAmt
C Parm Allow
C Parm FedTax
C Parm StateTax
To call this program with a CALLP operation, I could create a prototype and use a CALLP in place of the CALL, like the following example:
D CalcTaxes PR ExtPgm('PA0132')
D TotalPay 7P 2
D Allow_Nbr 3S 0
D FedTaxOutput 5P 2
D StTaxOutput 5P 2
D PayAmt S 7P 2
C Eval PayAmt = GrossPay + Bonus
C CallP CalcTaxes(PayAmt : Allow : FedTax : StateTax)
If my reasons for using a prototype were to use /Free logic, the CALLP statement would look more like this:
PayAmt = GrossPay + Bonus;
CalcTaxes(PayAmt : Allow : FedTax : StateTax);
For those of you not familiar with prototypes, notice that the prototype is defined in the D specs. The syntax is similar to that of a data structure in that the D specs that follow the prototype beginning statement (indicated by the PR) that have the "PR" columns blank are part of the prototype. They represent the parameters. The definition of the PayAmt standalone field indicates the end of the parameter descriptions for the program.
One thing that may look odd to you if you haven't used prototypes before is the fact that the "field names" used in the prototype do not match the field names passed as parameters on the CALLP statement. I put "field names" in quotes because the text in the field name position in a prototyped parameter definition is not naming a field at all. It is merely used as documentation for you and your fellow programmers to make it easier to tell the parameters apart. The data type and size definitions (as well as any keywords, which we will discuss later) are the critical parts of the parameter definitions.
Note that the EXTPGM keyword is required when calling a program, even if you decide to name the prototype the same as the *PGM object you are calling. I'm a big believer in making logic as understandable as possible, however, so I like to take advantage of the ability to override the (usually less meaningful) *PGM object name to a name that describes its function, as I did in this example (i.e., changing PA0132 to CalcTaxes). The CALLP statement now looks a bit more like a call to a built-in function, with the parameters in parentheses after the prototype name.
Besides being able to write the operation in free-form and using a more meaningful name in the logic, are there any other benefits to using the prototoyped call in the example above? The biggest benefit is the fact that the number of parameters and the data type and size of each parameter used on the call will be verified at compile time against those in the prototype. In the example of the CALL ... PARM statements before, if the Allow field in this calling program had been defined as 5-digit packed decimal with 2 decimals and the called program expected it to be zoned decimal 3-digit with no decimals, it would not have been detected until run time (hopefully during testing!). Likewise, if only three parameters had been passed when four were expected, it would cause a run-time error. In either of these cases, when using a prototype, the errors would be detected by the compiler. It's obviously much easier and faster to identify and fix a compile-time error than a run-time error.
Of course, for that compile-time check of parameter types to be completely effective, the programmer must ensure that the prototype definition matches the called program exactly. There is a way to help ensure that, and we will talk about it a little later.
Improving Our Prototype
Could our prototype be made more effective? I like to indicate in the prototype which parameters are used as "input only" in the called program by using the CONST ("constant reference" is the full term) keyword on those prototyped parameters. This not only helps by giving me more information about the data flow between this program and the called program, but more significantly, it allows the compiler to help me on the call. Consider the following example:
D CalcTaxes PR ExtPgm('PA0132')
D TotalPay 7P 2 Const
D Allow_Nbr 3S 0 Const
D FedTaxOutput 5P 2
D StTaxOutput 5P 2
CalcTaxes( GrossPay + Bonus : Allow : FedTax : StateTax);
In this version, the first two parameters are labeled as CONST. Since the called program will not be changing their values to supply information back to this program, I'm able to simplify my logic somewhat by using the expression (GrossPay + Bonus) directly as a parameter, doing away with the PayAmt temporary field. Likewise, in this case, if the Allow field in this program was defined as packed decimal, it would not result in either a compile-time or a run-time error. Instead, the compiler would create a temporary field for each of the first two parameters, based on the data type and size specified in the prototype. It would then copy the appropriate values from this program into them to pass as parameters to the PA0132 program.
While it doesn't help in this example, I could have even used a built-in function, such as %LEN, as a parameter, which is quite helpful when calling many system APIs. Note that the compiler does not make a temporary field and copy the data over unless it is necessary (i.e., when the parameters requested in the prototype do not match those used in the call statement).
Because the last two parameters are used as output parameters (i.e., their values are updated by the called program), they cannot be declared as constant; therefore, the fields passed for those parameters must match the data type and size specified by the prototype.
Specifying Optional Parameters in the Caller
Another nice benefit of using prototypes is that I can now add additional optional parameters to the end of my parameter list. Let's say that the program that calculates taxes now needs a new parameter to indicate some kind of deduction that should be taken into account when calculating, e.g., a deduction for additional medical insurance and/or a retirement savings plan. However, there may still be some circumstances when that extra information does not need to be passed to the tax calculation program. To allow for both situations--sometimes the extra parameters are needed, but other times they are not--you could make the passing of the extra parameters optional by putting them at the end of the parameter list. This requires also using the *NoPass option keyword on the new optional parameters.
D CalcTaxes PR ExtPgm('PA0132')
D TotalPay 7P 2 Const
D Allow_Nbr 3S 0 Const
D FedTaxOutput 5P 2
D StTaxOutput 5P 2
D MedDeduct 3P 2 Options(*NoPass)
D RetireDeduct 3P 2 Options(*NoPass)
CalcTaxes( GrossPay + Bonus : Allow : FedTax : StateTax );
CalcTaxes(GrossPay + Bonus : Allow : FedTax : StateTax :
Medical : Retire );
CalcTaxes(GrossPay + Bonus : Allow : FedTax : StateTax :
Medical );
In the example above, all three calls to the program are acceptable because of the use of the *NoPass option. Note that either the first or the first and second of the *NoPass parameters may be passed.
However, in this example, you cannot pass only the Retire parameter and not the Medical one. You could allow that scenario by adding the additional optional keyword *Omit in place of the fifth parameter. Actually, I rarely find this a useful feature, but since I'm often asked if it is an option, I'm including it in the example below. This illustrates the fourth possible call option for this revised prototype. Note the use of the RPG special value *Omit for the fifth parameter in the call statement. Note also that the called program must be prepared for both *Omit and *NoPass options to work properly; we'll look at how it does that in a moment.
D CalcTaxes PR ExtPgm('PA0132')
D TotalPay 7P 2 Const
D Allow_Nbr 3S 0 Const
D FedTaxOutput 5P 2
D StTaxOutput 5P 2
D MedDeduct 3P 2 Options(*NoPass : *Omit)
D RetireDeduct 3P 2 Options(*NoPass)
CalcTaxes(GrossPay + Bonus : Allow : FedTax : StateTax :
*Omit : Retire );
What About the Called Program?
Finally, let's turn our attention to the called program: PA0132 in this case. We can call any program (or procedure) in any language using a prototype. However, if it is an RPG IV program you are calling, it makes sense to take full advantage of all that the compiler can do for you by replacing the called program's *Entry PList with a Procedure Interface (PI). Indeed, to fully move to /Free, you must do this because the *ENTRY PLIST is not supported in /Free. When you do this, the compiler can check your prototype for you to ensure you have coded it correctly. This is how you ensure the prototype in the calling program is correct, as mentioned earlier.
Note the following *Entry PList. We have gone back to the original example without the optional parameters. We'll add them back in later.
C *Entry Plist
C Parm PayAmt
C Parm Allow
C Parm FedTax
C Parm StateTax
This can (and should) be replaced with the following Procedure Interface:
D CalcTaxes PI
D TotalPay 7P 2 Const
D Allow_Nbr 3S 0 Const
D FedTaxOutput 5P 2
D StTaxOutput 5P 2
One thing to note here is that names on the PI (e.g., TotalPay, Allow_Nbr, etc.) are defining the fields of those names to hold the parameters. This is in contrast to the prototype where those "names" are documentary only.
There is a requirement for the prototype for the program (or procedure) to also appear in the source member at compile time. This enables the complier to conduct a check of the prototype's accuracy in matching the parameter list in the PI. Since we always want to ensure that the prototype that was verified as correct is used in all the calling programs, I highly recommend that you place your prototypes in a separate source member and that you use a /Copy to bring the prototype into this program (the called program) and also into any and all calling programs. That way, you can be sure that the prototype used in the callers is exactly the same as the one the compiler has already verified as matching the called program.
In the example below, note the use of the /Copy statement. The same /Copy statement should be used in the calling program as well, replacing the hard-coded prototypes used in the earlier illustrations. Note that I have also included the use of the optional parameters here that I utilized in the later versions of the prototype above.
If the source member named PA0132PR contains the following code...
D CalcTaxes PR ExtPgm('PA0132')
D TotalPay 7P 2 Const
D Allow_Nbr 3S 0 Const
D FedTaxOutput 5P 2
D StTaxOutput 5P 2
D MedDeduct 3P 2 Options(*NoPass : *Omit) Const
D RetireDeduct 3P 2 Options(*NoPass) Const
.
..then the source member PA0132 should contain the following:
/Copy PA0132PR
D CalcTaxes PI
D TotalPay 7P 2 Const
D Allow_Nbr 3S 0 Const
D FedTaxOutput 5P 2
D StTaxOutput 5P 2
D MedDeduct 3P 2 Options(*NoPass : *Omit) Const
D RetireDeduct 3P 2 Options(*NoPass) Const
Handling Optional Parameters in the Called Program
Remember that I mentioned earlier that the called program must be prepared for the use of *NoPass and *Omit parameters? Let's now look at how this is done.
Since the *NoPass parameters require that all parameters be passed up until the first optional one, we can simply use the %Parms built-in function to determine the number of parameters that were passed and then take the appropriate action. For any *Omit parameters, we check for a *Null value in the parameter's address. Take a look at the following example:
D CalcTaxes PI
D TotalPay 7P 2 Const
D Allow_Nbr 3S 0 Const
D FedTaxOutput 5P 2
D StTaxOutput 5P 2
D Parm5 3P 2 Options(*NoPass : *Omit) Const
D Parm6 3P 2 Options(*NoPass) Const
D MedDeduct S 3P 2
D RetireDeduct S 3P 2
/Free
If %Parms >= 5 and %Addr(Parm5) <> *Null;
MedDeduct = Parm5;
Else;
MedDeduct = 0;
EndIf;
If %Parms = 6;
RetireDeduct = Parm6;
Else;
RetireDeduct = 0;
EndIf;
Note the names of the last two (optional) parameters in the PI. It is critical that you never attempt to touch a parameter that was not passed to you, either because of *NoPass or *Omit. Therefore, elsewhere in the program logic, we will never again refer to fields Parm5 or Parm6. Instead, we will refer to the MedDeduct and RetireDeduct standalone fields to get the data. If either of those two parameters were not passed to the program, we'll simply plug in default values.
Note that in this case, we could have omitted the "Else" logic if the program returned with LR on each time because the two standalone fields would be initialized to 0 already. Also note that if the optional parameters were not constant, we would need to do a similar (but reversed) type of logic to populate the last two parameters after the calculations are done before returning to the caller.
The Options parameter for prototypes has several other useful capabilities, such as the following:
-
*VarSize allows character fields that are shorter than specified in the prototype to be passed. (Note that longer character fields can always be passed for character fields.)
-
*RightAdjust automatically right adjusts a character value passed as a CONST parameter.
-
*Trim automatically trims leading and/or trailing spaces from the value before passing it using a CONST parameter.
While you can only use it when calling bound procedures (not programs), another useful prototype keyword is VALUE. This tells the compiler to pass the actual value of the field rather than a pointer to the value in the calling program.
I hope this look into the power of RPG prototyping has helped you to understand more reasons to use prototypes than simply to allow a call in /Free logic. Utilizing the full capabilities of prototypes can make your life as a programmer easier and make your programs more bulletproof.
LATEST COMMENTS
MC Press Online