I was at a presentation given by Partner/400's Susan Gartner and Jon Paris where, amongst many things, they discussed replacing subroutines in your RPG code with subprocedures.
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.
An additional benefit of the PTF that allows all free format RPG is that the /free and /end-free directives are no longer required, including within fixed format code.
ReplyDeleteI found an interesting naming converntion from somewhere that uses the # as the first character of a subprocedure. It makes it easier for fellow developers to identify subprocedure calls within your program. for example proc2 would be name #proc2. This helps distinguish subprocedure calls from external calls.
ReplyDeleteGenerally a subprocedure is obvious because it returns something and takes parameters.
DeleteFor example Hours= GetWeeklyHours(Emp_Number);
If you simply code GetweeklyHours then you aren't really using subprocedures to their maximum capability because you are using global variable for both the returned value and the incoming parameter.
Sam
I am frustrated to work with version 5.4 or 6.1! Strongly that one of my customer migrates to 7.2
ReplyDeleteWhy wouldn't you write only subprocedures? Subprocedures have so many advantages over subroutines. Local variables, the ability to pass in variables, the ability to return data, and even the ability to use those procedure names in evaluation and control struction statements.
ReplyDeleteStructure = struction
DeleteSubProcedures are NOT new shiny toys. They have been around for two decades. There is no reason to use a subroutine with the possible exception of *InzSR. It is rgeat when diagnosing a problem in real time that you can look at the call stack and see what proc you're in.
DeleteI agree with all of you. The problem that I am always facing is that, even after 20 years, many programmers still don't know how to write procedures, take the full advantage of ILE, or are able to write inline SQL.
DeleteSpeaking of SQL, many managers don't even "allow" the "new" gadgets. In my opinion it is just the fear that they don't know how it works and are not willing to learn something new.
Agree Guido, That's why I said a long time ago RPG is not dying we are killing it., Nowadays there are easier way to do many things in RPG with a few codes but some programmers are reluctant and they prefer to create 5 programs or x amount of programs for a process when it can be done in a few lines, but I think as you said is because the lack of knowledge and not willing to learn, they think for example that using Binding, Procedures, Modules, API, SQL is complicated.
DeleteA subprocedure can own a subroutine but not vice versa. Thus subprocedure wins!
DeleteAll your base are belong to subprocedure!!!
Subprocedure owns subroutine.
I always try to see the big picture....
ReplyDeleteIf there is a need to encapsulate your procedure from the rest of the program, use subprocedures, otherwise stick with subroutines. (others might have to maintain your code, but are not up-to-date)
If you think about reuseability, stick them inside a external procedure, which also works for PHP with the XML-toolkit. - write once -> use many!
Sure, it might be a new shiny toy, but what is the benefit, if created within your regular RPG, besides showing off? ( btw. I still believe in copy books where useful)
Subprocedures are not new - they became part of RPG in 1996 (V3R2). If a follow-on programmer can't maintain a program with inline sub-procedures, he has no business in the code. As for why I use inline sub-procedures, it's because local variable scope and the ability to return a value creditLimit = getCreditLimit(customer#) makes the overall program more robust and easier to maintain than a global mishmash of subroutines. So even if a sub-procedure can only be used in one program, I will still use it instead of a subroutine. On the other hand, I don't go to the effort of converting all subroutines to procedures, becuase of the analysis time needed to decipher what variables are tangled up between the subroutine and the mainline. This chore is exceptionally unpleasant if file I/O occurs in the subroutine.
DeleteHear, hear, Buck. I agree with your philosophy re: writing new routines as subprocedures, but it's probably seldom worth the effort to convert a lot of existing subroutines.
DeleteThanks for the mention, Simon. And apologies in advance if some of this is destined for a "part 2" of this piece - don't mean to steal your thunder.
ReplyDeleteimho, your subprocedure doesn't illustrate the advantages over subroutines nearly as much as if you had taken advantage of parameters and return values. To me, the natural way to call this subprocedure would be:
EmployeeName = GetEmployeeName(EMPNBR);
The value of this is that there's now no need to look at the code for GetEmployeeName to figure out what employee # we're talking about and where the Employee's name is going to end up. That's the primary reason I use subprocedures - to make my mainline code so obvious there's no need to look at the subproc logic to understand the mainline logic and data flow. That saves tons of time for me and any developers following me maintaining that code down the road.
I strive to make my subprocs work as functions like RPG's built-in functions - where any/all the data flow is seen in the call.
There certainly are some exceptions where some bit of logic needs to be separated from the mainline and the dataflow isn't important or is infeasible to show via parms - something like an initialization routine comes to mind.
My 2 cents on improving your example to make it a much bigger improvement over a subroutine.
My experience with the modern RPG is that it comes closer to the object oriented design. It is the chance of RPG-programmers to become more familiar with OO. So it will be easier to change to the Java world :-)
ReplyDeleteAnd it is much easier to read and understand the code especially when you use rdi-environment.
Personally, I think subprocedures have their place and really shouldn't be used for everything. I think a subroutine at it's biggest should be not much more than the size of your screen. It keeps things nice and tight together, easy to maintain, and easy to read. The people that make a 200 line subroutine drive me nuts when you get into nested IF's or Do's etc. So to me a subprocedure should have kind of a rule to it. Ask the question, could another program use what I'm going to write. If the answer is yes then it might be a good idea to make a subprocedure but I would code it outside the main program in another piece of source code. That's just my $0.02.
ReplyDeleteSteve - Just to play devil's advocate - you like to keep things nice and tight together. I agree. Yet with a subroutine all your data definitions are surely going to be very far away from your subroutines. If you used subprocedures the way I like to write them, all the data referenced in the subprocedure would be defined locally right there with the logic - encapsulated with the logic (to give credence to Andrea's OO comment)
DeleteThat's another reason that I would not have written the subprocedure this way. In Simon's example, the subprocedure references and even updates global data (ie, data defined and available in the main part of the program.) That violates my personal rule for writing subprocedure logic. The way I'd have written it, the subprocedure would only access and update it's own local data. Now that's nice and tight together, easy to maintain and easy to read.
I agree that our procedures should be somewhat small and that nesting conditions 6 and 7deep is not a good idea. I will say that regardless of if you think the code can be reused by other programs and should be external, procedures are head and shoulders better than using a subroutine. Consider this, you have the ability to define local variables in a procedure, you have the ability to pass parameters to a procedure and make them not changable within that procedre. Ever had a program stomp all over a field and not know where? You can get a value back from a procedure and you can use a procedure call in a n IF statement, a when, or an eval.
DeletemyWeeklySalary = retrieveWeeklyPay(employeeId);
If retrieveWeeklyPay(employeeId) < 1000;
giveRaise(employeeId);
Endif;
If isRainyDay();
takeUmbrella = *On;
Endif;
takeUmbrella = isRainyDay();
DeleteI love it, program code becomes self documenting.
Another death blow for subroutines.
Jon & I wrote an article in IBM Systems Mag a few years ago on this subject - 3 Good reasons to Stop Writing Subroutines. http://www.ibmsystemsmag.com/ibmi/developer/rpg/Three-Good-Reasons-to-Stop-Writing-Subroutines/
ReplyDeletehttp://www.ibmsystemsmag.com/ibmi/developer/rpg/Three-Good-Reasons-to-Stop-Writing-Subroutines/
DeleteI find it is kind of a progression, subroutines then subprocedures, then service program procedures, then stored procedures as the scope of the function grows.
ReplyDeleteOkay, I like subprocedure, execpt when they error.... Let me explain...
ReplyDeleteProgram blows up, and I give it a "D"=Dump to research when time allows later, as per usual... The problem is the Dump says call to Procedure MyProc Error. Where did it blow up in the procedure? It does NOT give the line in error in the procedure. Hence using good old subroutine works much better! At least you get the dump information! (BIg Grin)
Hi Simon,
ReplyDeleteIs there any specific way of defining procedure for *inzr and *pssr without altering it's significance
Regards
Sukumar g
It all depends what you are doing within them.
DeleteYou probably could not copy the contents of those subroutines into procedures and have them work the same way. But with a bit of thinking and reworking the logic of their content you probably could.
One important thing:
ReplyDeleteAdd the "actgrp (*caller)" to the options, if you like to use your RPG program as OPM program as well.