In my earlier post Subroutines versus Subprocedures I described some of the advantages of using subprocedures rather than subroutines. In it showed how you could use, what I call "open" subprocedures. An "open" subprocedure does not have a procedure interface specifications, therefore all of the variables defined in the main line/body are available in the subprocedures, and if you change the contents of one of those variables the changed value is seen in the main line/body.
The other type of subprocedure I call "closed". The subprocedure can be passed parameters and can return a single variable, which can be data structure with multiple subfields. A "closed" subprocedure is should be placed in a separate module, therefore, they are now available for multiple program to use. As the main line/body can only change the variables they pass to the subprocedure and the subprocedure can only return one pre-defined variable there is no accidentally changing to the values of the variables.
Many people commented on that post explaining the advantages of the "closed" subprocedure. I do agree and below I will describe how I code them and their advantages, including one potential gotcha.
I think it makes it easier for everyone (including myself writing this) to break this post into sections that correspond to the parts you need to be able to use "closed"/external subprocedures, these being:
Below I will be giving two examples, one with objects created for IBM i 7.2 and the other with the older V5R4.
Navigation hint: There are links in this post to other parts of this page. Feel free to make use of them to work your navigate your way around the post. When you want to return to the place where you clicked on the link rather than scroll through the post looking for that place, use your browser’s "Back" button or press the "Backspace" key on your keyboard.
The program that calls the subprocedure.
Let's start with the programs, the first the one I have written in 7.2 compatible RPG:
01 ctl-opt dftactgrp(*no) actgrp(*new) bnddir('*LIBL/TESTRPG') ; 02 /define One 03 /include mysrc,testrpg_c 04 /undefine One 05 dcl-s EmployeeNumber char(10) inz('8024') ; 06 EmployeeName = GetEmployeeName(EmployeeNumber) ; 07 EmployeeNumber = *all'?' ; 08 EmployeeName = GetEmployeeName(EmployeeNumber) ; 09 *inlr = *on ; |
The Control Options, CTL-OPT on line 1, state this program will not use the default activation group and it will run in a new activation group. I am also using a binding directory for the module that contains my subprocedure.
Lines 2 – 4 control how the procedure prototype, DCL-PR, and the data structure, EmployeeName, is copied into the source. I will explain in more detail when I discuss the copy book.
On line 5 I define the variable, EmployeeNumber, that I will be using to pass the employee to the subprocedure, and I am initializing it with the value '8024'.
I call the subprocedure for the first time on line 6. As I have passed a valid employee number the returned data structure, EmployeeName, the name subfields are filled.
On line 7 I am moving all question marks into the EmployeeNumber, which is not a valid employee number.
This time when then subprocedure is called as the employee number is not valid the EmployeeName data structure name subfields are blank and a return code subfield is not. You will see how this works when I describe how the subprocedure works.
The code for the V5R4 version of the same program looks pretty similar to the above. Except that the Control Options are fixed format in the H-spec, and the definition of the Employee Number, EmplyeeNbr, is in a fixed format D-spec.
01 H dftactgrp(*no) bnddir('TESTRPG2') 02 /define Two 03 /copy mysrc,testrpg_c 04 /undefine Two 05 D EmplyeeNbr S 10 inz('8024') /free 06 EmplyeeName = GetEmplyeeName(EmplyeeNbr) ; 07 EmplyeeNbr = *all'?' ; 08 EmplyeeName = GetEmplyeeName(EmplyeeNbr) ; 09 *inlr = *on ; 10 EmplyeeNbr = '*end' ; 11 EmplyeeName = GetEmplyeeName(EmplyeeNbr) ; |
But there are two additional line, 10 and 11, that are not present in the 7.2 version. I use these to close the file, but I will give more details when I discuss the V5R4 subprocedure.
The copybook, source member.
And now to the copy books. I have included both the 7.2 and V5R4 procedure prototypes and the returned data structure definitions in the same copy book/source member:
01 /if defined(One) 02 dcl-pr GetEmployeeName char(85) ; 03 *n char(10) options (*nopass) value ; 04 end-pr ; 05 dcl-ds EmployeeName qualified ; 06 FirstName char(15) ; 07 MiddleInitial char(1) ; 08 LastName char(25) ; 09 FullName char(43) ; 10 ReturnCode char (1) ; 11 end-ds ; 12 /endif 13 /if defined(Two) 14 D GetEmplyeeName PR 85 15 D 10 options(*nopass) value 16 D EmplyeeName DS qualified 17 D FirstName 15 18 D MidInitial 1 19 D LastName 25 20 D FullName 43 21 D RtnCde 1 22 /endif |
The fixed format definitions can be included in the 7.2 RPG code as it is possible to mix fixed and free format code. But the reverse is not possible, pre-7.1 TR7 RPG cannot contain free format definitions.
Notice the IF DEFINED compiler directives on lines 1 and 13, and the matching /ENDIF on lines 12 and 22. These match the /DEFINE compiler directive in the source for the two programs. For example in the 7.2 program the compiler will copy all of the lines in the copy book from the /IF DEFINED(ONE), line 1, to its matching /ENDIF, line 13 into the source of the program source starting at the /DEFINE ONE on line 2. The /UNDEFINE ONE ensures that if I use another copy book with the same definition name it will not be copied into the program. The same happens in the V5R4 program.
When I look at the compile listing for the 7.2 program I see:
000300 /define One 000400 /include mysrc,testrpg_c *--------------------------------------------------------------------------------------------* * RPG member name . . . . . : TESTRPG_C * * External name . . . . . . : MYLIB/MYSRC(TESTRPG_C) * * Last change . . . . . . . : 11/05/2014 05:00:00 * * Text 'description' . . . . : Copy book * *--------------------------------------------------------------------------------------------* 000100+ /if defined(One) *--------------------------------------------------------------------* * Compiler Options in Effect: * *--------------------------------------------------------------------* * Text 'description' . . . . . . . : Test * * Generation severity level . . . : 10 * * Default activation group . . . . : *NO * * Compiler options . . . . . . . . : *XREF *GEN * * *NOSECLVL *SHOWCPY * * *EXPDDS *EXT * * *NOSHOWSKP *SRCSTMT * * *NODEBUGIO *NOUNREF * * *NOEVENTF * * Optimization level . . . . . . . : *NONE * * Source listing indentation . . . : *NONE * * Type conversion options . . . . : *NONE * * Sort sequence . . . . . . . . . : *HEX * * Language identifier . . . . . . : *JOBRUN * * User profile . . . . . . . . . . : *USER * * Authority . . . . . . . . . . . : *LIBCRTAUT * * Truncate numeric . . . . . . . . : *YES * * Fix numeric . . . . . . . . . . : *NONE * * Allow null values . . . . . . . : *NO * * Storage model . . . . . . . . . : *SNGLVL * * Binding directory from Command . : *NONE * * Binding directory from Source . : TESTRPG * * Library . . . . . . . . . . . : MYLIB * * Activation group . . . . . . . . : *NEW * * Enable performance collection . : *PEP * * Profiling data . . . . . . . . . : *NOCOL * * Generate program interface . . . : *NO * *--------------------------------------------------------------------* 000200+ dcl-pr GetEmployeeName char(85) ; 000300+ *n char(10) options (*nopass) value ; 000400+ end-pr ; 000500+ 000600+ dcl-ds EmployeeName qualified ; 000700+ FirstName char(15) ; 000800+ MiddleInitial char(1) ; 000900+ LastName char(25) ; 001000+ FullName char(43) ; 001100+ ReturnCode char (1) ; 001200+ end-ds ; 001300+ /endif 001400+ 001500+ /if defined(Two) LINES EXCLUDED: 10 002600+ /endif 000500 /undefine One |
The compile listing for the V5R4 program would contain something similar.
The subprocedure.
There is a difference between the two subprocedures which is more than the difference between fixed format and free format code. IBM i 7.1 introduced the ability to place the file definition into the subprocedure. Before that the file had to be defined at the top of the source member before any of the subprocedures are defined. As the file definition is independent of the subprocedure the file is not closed when the subprocedure is finished, which is what lines 10 and 11 are for in the V5R4 program above. This is what the source for the V5R4 program looks like:
01 H nomain 02 FEMPMAST IF E K DISK 03 /define Two 04 /copy mysrc,testrpg_c 05 /undefine Two 06 P GetEmplyeeName B export 07 D GetEmplyeeName PI 85 08 D EENumber 10 options(*nopass) value /free 09 if (EENumber = '*end') ; 10 close EMPMAST ; 11 return EmplyeeName ; 12 endif ; 13 chain EENumber EMPMASTR ; 14 if not(%found) ; 15 EmplyeeName = ' ' ; 16 EmplyeeName.RtnCde = '1' ; 17 else ; 18 EmplyeeName.FirstName = FIRSTNME ; 19 EmplyeeName.MidInitial = MIDINITIAL ; 20 EmplyeeName.LastName = LASTNME ; 21 EmplyeeName.FullName = %trimr(FIRSTNME) + ' ' + 22 MIDINITIAL + ' ' + 23 LASTNME ; 24 EmplyeeName.RtnCde = ' ' ; 25 endif ; 26 return EmplyeeName ; /end-free 27 P E |
On line 1 I have defined that this procedure has no main line. It just contains a subprocedure. As I explained above the file is defined outside of the subprocedure on line 2. Line 3 – 5 copy the procedure prototype and data structure from the copy book.
The subprocedure GetEmplyeeName begins with the P-spec on line 6. The procedure interface is defined on line 7 and 8. On line 7 "PI" has to be in the "Declaration type" column of the D-spec for the compiler to recognize that this is the procedure interface. As the returned data structure is 85 character that needs to be given in the "To/length" column. As the subprocedure is only passed one parameter that is defined on line 8.
Lines 9 – 12 is used to close the file at the end of the calling program, see lines 10 and 11 in the calling program.
I think that everyone should be able to read and understand what is happening in lines 13 – 25.
The subprocedure returns the values in the data structure, EmplyeeName, to the calling program when it executes the line 26.
And the subprocedure ends at line 27 with the ending P-spec.
Personally I think the 7.2 subprocedure is easier to understand.
01 ctl-opt nomain ; 02 /define One 03 /include mysrc,testrpg_c 04 /undefine One 05 dcl-proc GETEMPLOYEENAME export ; 06 dcl-pi *n char(85) ; 07 EmplNbr char(10) options(*nopass) value ; 08 end-pi; 09 dcl-f EMPMAST keyed ; 10 dcl-ds EmployeeData likerec(EMPMASTR) ; 11 chain EmplNbr EMPMASTR EmployeeData ; 12 if not(%found) ; 13 EmployeeName = ' ' ; 14 EmployeeName.ReturnCode = '1' ; 15 else ; 16 EmployeeName.FirstName = EmployeeData.FIRSTNME ; 17 EmployeeName.MiddleInitial = EmployeeData.MIDINITIAL ; 18 EmployeeName.LastName = EmployeeData.LASTNME ; 19 EmployeeName.FullName = %trimr(EmployeeData.FIRSTNME) + ' ' + 20 EmployeeData.MIDINITIAL + ' ' + 21 EmployeeData.LASTNME ; 22 EmployeeName.ReturnCode = ' ' ; 23 endif ; 24 return EmployeeName ; 25 end-proc ; |
The only differences are that the file is defined with the procedure. As the subprocedure is not cyclical the file must be read into a data structure, EmployeeData, and all of the file fields are accessed using the qualified data structure subfields. There is no code associated with opening or closing of the file.
The procedure starts with the DCL-PROC, line 5, and ends with END-PROC, line 25. The interface is defined between the DCL-PI, line 6, and END-PI, line 8. The file is declared on line 9 and the file data structure on line 10 using the LIKEREC keyword. I think that everyone should be able to read and understand what is happening in the rest of the subprocedure, lines 11 – 24.
The gotcha.
What is the gotcha I mentioned at the top of this post? It also explains why most subprocedures are best placed in different modules rather than in the program. For example if I put the subprocedure used above into the program it would look something like this:
01 ctl-opt dftactgrp(*no) ; 02 dcl-pr GetEmployeeName char(85) ; 03 *n char(10) options (*nopass) value ; 04 end-pr ; 05 dcl-ds EmployeeName qualified ; 06 FirstName char(15) ; 07 MiddleInitial char(1) ; 08 LastName char(25) ; 09 FullName char(43) ; 10 ReturnCode char (1) ; 11 end-ds ; 12 dcl-s EmployeeNumber char(10) inz('8024') ; 13 dcl-s Flag char(1) ; 14 EmployeeName = GetEmployeeName(EmployeeNumber) ; 15 EmployeeNumber = *all'?' ; 16 EmployeeName = GetEmployeeName(EmployeeNumber) ; 17 *inlr = *on ; 18 dcl-proc GETEMPLOYEENAME export ; 19 dcl-pi *n char(85) ; 20 EmplNbr char(10) options(*nopass) value ; 21 end-pi; 22 dcl-f TESTFILE keyed ; 23 dcl-ds EmployeeData likerec(EMPMASTR) ; 24 chain EmplNbr EMPMASTR EmployeeData ; 25 if not(%found) ; 26 EmployeeName = ' ' ; 27 EmployeeName.ReturnCode = '1' ; 28 else ; 29 EmployeeName.FirstName = EmployeeData.FIRSTNME ; 30 EmployeeName.MiddleInitial = EmployeeData.MIDINITIAL ; 31 EmployeeName.LastName = EmployeeData.LASTNME ; 32 EmployeeName.FullName = %trimr(EmployeeData.FIRSTNME) + ' ' + 33 EmployeeData.MIDINITIAL + ' ' + 34 EmployeeData.LASTNME ; 35 EmployeeName.ReturnCode = ' ' ; 36 endif ; 37 Flag = '1' ; 38 return EmployeeName ; 39 end-proc ; |
I have defined a variable in the main line of the program called Flag, line 13. I find that even though it is not defined as a passed parameter when I call the subprocedure, that I can change it, line 37. The value I changed it to is retained when I return to the main line of the program.
Therefore, if you want to keep your subprocedures and main line safe from an accidental change then the subprocedures need to be separated from the main line into their own source members. This is also helpful as they become available for other programs to use allowing us to code something once and use it in many places.
It looks and is simple to do. The subprocedures can be complex and contain many lines of code, or be simple and just contain a few. They can be used to return any type of variable, including indicators, for example:
if CheckEmployeeNumber(EmployeeNumber) ; dsply 'Employee number invalid' ; endif ; |
This article was written for IBM i 7.2, and it should work with other releases too.
Thanks a lot for your article, Simon. It's really helpful to see how other programmers organize their stuff. Plus, the new "over-free" specs are very interesting to watch while being put into practice.
ReplyDeleteNevertheless, I'm afraid I didn't get the /IF DEFINED part.
"The /UNDEFINE ONE ensures that if I use another copy book with the same definition name it will not be copied into the program."
So it's not only to distinguish between the 7.2 and V5R4 code pieces, right? The practical value of that would be rather small I think. Why not one copyfile for all 7.1 sources and another for V5R4? And how many programmers have to code in different releases? Well, you obviously have another reason.
I think I understand you avoid multiple definitions of the same subprocedures in one pgm source. I know the problem and I handle it this way: each definition can be in only one copy file. With the result of an increasing number of included copy files in many of my sources.
But then, you have only one copy file in your example. So how does it work? I'm standing on the schlauch as the Germans say.
Markus, it is great to see that people are finding these posts both interesting and educational.
DeleteThe questions and observations you have made are very good.
Let’s see if I can answer your questions:
I gave the example of the /IF DEFINED and its matching /UNDEFINE as an example of its functionality. In this case with just two procedure prototypes and two data structures it would not have been a big deal to have both copied into the source code. I have copy books/source member like this that contain many (50+) procedure prototypes, each one has its own unique “defined” name. But we interface with other code created by other teams of programmers, some of these also use procedures and have their own copy books. While I can guarantee that my team’s name are unique in the copy book we maintain, I cannot stop another team from using the same name for a different subprocedure in their application.
For example: I may need to get the quantity of items held in the warehouse for a customer using a subprocedure called, CUSTOMERBALANCE. Now I need to check to see if the customer is on “credit hold” if I then include the finance team’s copy book and they have a subprocedure called CUSTOMERBALANCE that determines the balance of the customer’s account with the company. If I had not used the /UNDEFINE after using the /COPY for the first copy book I would then have all of finance’s CUSTOMERBALANCE procedure prototype, data structures and whatever else they have included copied into my source too even even if I did not use it.
I created the copy book with both the 7.2 and the V5R4 versions of the procedure prototype and the data structure to show that it was possible. You cannot put free format if you are on an IBM i that is less than 7.1 TR7. If your IBM i does have 7.1 TR7 or later then you can insert the free format into the fixed format code. Therefore, I would keep all of the prototypes and data structures, free and fixed, in the same source member.
If you have multiple IBM i servers/partitions working on different releases of operating system then I do feel for you, as you would have to make sure your code is compatible with the lowest release rather that have different versions of the same program on each server for the different releases.
As for whether you decided to put your procedure prototypes, etc., into a separate member I think that depends on the person creating the code. If you put all the procedure prototypes into one source member for an entire application it may get very large. If you put each procedure prototype into a separate member then you could get a lot of members. I am sure each of us can develop a balance between the two suits our specific working environment.
I like to prepend the name of the module name onto the sub-procedure name. Think of it as a namespace. This avoids name collisions in compiles. Imagine a sub-proc called CloseAllFiles( ) in many service programs. When the client calls CloseAllFiles( ), how would the compiler know which one?
ReplyDeleteINVEN100_CloseAllFiles( )
PURCH100_CloseAllFiles( )
INVEN100_GetAvailQty( )
IBM does the same thing.
DSPSRVPGM SRVPGM(QRNXIO) DETAIL(*PROCEXP)
The other benefit is you (and the next programmers) know exactly where the sub-proc is coming from, so it makes the code more readable too. My 2 cents. Thanks.
Chris Ringer
+1
DeleteI find the sample code confusing without indicating the source member name you save each one as.
ReplyDeleteIf the source member names are supposed to be obvious then I am not seeing it. Can you clarify?
I rarely give the source member name as they would all be the same: TEST + something.
DeleteThe way to look which bits of code go together is to look at the numbers on the left. If they continue from the last snippet of code they belong together. If the first line of the code is 01 then it is a new member.
I was a little confused at first by your statement that you could create a sub-procedure with procedure interface and it would be scoped out of the global main procedure variables...then I read your gotcha...global variables are still available to sub procedures with or without Procedure Interface... how ever sub procedures in another module would be out of that scope and within their own module scope.
ReplyDeleteYou are correct. Global variables in one module are not available to be used by procedures in another module.
Delete