Having spent the last few weeks using subprocedures in place of subroutines in any new programs I have written, I have been won over to the subprocedure side of the argument.
Let me clarify this post is not about taking code from an existing program and placing it in an external procedure. It is about the differences I found using in-line subprocedures when compared to in-line subroutines. So what were the major differences I found?
Before I start listing my findings let me explain how to code an in-line subprocedure. All of the subprocedures need to be coded at the end of your source member, if your code includes Output specifications (O-specs) the subprocedures need to be placed after them.
In all free RPGLE it is very easy, all you have to do is enter DCL-PROC with the subprocedure name at the start and the END-PROC at the end.
dcl-proc GetEmployeeName ; end-proc ;
In pre-all free RPGLE it is a bit more awkward, as you have to switch in and out of /free.
/end-free PGetEmployeeName B /free /end-free P E
Now we have the start and the end of the subprocedure not we can start with the cool stuff!
With subroutines all of the variables are defined near the top of the code either in the Definition specification (D-spec) or using DCL-S, DCL-C, and DCL-DS. They can be considered as "global" variables as they can be used in the main line section of the code and in all of subroutines. With subprocedures we can code variables in them, these variable become "local" and are only available to be used within that subprocedure. It is even possible to use the same name for a variable in several different subprocedures with different attributes, I will show an example later. Below are examples of how to code variables in subprocedures.
dcl-proc GetEmployeeName ; dcl-s WeeklyPay packed(7:3) ; end-proc ;
P GetEmployeeNme B D WeeklyPay S 7 3 /free /end-free P E
Not only can we have local variables, since IBM i 6.1 it is possible to define "local" files in subprocedures. I can have one subprocedure that uses a file for input, and another subprocedure that uses the same file for output. When you do use files in subprocedures and you read, write, chain, or update you must do so using a data structure for the file's fields, see the example below.
dcl-proc GetEmployeeName ; dcl-f EMPMAST keyed ; dcl-ds EmployeeData likerec(EMPMASTR) ; chain EmployeeData.EMPNBR EMPMASTR EmployeeData ; end-proc ; dcl-proc AddNewEmployee ; dcl-f EMPMAST usage(*output) ; dcl-ds EmployeeData likerec(EMPMASTR:*output) ; write EMPMASTR EmployeeData ; end-proc ;
PGetEmployeeName B FEMPMAST IF E K DISK D EmployeeData DS likerec(EMPMASTR) /free chain EmployeeData.EMPNBR EMPMASTR EmployeeData ; /end-free P E PAddNewEmployee B FEMPMAST O E DISK D EmployeeData DS likerec(EMPMASTR:*output) /free write EMPMASTR EmployeeData ; /end-free P E
In the first subprocedure of the examples the data structure EmployeeData uses the LIKEREC to define the same fields as there are in EMPMAST's record format EMPMASTR. In the second subprocedure, AddNewEmployee, the record format name has to be followed by *OUTPUT to denote that the data structure will be used for output during the WRITE operation. The LIKEREC also qualifies the data structure subfields, so all subfields must be given with the data structure name, as you would do if you had used QUALIFIED for the data structure.
Let me put all of this together in a bit more meaningful example (a pre-all free version of this code is shown at the bottom of this post):
01 ctl-opt dftactgrp(*no) ; 02 dcl-s EmployeeName char(70) ; 03 dcl-s WeeklyPay packed(6:2) ; 04 GetEmployeeName() ; 05 WeeklyPay = WeeklyPay ; 06 Proc2() ; 07 *inlr = *on ; 08 dcl-proc GetEmployeeName ; 09 dcl-f EMPMAST keyed ; 10 dcl-ds EmployeeData likerec(EMPMASTR) ; 11 dcl-s WeeklyPay packed(7:3) ; 12 EmployeeData.EMPNBR = 8024 ; 13 chain EmployeeData.EMPNBR EMPMASTR EmployeeData ; 14 if not(%found) ; 15 EmployeeName = ' ' ; 16 return ; 17 endif ; 18 EmployeeName = %trimr(EmployeeData.EMPFIRST) + ' ' + %trimr(EmployeeData.EMPMIDDLE) + ' ' + EmployeeData.EMPLAST ; 19 eval(h) WeeklyPay = EmployeeData.EMPRATE * 40 ; 20 end-proc ; 21 dcl-proc Proc2 ; 22 dcl-s WeeklyPay packed(7) ; 23 WeeklyPay = 1 ; 24 end-proc ;
Any program that uses subprocedures, whether they are external or in-line, will not compile if they are to run in the default activation group. You can either change the default activation group (DFTACTGRP) in the compile command, or you can included it in the control options (header specification), see line 1.
Lines 2 and 3 define the "global" variables. The "global" WeeklyPay if defined at a 6 packed numeric field with 2 decimal places.
Line 4 shows how simple it is to call the in-line subprocedure GetEmployeeName.
Line 5 only exists so I can use debug to view the value of the variable WeeklyPay.
On line 6 I call another subprocedure. I expect no points for originality for its name, Proc2.
I set on *INLR on line 7.
And now to dive into the first subprocedure, GetEmployeeName. Line 8 marks its start and line 20 its end.
I have defined a "local" file that I will only use in this procedure, on line 9. It is keyed and as I have not give the usage the default, input is assumed.
As I have to read the file into a data structure I have defined the data structure on line 10. As I mentioned above, the LIKEREC means that the data structure will contain all of the subfields that are the same as the fields in the file's member EMPMASTR.
On line 11 I define a "local" version of the WeeklyPay variable, this one is a 7 packed numeric field with 3 decimal places, which is different from the "global" version of this variable.
On line 12 I move an employee number to a field in preparation to performing a CHAIN operation. The CHAIN is performed on line 13. Notice how the data structure name is given after the file format name. If the chain fails, line 14, then I move blank to the EmployeeName variable, line 15, and exit the subprocedure by using the RETURN operation code.
As the CHAIN operation moved the data from the employee record to the EmployeeData data structure I make the full employee name from the individual name variable, EmployeeName, from the data structure's subfields.
I calculate the employees weekly rate of pay into the "local" WeeklyPay on line 19. In this example will be say that this employee earns $10.000, then WeeklyPay will contain $400.000.
At the end of the subprocedure I return to the main line, line 4. If I check the value of the variable WeeklyPay, line 5, it contains zero. The value of GetEmployeeName's WeeklyPay is not returned to the main line as it is "local" to that subprocedure.
The subprocedure Proc2, starts on line 21 and ends line 24, just reinforces the point of "local" variables. Its WeeklyPay is defined as 7 long packed numeric with no decimals. Before line 23 is executed the "local" WeeklyPay is zero, and 1 after line 23 is executed.
This example does illustrate something you need to keep in mind. If you define a file or variable as "local" in one subprocedure it cannot be used in another, even if the second subprocedure is called from the first.
I am sure that those of you who already use subprocedures have noticed that I have not coded any Procedure Interface specifications. For the examples I have shown above you do not need them.
This is the pre-all free RPG version of the same program.
01 H dftactgrp(*no) 02 D EmployeeName S 70 03 D WeeklyPay S 6 2 /free 04 GetEmployeeNme() ; 05 WeeklyPay = WeeklyPay ; 07 Proc2() ; 08 *inlr = *on ; /end-free 09 P GetEmployeeNme B 10 FEMPMAST IF E K DISK 11 D EmployeeData DS likerec(EMPMASTR) 12 D WeeklyPay S 7 3 /free 13 EmployeeData.EMPNBR = 8034 ; 14 chain EmployeeData.EMPNBR EMPMASTR EmployeeData ; 15 if not(%found) ; 16 EmployeeName = ' ' ; 17 return ; 18 endif ; 19 EmployeeName = %trimr(EmployeeData.EMPFIRST) + ' ' + %trimr(EmployeeData.EMPMIDDLE) + ' ' + EmployeeData.EMPLAST ; 20 eval(h) WeeklyPay = EmployeeData.EMPRATE * 40 ; /end-free 21 P E 22 P Proc2 B 23 D WeeklyPay S 7 0 /free 24 WeeklyPay = 1 ; /end-free 25 P E
You can read more about the differences between subroutines and subprocedures here.
This article was written for IBM i 7.2, and it should work with earlier releases too.