Properly Placed Procedures
January 4, 2006 Hey, Ted
I like your IIF function. As you stated, it would be nice to replace five lines of code with one. However, I don’t want to include all the subprocedure code in every program that uses IIF. Can you give instructions on how to make this a real function or service program? –Armando There’s nothing unfair about Armando’s request. When I wrote the article to which he refers, I focused on the function itself rather than how to implement it. I hear quite often from readers who want to use subprocedures, but they aren’t sure how to get started. Since subprocedures are still so new to so many of you, let’s look at how to put subprocedures to work. First, let’s get a big assumption out of the way. For purposes of this article, I assume you already know how to write a subprocedure. If you don’t, I suggest you read Subprocedures: Better than Subroutines and ILE Static Binding. Alternatives There are three places you can put a subprocedure.
All three are valid places to put subprocedures, although in my opinion, numbers 1 and 3 are superior to number 2. Let’s consider the three different approaches to subprocedure placement by tackling a problem. An Example Suppose you’re writing an RPG program that, among other things, must process a field in a certain way. Here are the rules.
For example, Joe Smith divides into Joe and Smith, and Shicargo Illinoise divides into Shicargo and Illinoise. So far, so good? First think about how to divide such a field using only the resources available from the compiler. That is, how could you solve this problem without writing a subprocedure? There are many ways you could extract the values. Here’s a method that divides NAME into NAME1 and NAME2. FSomeFile if f 80 disk Fqsysprt o f 132 printer D Input ds 80 D InputData 1 30 D Counter s 3p 0 D Name s 30a D BlankPos s 10i 0 D Name1 s 25a D Name2 s 25a D /free *inlr = *on; dow '1'; read SomeFile Input; if %eof(); leave; endif; Name = InputData; Counter += 1; clear Name1; clear Name2; if Name <> *blanks; BlankPos = %scan(' ':Name); if BlankPos > *zero; Name1 = %subst(Name:1:BlankPos-1); if BlankPos < %len(Name); eval Name2 = %trim(%subst(Name:BlankPos+1)); endif; else; Name1 = Name; endif; endif; except pline; enddo; /end-free Oqsysprt e pline 001 O Counter 4 O Name1 + 1 O Name2 + 1 For the sake of simplicity, this solution ignores the possibility that the input might be invalid. For example, if there are three or more character values in a field, the first value gets loaded into the first variable and everything else is placed into the second variable. Other solutions would achieve other results, given invalid data. But error handling will pull me off topic, so I’m not going to address it. Now, what if there is not one field that must be divided, but a dozen? My solution could be shortened, but that wouldn’t change the fact that the code must be duplicated for each field. Since it is possible to introduce errors when copying source code, each field must be debugged. However, if I could write an error-free routine that could be used for any field, I would eliminate a lot of the possibility that something might go wrong. Subprocedures do not have to implement reusable solutions, but whenever you come across a task that is to be done in more than one instance, you would do well to ask yourself if a subprocedure might be useful. Here is the same program implemented with a DivideChar subprocedure. H dftactgrp(*no) actgrp(*new) FSomeFile if f 80 disk Fqsysprt o f 132 printer D Input ds 80 D Counter s 3p 0 D Name s 30a D Name1 s 25a D Name2 s 25a D Address4 s 30a D City s 25a D State s 25a D TagData s 30a D ModelNumber s 25a D SerialNumber s 25a DDivideChar pr D ValueToDivide 30a value D Value1 25a D Value2 25a /free *inlr = *on; dow '1'; read SomeFile Input; if %eof(); leave; endif; Name = %subst(Input:1:30); Address4 = %subst(Input:31:30); TagData = %subst(Input:61:20); Counter += 1; callp DivideChar (Name: Name1: Name2); callp DivideChar (Address4: City: State); callp DivideChar (TagData: ModelNumber: SerialNumber); except pline; enddo; /end-free Oqsysprt e pline 001 O Counter 4 O Name1 + 1 O Name2 + 1 Oqsysprt e pline 001 O City + 4 O State + 1 Oqsysprt e pline 001 O ModelNumber + 4 O SerialNumber + 1 * =========================================================== PDivideChar b D pi D ValueToDivide 30a value D Value1 25a D Value2 25a D D BlankPos s 10i 0 /free clear Value1; clear Value2; if ValueToDivide <> *blanks; BlankPos = %scan(' ':ValueToDivide); if BlankPos > *zero; Value1 = %subst(ValueToDivide:1:BlankPos-1); if BlankPos < %len(ValueToDivide); eval Value2 = %trim(%subst(ValueToDivide:BlankPos+1)); endif; else; Value1 = ValueToDivide; endif; endif; /end-free P e Implementing this routine as a subprocedure required two new sections of code. The first is the procedure prototype, which is located in the D specs of the main program. The second is the subprocedure itself, which follows the O specs. Because of the subprocedure, it is easy to divide many fields without duplicating code, as the three CALLP operations demonstrate. callp DivideChar (Name: Name1: Name) callp DivideChar (Address4: City: State) callp DivideChar (TagData: ModelNumber: SerialNumber) Placement The easiest way to implement a subprocedure is to place it at the end of a program, just before the compile-time array data. Since I rarely use O specs or compile-time arrays, the subprocedures end up following the C specs. Can you think of anything else that goes at the bottom of the C specs? If you’re thinking about subroutines, good for you. When you embed a subprocedure in an RPG IV program, you are writing super subroutines–subroutines that can define local variables and share parameters with calling routines. If the RPG compiler would automatically generate procedure prototypes for unprototyped internal subprocedures, I would never use a subroutine. This is the way I implemented the IIF procedure in the article to which Armando referred. I included IIF in the program for purposes of illustration only. I was not suggesting that IIF should be included in every program in which it is used. Defining a subprocedure in a program is fine for tasks that pertain to that program only. In the case of a routine that may be useful in other programs–routines like IIF and the character division routine–placing the routine somewhere generally accessible makes a lot more sense. This is the point of Armando’s correspondence. He could include the source code for IIF in every program that uses it, either by copying the code while editing or by using the /COPY or /INCLUDE compiler directives. However, if he someday changes the IIF routine, every program that uses IIF will have to be recompiled. I will grant that the chances of changing IIF are pretty slim, but you would not be so lucky with other routines. For instance, you might choose to modify the DivideChar routine to allow quotation marks around character values. Such a modification would permit a character value to contain blanks. “Lost Angeles” “Peoples Republic of California” would divide into Lost Angeles and Peoples Republic of California. Wouldn’t it be nice if you could change the subprocedure one time only? If you’re using the /COPY or /INCLUDE compiler directives to include the subprocedure’s source code, you only have to make one change, but you still have to recompile all the programs that use the subprocedure. Let’s look for a better way, a way that permits callers to access the latest version of DivideChar without recompiling. Modules Like a program, a module is an object that contains compiled procedural code. However, unlike a program, a module cannot be directly executed with a CALL command. Instead, the routines within the module must be invoked from other procedures. The source member for the module needs the NOMAIN keyword in the H spec to tell the compiler not to include the RPG cycle in the object code. The beginning P spec for each procedure in the module must have the EXPORT keyword if that procedure is to be available to callers outside the module. Here’s the source code for module DIVIDECHAR, which contains the DivideChar subprocedure. H nomain D/copy prototypes,DivideChar * ================================================================= PDivideChar b export D pi D ValueToDivide 30a value D Value1 25a D Value2 25a D D BlankPos s 10i 0 /free clear Value1; clear Value2; if ValueToDivide <> *blanks; BlankPos = %scan(' ':ValueToDivide); if BlankPos > *zero; Value1 = %subst(ValueToDivide:1:BlankPos-1); if BlankPos < %len(ValueToDivide); eval Value2 = %trim(%subst(ValueToDivide:BlankPos+1)); endif; else; Value1 = ValueToDivide; Value1 = ValueToDivide; endif; endif; /end-free P e Notice the /COPY directive. It retrieves the procedure prototype for the DivideChar subprocedure from a source physical file named PROTOTYPES. Calling RPG modules must also copy the prototype. DDivideChar pr D ValueToDivide 30a value D Value1 25a D Value2 25a Use the Create Module (CRTMOD) command to create the module. (No, you’re not creating a mode; IBM was using MOD for modes before modules came about. I suppose they decided they wouldn’t need any more mode commands.) CRTRPGMOD MODULE(mylib/DIVIDECHAR) SRCFILE(mylib/QRPGLESRC) SRCMBR(DIVIDECHAR) Suppose another program–we’ll call it X–needs to use the DivideChar function. Here’s X. FSomeFile if f 80 disk Fqsysprt o f 132 printer D Input ds 80 D Counter s 3p 0 D Name s 30a D Name1 s 25a D Name2 s 25a D Address4 s 30a D City s 25a D State s 25a D TagData s 30a D ModelNumber s 25a D SerialNumber s 25a D/copy prototypes,DivideChar /free *inlr = *on; dow '1'; read SomeFile Input; if %eof(); leave; endif; Name = %subst(Input:1:30); Address4 = %subst(Input:31:30); TagData = %subst(Input:61:20); Counter += 1; callp DivideChar (Name: Name1: Name2); callp DivideChar (Address4: City: State); callp DivideChar (TagData: ModelNumber: SerialNumber); except pline; enddo; /end-free Oqsysprt e pline 001 O Counter 4 O Name1 + 1 O Name2 + 1 Oqsysprt e pline 001 O City + 4 O State + 1 Oqsysprt e pline 001 O ModelNumber + 4 O SerialNumber + 1 The DivideChar subprocedure is not defined in X. When X needs to use the DivideChar function, it must be told to bind to module DIVIDECHAR. First, create a module from X. Then create a program from the X and DivideChar modules, as the following commands illustrate. CRTRPGMOD MODULE(mylib/X) SRCFILE(mylib/SRC) SRCMBR(X) CRTPGM PGM(mylib/X) MODULE(X DIVIDECHAR) Using a module is an improvement over including the DivideChar source code in each program that uses it, but it’s not much of an improvement. If you modify DivideChar, you must either recreate the calling programs or use the Update Program (UPDPGM) command to rebind the new module to each program that uses it. Here’s the command to update program X with the new version of the DivideChar subprocedure. UPDPGM PGM(X) MODULE(DIVIDECHAR) Wouldn’t it be nice if you could change the subprocedure in such a way that all the callers would use the new version without having to be recompiled or updated? You can, if you convert the module to a service program. Service Programs A service program is a collection of subprocedures that is loaded into memory as needed at runtime. It is the iSeries equivalent of a dynamic link library (DLL) in the Windows world. The procedures in a service program are usually related in some way. For instance, a service program might contain procedures for working with character strings. Each service program needs a source member of binder source to define the exports. If you were to install the DivideChar subprocedure, you would place the following code in member DIVIDECHAR of source physical file QSRVSRC. STRPGMEXP PGMLVL(*CURRENT) SIGNATURE("CHAR") EXPORT SYMBOL('DIVIDECHAR') ENDPGMEXP The following command creates a service program from the DIVIDECHAR module. Notice that it refers to the DIVIDECHAR module and to the binder source. CRTSRVPGM SRVPGM(mylib/DIVIDECHAR) MODULE(mylib/DIVIDECHAR) EXPORT(*SRCFILE) SRCFILE(mylib/QSRVSRC) SRCMBR(DIVIDECHAR) Once the service program exists, you are finished with the DIVIDECHAR module and can delete it if you wish. The final step is to bind program X to the service program instead of binding it to the DIVIDECHAR module. CRTPGM PGM(mylib/X) MODULE(mylib/X) BNDSRVPGM(mylib/DIVIDECHAR) If you modify the DivideChar routine, recreate the module, and update the service program, program X will automatically use the new DivideChar routine without having to be recompiled. Here’s a summary of what Armando needs to do to implement IIF in a service program.
I would add one more step.
But the subject of binding directories is too big to be addressed here. I will try to tackle it in another article. Rule of Thumb Here is the rule of thumb I’m working under at present. If a subprocedure is not a reusable routine, i.e., it has meaning only to one program, I include it within the source code of the program that uses it. If the subprocedure is a generic routine, i.e., it implements a business process or a utility function, I try to get it into a service program. There is, of course, no reason why a subprocedure can’t begin its existence as an internal subprocedure, and then later be converted into a service program or added to an existing service program. Resolutions I don’t make new year’s resolutions, but if I did, I would likely resolve to focus more on the fundamentals in Four Hundred Guru during 2006. I write with the assumption that my readers are intelligent professionals, and I believe that’s a valid assumption. However, knowing how busy my readers are, I should not assume that they have time to fill in all the gaps in their understanding in order to put a tip or technique to work. In spite of the busyness of my everyday life, I’ll try to be more detailed and provide better explanations of my code in the future. I will depend on the readers, especially those of you who have complained, to keep me straight. May 2006 be the best year ever for all of us. –Ted RELATED STORIES |