Programs and procedures that perform general tasks are the hallmark of a well-designed computer system. An example of a general-purpose procedure is one that takes a customer number as a parameter and returns a formatted string of the customers name, complete with title and initials, suitable for printing on letterhead or envelopes. Of course, not every general-purpose task is so simple. Even formatting a customer name can be complex. Do you want to place the surname first or last? Should you use initials or full first names? Maybe youd rather not precede the name with Mr. or Ms. Passing extra parameters that specify exactly how you want the name formatted improves the usefulness and generality of the procedure. But whether the task at hand is simple or complex, one thing remains the same: After you have called the procedure, you dont want to hear from it again until it has done its job. We RPGers have become so used to this discipline that the notion of call- back may, at first, seem a little foreign.
Call-back is a means by which a general-purpose procedure can interrogate the waiting calling program. It does this by executing logic that resides within the caller. In some situations, call-back is superior to adding extra parameters to improve a general- purpose procedures usefulness. Take a look at an example of just such a situation.
A Simple Prompt Program
I have a simple window program that I call PartsPrompt. Its purpose is to present a list of automotive parts and allow a 1 to be entered against any line, indicating that the part number displayed on that line is to be returned to the caller. This program is called from dozens of interactive programs that require a part number to be entered. If users dont know a part number, they can press F4 on the part number field to make the PartsPrompt appear. They can then either pick a part or press F3 to return. The prototype to this program (Figure 1) takes only one parameter and returns a part number.
The example code in Figure 1 shows how the PartsPrompt is used. The C-spec comes from the F4 logic of an interactive program. X_PART is the part number field displayed. Passing the current value of X_PART to PartsPrompt causes PartsPrompt to position automatically to the nearest part with a matching value to X_PART. If users select a part, the program will return that part and load X_PART with it. If users press F3 to exit PartsPrompt, the current value of X_PART will be returned.
PartsPrompt works fine when all the user wants to do is select one part, but my users asked me to beef up the programs usefulness. I have a few subfile programs that allow entry of multiple lines of parts. When users press F4, they want to key a 1 against several parts, not just one. They also want any parts already entered in the subfile to appear in the PartsPrompt with a 1 preloaded against them. Then, if they clear the 1 in PartsPrompt, that means they want it removed from their subfile. And just to make my job interesting, they want a line to appear at the bottom of PartsPrompt that shows a running total of the cost of the parts picked so far. These demands come from the users in the service department. The guys in the retailing department want the same capability, but their running total needs to show not cost but total retail price of parts selected so far. Maybe you can think of a way to do all this using extra parameters, but I suspect youd end up being the only person in the company able to maintain something so cumbersome. Call- back offers a more elegant solution, addressing all of these requirements while keeping the PartsPrompt relatively simple.
Need to Know
The Need to Know principle is one you should consider when designing general-purpose procedures. Although the requirements I had for PartsPrompt may seem complicated, just how much of it is truly necessary for the job of selecting parts? Is PartsPrompt really interested in adding up totals? Is it really interested in cost versus retail? Should it care that the caller has already selected half a dozen parts or none at all? Just what does PartsPrompt really need to know from or tell to the caller? Heres a list of PartsPrompts bare necessities:
A string that it will display at the foot of the list
The knowledge of whether it should display a 1 against a line
An indication that a 1 was entered against or removed from a part If PartsPrompt can call three procedures allowing it to fulfill these requirements, it can then fulfill the new requirements as well. These three procedures (getString, IsPicked, and Pick) and their prototypes are shown in Figure 2.
Now, consider how PartsPrompt uses the three procedures. The running total part is the simplest. Just prior to displaying its screen format, PartsPrompt calls getString and displays whatever it returns. This information could be cost total, retail total, or a birthday greeting. PartsPrompt doesnt know what its displaying, nor does it care. Nice and easy. As for showing whether a part is already selected, PartsPrompt just calls IsPicked for every line that its about to display, passing the part number as a parameter. The 1 or 0 returned is what gets displayed against the line. Finally, every time the user presses the Enter key or scrolls, PartsPrompt calls Pick for every line displayed, passing both the part number and the action keyed against the line (either a 1 or 0).
PartsPrompt is now a great deal more capable than it was before and with very little coding effort required.
But wait a minute! Just where are the procedures getString, IsPicked, and Pick? Well, this is how the term call-back arose. The procedures are in the calling program. PartsPrompt is calling back to the program that initially called it. Thats because the calling program is in the best position to know whats already picked and what running total should be displayed at the bottom. In effect, PartsPrompt is saying it can provide multiple selection and a running total display so long as the caller provides the getString, IsPicked, and Pick procedures. (Java programmers will recognize this principle as implementing an interface.)
Executing Call-back Procedures
Ive outlined what procedures PartsPrompt needs to call to perform its new duties, and Ive stated that these procedures reside in the calling program, but Ive made no mention about how PartsPrompt is able to execute a procedure in the caller. This is done using procedure
pointers. Take a look at the new PartsPrompt prototype in Figure 3 and how the three call- back procedures are declared within PartsPrompt. Ive made the three new parameters optional so that all programs currently using PartsPrompt (in single-selection mode) continue to work as before. The C-specs are the first few lines of the PartsPrompt mainline processing. These lines ensure that the rest of the program can reference IsPicked@, Pick@, and getString@ without needing to check if they were passed by the caller.
Youll notice that the three call-back procedure prototypes have the keyword extproc defined. Extproc can take two kinds of arguments: either a string enclosed in quotation marks or the name of a procedure pointer. If the argument is a string enclosed in quotes, it is assumed to be the name of another procedure. A call to the procedure named in the prototype becomes a call to the procedure named in the extproc keyword. When the argument to extproc isnt enclosed in quotes, as in the call-back prototypes, it is assumed to be a procedure pointer. A procedure pointer is an address, a way of identifying a procedure by its address in memory rather than by its name. Youre forced to used procedure pointers for call-back because RPG provides no useful way of passing procedure names from one program to another. Sure, you can load up a character field with the name of a procedure and pass the field, but RPG cant execute a character field, only literal strings that explicitly name a procedure or procedures that are based on procedure pointers.
In Figure 3, you can see that the procedure IsPicked is based on the procedure pointer IsPicked@. Immediately above the prototype is the definition of IsPicked@. You define a procedure pointer by keying a * in the data type column, meaning pointer, and qualify the pointer as a procedure pointer by the keyword procptr. (If you leave out the procptr keyword, IsPicked@ is understood to be a data pointer). What this definition is saying is this: There is a procedure somewhere called IsPicked that takes a 20-alpha parameter and returns a 1-packed field. Just where IsPicked actually resides remains unknown until IsPicked@ is loaded with its address in memory.
Its up to the calling program to pass the addresses of IsPicked, Pick, and getString to PartsPrompt. Take a look at how it does just that.
Defining Call-back Procedures
In my case, the programs calling PartsPrompt are multiline subfiles, and Ive coded the IsPicked, Pick, and getString procedures with a subfile structure in mind. However, Im going to show how you would code these three procedures for a calling program that stores its part numbers in an array, not a subfile. Not only is this simpler, but it also demonstrates the generality of the call-back design. Remember, PartsPrompt can do its job as long as it is provided with the addresses of the IsPicked, Pick, and getString procedures.
PartsPrompt is not concerned with the internal workings of its caller. Take a look at Figure 4 to see how this calling program defines the three procedures.
Because the logic in Figure 4 is straightforward, I wont explain it any further. Suffice it to say that theres not much to it (although you should note that users will encounter problems if they select more than 50 parts). All that remains is to see how the calling program tells PartsPrompt about these procedures. Figure 5 demonstrates the actual call. (Notice that, when taking the addresses of the procedures, I have to enclose their names in quotes and put them in uppercase. This formatting is necessary because all RPG symbols are stored internally in uppercase, regardless of how they were coded in the source.) The program uses the built-in function %paddr to find out the address of a procedure. By passing the addresses of IsPicked, Pick, and getString (which reside in the calling program) to PartsPrompt, PartsPrompt now knows where to locate them. While the calling program waits, PartsPrompt executes these procedures as the user scrolls through the parts list, picking and unpicking parts, with a running total being updated in real time at the foot of the screen. As soon as PartsPrompt ends and control returns to the caller, nothing more need be done. The callers internal array of parts has already been updated!
Gotcha!
I left one little detail out. What happens if users pick a few parts in PartsPrompt, decide they dont want them after all, and press F3 to return to the calling program? The calling program wouldnt be as they left it but would instead include the parts they had selected in PartsPrompt before pressing F3.
My solution to this problem is to make a copy of current parts selected in the calling program immediately prior to calling PartsPrompt. In the example, making a copy equates to a simple MOVEA statement to a backup array. If users press F3 in PartsPrompt, I return the string F3 as the return value of PartsPrompt. (Recall that the return value of PartsPrompt is the part number selected when in single-selection mode. When you are performing a multiple selection, its safe to use the return value for another purpose.) If PartsPrompt returns F3, I then reinstate the parts that were recorded in the copy.
************************************************************
* Show list of parts, allow selection of one part only
D PartsPrompt PR 20A
D PosnToPart 20A value
C eval X_PART = PartsPrompt(X_PART)
************************************************************
Figure 1: The PartsPrompt prototype takes a single parameter and returns a part number.
************************************************************
* get a string to display at the bottom of PartsPrompt
D getString PR 80A
* Is a part already picked? Returns 1 or 0.
D IsPicked PR 1P 0
D PartNumber 20A value
* Pick/UnPick a Part.
D Pick PR
D PartNumber 20A value
D OneOrZero 1P 0 value
************************************************************
Figure 2: These procedures provide PartsPrompt with multiple-picking and running-total capability.
************************************************************
D PartsPrompt PR 20A
D PosnToPart 20A value
D OptIsPicked@ * procptr value options(*NOPASS)
D OptPick@ * procptr value options(*NOPASS)
D OptgetString@ * procptr value options(*NOPASS)
* Call-Back procedures:
D IsPicked@ S * procptr inz(*NULL)
D IsPicked PR 1P 0 extproc(IsPicked@)
D PartNumber 20A value
D Pick@ S * procptr inz(*NULL)
D Pick PR extproc(Pick@)
D PartNumber 20A value
D OneOrZero 1P 0 value
D getString@ S * procptr inz(*NULL)
D getString PR 80A extproc(getString@)
C if %parms > 1
C eval IsPicked@ = OptIsPicked@
C endif
C if %parms > 2
C eval Pick@ = OptPick@
C endif
C if %parms > 3
C eval getString@ = OptgetString@
C endif
************************************************************
Figure 3: Call-back procedures are declared within this new PartsPrompt prototype.
************************************************************
* Global definition of the parts array:
D Parts S 20A dim(50)
*----------------------------------------------------------*
P getString B
D getString PI 80A
* Locals:
D TotalCost S 9P 2 inz(0)
D I S 10I 0
C do 50 I
C if Parts(I) = *BLANKS
C eval TotalCost = TotalCost
C + RtvPartCost(Parts(I))
C endif
C enddo
C return 'Cost of parts selected so far: '
C + %trim(%editc(TotalCost:'L'))
P E
*----------------------------------------------------------*
P IsPicked B
D IsPicked PI 1P 0
D PartNumber 20A value
C PartNumber lookup Parts 01
C if *IN01
C return 1
C else
C return 0
C endif
P E
*----------------------------------------------------------*
P Pick B
D Pick PI 80A
D PartNumber 20A value
D OneOrZero 1P 0 value
* Locals:
D I S 10I 0 inz(1)
C PartNumber lookup Parts(I) 01
C select
*C when OneOrZero=0
C if *IN01
C eval Parts(I) = *BLANKS
C endif
*C when OneOrZero=1
C if not *IN01
C eval I=1
C *BLANKS lookup Parts(I) 01
C eval Parts(I) = PartNumber
C endif
*C endsl
P E
*----------------------------------------------------------*
************************************************************
Figure 4: A calling program that stores part numbers in a 50-element array defines the three call-back procedures.
************************************************************
C callp PartsPrompt(*BLANKS
C :%paddr('ISPICKED')
C :%paddr('PICK')
C :%paddr('GETSTRING'))
************************************************************
Figure 5: This is the actual call of the PartsPrompt program.
LATEST COMMENTS
MC Press Online