Prototypes are a topic that most RPGers know a little about because they are required for coding program and procedure calls in free format RPG. However, we find that many don't fully understand prototyping and the true value that they bring to the table. We've written about them, singing the praises of prototypes in EXTRA for a while now - beginning in 2001 where we discussed "RPG IV Prototypes: What are they and why should you care?" That article covered the basics of prototypes for program calls (only). We covered some other parameter keywords in later writings on the subject, including "The Case of the Missing Parameters" There we discussed how to make your called routines more flexible by using some additional keywords to deal with optional parameters. We followed that up with some other writings about using prototypes with bound procedure or function calls.
Since we've covered it before, you may be wondering why we feel the need to revisit the topic now. There are three primary reasons:
- When chatting recently with RPGers on forums, via email or in person at conferences and classes that we teach, we’ve noticed that many RPGers don't seem to fully understand how prototypes work and how much they can do to help simplify and bullet proof your code.
- Often when we have those conversations, we like to be able to point to an article or two where users can learn more about the topic. We feel guilty pointing them to some of those early articles because the coding style (much of it written before free format was available) is painful to look at and may be difficult to understand for new RPGers.
- Many enhancements have been made to support for prototypes in the intervening years so we want to be able to include some updates.
So, what we decided to do is to update and enhance the examples from some of those older articles and review some of the more critical points in them. We don't intend to cover all the details from those earlier pieces; you may still want to read them (just brace yourself to deal with the old coding style!)
This will be a multi-installment article series. In this first installment, we'll be reviewing and updating the examples from the first two articles.
Quick Review and Update on the Basics
First, let’s go back to the first of the prototype articles, updating the code examples and reviewing the main points in the answer to "why should you care?"
Our example calls a tax calculation program named PX027C passing three parameters. The first prototype and call would look like this in today's RPG coding style.
Dcl-PR TaxCalc ExtPgm('PX027C');
TaxCalc( Gross : Allow : Net);
There are some big advantages of this over the old-style Call using Parm or PList:
- First, we can give programs a more meaningful name in the program for readability than naming standards normally dictate.
- We can also use meaningful descriptive names for the parameters in the prototype since the names are documentary only.
- Most importantly, the compiler will check that the parameters you pass on the call match in data type and size to the ones specified in the prototype.
In the example above, if the field named Gross were actually a packed field or a zoned field of length 7:2 instead of 9:2, then a compile error would result. That error would be a good thing since it's much easier to fix parameter mismatch errors at compile time than trying to figure out what went wrong later during testing, or even worse, in production.
Of course, today’s prototypes have an even more significant benefit compared to the time of the original article: They facilitate program calls from free format logic.
Prototypes can fix Things for us
Two very small enhancements to the prototype can make it even more powerful.
Dcl-PR TaxCalc ExtPgm('PX027C');
GrossPay zoned(9:2) Const;
Allowances zoned(7:2) Const;
By adding the Const (constant) keyword to the input parameters, we give the compiler permission to "fix" any small parameter mismatches that may occur. In this case, if the Gross field was the wrong size or a different numeric type, such as packed, the compiler would deal with it. It would generate the necessary code to create a temporary field of the correct type and size, copy the value from Gross into it, and then pass that temporary as the parameter. In addition, the Const keyword goes further and also allows the programmer to pass a constant, a literal, or an expression. We'll see a simple example of that a little later.
The use of Const is one of those prototype features that we feel is under-utilized. Why write (and maintain forever) extra code just to move values around to other fields to change the data type and/or size of the data being passed? It's also handy to be able to use a built-in function as a parameter, for example using%Len() for as a parameter to an APIs which requires that you specify the length of a string.
We always specify Const on any parameters that should not be changed by the called routine. Of course, those that need to have their values changed by the called routine (such as NetPay in our example) cannot specify Const. As a result, those will need still need to match in size and type exactly. This is because the extra code that may be generated for Const parameters only moves the value to the temporary field before the call—it does not copy the temporary field back into the original field on returning from the call. Some people refer to Const parameters as "read only" but in practice there is no absolute guarantee that the value in a Const parameter cannot be changed. When we use Const we are essentially saying that we do not expect the called routine to change the value.
Need More Flexibility With Parameters?
Suppose we have a program or procedure that is called from many places, then we decide to add some new functionality to it that requires that additional parameter(s) to be passed. Rather than have to go back and modify all the places where that code is currently being called, prototypes give us the option of specifying that some of our parameters are optional. This is done via values for the Options keyword.
The *NoPass option means that the parameter may or may not be passed. It may only be used for parameters at the end of the list. The *Omit option can be used anywhere in the parameter list. However, with *Omit you must specify the special value *Omit as a place holder for the parameter value. With *NoPass, you simply omit the parameter(s) you don't need. Note though that once you omit one parameter, you must also omit the remaining parameters as well.
Let's look at an updated example that exploits these features. The routine LastDayOfMonth returns the date of the last day of the month. The original version required that a single date parameter be passed. We’ve now enhanced the functionality so that if no parameters are passed (or the special value *Omit is used) the current date is used, so the last day of the current month is returned. If a single parameter (a date) is passed, as in the original version, then the last day of the month of the date passed is returned. But if both parameters (a date and a number of months) are passed, the date returned is the last day of the month the specified number of months after the passed date.
With the prototype shown below, all the calls following it are valid.
Dcl-Pr LastDayOfMonth Date(*USA);
InpDate Date(*USA) Const Options(*NoPass : *Omit);
Months Packed(3) Const Options(*NoPass);
Day = LastDayOfMonth();
Day = LastDayOfMonth(MyDate);
Day = LastDayOfMonth(*Omit : MonthsToAdvance);
Day = LastDayOfMonth(%Date(NumericDate:*YMD) : 3);
Note that due to *NoPass,we can omit (completely) one or both of the two parameters, as in the first two calls. In the third call we must specify *Omit as a placeholder for the parameter we don't want to pass because we need to pass the second parameter. The last example uses all the possible parameters. It also takes advantage of Const to allow passing a built-in function (%Date) and a literal numeric value (3).
One of the most important things to understand about using these parameter options is that the routine being called must be coded to recognize and properly deal with situations where it does not receive a value. It’s critical to ensure that the called routine doesn't use (or even look at) the value of any parameter that was not passed. You can't rely on there being any special value such as blanks or zeroes there. You can't even rely on getting a runtime error. You may get an error, but then again, you may not.
The logic to utilize these optional parameters properly in the LastDayofMonth procedure is highlighted below. The Procedure Interface (PI) looks like this, mirroring the prototype above.
Dcl-PI LastDayofMonth Date(*USA);
InpDate Date(*USA) Const Options(*NoPass: *Omit);
Months Packed(3) Const Options(*NoPass);
We use different methods to check whether a parameter was passed, depending on whether the parameter uses *NoPass or *Omit. If (as in our example) parameters can use either *NoPass or *Omit, we need to check for both scenarios.
Here's the logic for the LastDayOfMonth procedure. We'll discuss below the methods used to determine what parameters (if any) were passed.
< B > If (%Parms < %ParmNum(InpDate)) or (%Addr(InpDate) = *Null);
DateToUse = %Date(); // Use current date
DateToUse = InpDate; // otherwise use the value passed
WorkDate = DateToUse - %Days(%SubDt(DateToUse:*D) - 1);
< A > If %Parms < %ParmNum(Months);
LastDay = (WorkDate + %Months(1) - %Days(1));
LastDay = (WorkDate + %Months(Months + 1) - %Days(1));
At < A > we’re checking to see if we received a value for the Months parameter. We use %Parms to determine how many parameters were received - in this example %Parms could return a value of 0, 1 or 2. If we received fewer than two parameters, then we know Months was not passed. This is the basis for how we deal with any *NoPass parameter.
While we could hard-code the number we're comparing to %Parms, beginning with V7.1 we have a far better and safer way - %ParmNum(). This BIF allows us to soft-code the number. In the code above, %ParmNum(Months) returns 2, because Months is currently the second parameter in the Procedure Interface. If in the future we were to change the parameter sequence such that Months was now the first parameter no change would be needed—the compiler will deal with it. Back in the "bad old days" before V7.1, we'd have had to remember to go back and change the constant 2 to 1.
At < B > we check to see if we received a value for InpDate. Because InpDate is a *NoPass parameter, we can use %Parms. However, it is also an *Omit parameter so we also need to check to determine if the *Omit special value - which translates to a null pointer - was passed. For that, we use %Addr to check for a null pointer.
We're populating the DateToUse field, used later in the logic, with either the value received or the current date in the event InpDate parameter was not received
While it takes a bit more code in the called routine to use optional parameters, think of the time you'll save if you decide to add an extra feature to an existing routine that's already called from many places. With *NoPass parameters (as long as you put any new parameters at the end of the list) you won't need to change any of the existing code where it's used.
Is That all Prototypes can do?
No, not by a long shot. We haven't touched here on the ability to pass more complex parameters such as data structures and files. Also, we've limited this discussion so far to those keywords you can use whether calling programs or procedures. There are others that are only available for bound procedure calls. In a later article, we’ll continue this discussion of the power of prototypes to include those features.