Q: I would love to see some articles about subprocedures in RPG. I have read many articles that say, "get rid of subroutines and use subprocedures." I have a basic understanding of them, but I keep running into stumbling blocks and give up.
The main thing is file access. How do you pass record formats back and forth? Or maybe you shouldn't? Normally I write subroutines that retrieve records from files. I would like to convert these subroutines to procedures. How can/should the procedures pass the fields from the retrieved record back?
A: The first thing that I'd like to point out is that subprocedures have a superset of the functionality that subroutines do. That is, anything that you can do with subroutines can be done the same way with subprocedures.
Consider the following "traditional" code snippet. It performs the file access needed to load a record from an item master file:
. . .
* Copy Item number keyed in to screen (scItem) to the
* one required by the subroutine, then load the
* record.
C eval Item = scItem
C exsr LoadItem
* Check to see if it succeeded
C if Fail = 'Y'
c eval ErrMsg = 'Item Not Found'
c endif
. . .
*-------------------------------------------------------------
* Load an Item from ITMMAST file
*-------------------------------------------------------------
C LoadItem begsr
C eval Fail = 'N'
C Item chain ITMMAST 99
C if *IN99 = *ON
C eval Fail = 'Y'
C endif
C endsr
It would not be difficult to convert this code to use a subprocedure if you used the subprocedure the same way you use the subroutine. The following example illustrates this:
D LoadItem PR
. . .
* Copy Item number keyed in to screen (scItem) to the
* one required by the subroutine, then load the
* record.
C eval Item = scItem
C callp LoadItem
* Check to see if it succeeded
C if Fail = 'Y'
c eval ErrMsg = 'Item Not Found'
c endif
. . .
P LoadItem B
D LoadItem PI
C eval Fail = 'N'
C Item chain ITMMAST 99
C if *IN99 = *ON
C eval Fail = 'Y'
C endif
p E
See what I mean? A subprocedure can be used in the same manner as a subroutine, so there's no reason to keep using subroutines. Granted, the code above isn't very reusable or modular. Although it's a valid subprocedure, it doesn't give you any advantages over what you might've done with a subroutine. However, it's a step in the right direction!
Very few people will learn things well if they have to learn too much at once. Subprocedures are no exception. Start working with them, even if it's doing very basic things. Once you're past that first "baby step," the next steps won't seem so big.
In the subroutine example, I defined a field called "Item" because I wanted to keep the subroutine generic enough that it could be called from anywhere in my program. In the one place I called it from, I copied scItem to Item. I didn't want to use scItem in the subroutine directly because I might later want to use a different field for the key. For example, I might later be reading an order file, and for each item on the order, I want to get the item master record so that I can print the item's description. Likewise, I created a field called "Fail" that the subroutine will always set to indicate whether the CHAIN operation succeeded or failed.
These fields represent the program's "interface" to the subroutine. The only way they should affect the way the subroutine operates is by putting a different value in the Item field, and the only way they should determine that it failed is by checking the Fail field. My program doesn't use these fields for anything else besides input and output to this subroutine. This helps make my routine reusable.
There are still problems, however. Although I've done a nice job of creating an interface, I have no way to ensure that the program won't, for example, modify indicator 99. Since these other fields in the routine are available everywhere in the program, I'm limited in how reusable my subroutine can be. Any time I want to use it in a different program, I have to be very careful to make sure that when it changes *IN99, it doesn't affect how the program operates.
Subprocedures make it easier to write reusable code because they give us the added capabilities of parameters and local variables. By defining the interface to the subprocedure with parameters, I know exactly what goes into and out of my routine. If any temporary variables that my routine uses (like *IN99 in the previous example) are done with local variables, I know that I won't affect the rest of the program.
Consider the following example:
D LoadItem PR 1A
D Item 5 0 const
. . .
C if LoadItem(scItem) = 'Y'
c eval ErrMsg = 'Item Not Found'
c endif
. . .
P LoadItem B
D LoadItem PI 1A
D Item 5 0 const
D Fail s 1N
C eval Fail = 'N'
C Item chain ITMMAST
C if not %found
C eval Fail = 'Y'
C endif
C return Fail
p E
I'd like to point out that in the code above, I've actually reduced the amount of code in the mainline. When you're attempting to write code that's written once, and used many times, it's always a good thing when you make it easier to use!
Of course, there's still information that's being modified by this subprocedure that's not being passed in and out via parameters, that being the record itself!
One of the unfortunate things about ILE RPG is that file access is always global to the entire module. Anytime you use a file in a subprocedure, it will always affect things that are outside the subprocedure since the file is located outside the subprocedure.
Even though there isn't a perfect solution to this problem, it's still a lot better than using subroutines! With subprocedures, you can keep the amount of data that's not part of the interface to a minimum.
Starting in V5R2, you can use the LIKEREC keyword to create a data structure that has the same fields as a record format. In free format RPG, you can specify the data structure in the result field of a file operation so that the system will load the file data into the data structure. You can also pass this data structure as a parameter, so that the file fields become part of the well-defined interface to the subprocedure.
The following sample code illustrates this technique:
. . .
D LoadItem PR 1N
D Item 5 0 const
D Record likerec(ITMMASTF:*INPUT)
. . .
c if LoadItem(scItem:rec) = *OFF
c eval ErrMsg = 'Item Not Found'
c endif
. . .
P LoadItem B
D LoadItem PI 1N
D Item 5 0 const
D Record likerec(ITMMASTF:*INPUT)
/free
chain Item ITMMASTF record;
if not %found;
return *OFF;
endif;
return *ON;
/end-free
P E
When you use this technique, it's important to remember that the data structures declared with LIKEREC are like a record format, not a file. You have to perform your file operations using the record format name instead of the file name, or they won't work. In the example above, ITMMASTF is the record format name used in the ITMMAST file.
This still isn't a perfect solution. Although the fields from the file are now all passed as parameters, it still changes the file pointer, which might affect another routine in the program.
Of course, now that you're using a subprocedure instead of a subroutine, you can put it into a separate module so that it'll have its own copy of the file and not affect anything else.