Functions Can Modify Parameters, Too
January 14, 2004 Hey, Ted
Steven Gray suggested that it’s a good idea for an RPG function subroutine to return a data structure. I think he’s on the right trail but barking up the wrong tree. Barbara Morris pointed out that it’s more efficient to modify a data structure as a parameter of a subprocedure. I suggest that the best approach is to combine the strengths of the two methods.
I understand and fully appreciate the enthusiasm many programmers feel for functions. Compared with traditional RPG coding methods, nesting an %INT function within a %SUBST function within a %CHAR function, for example, eliminates intermediate work fields and reduces lines of code.
What programmers seem to forget is that a function may, and sometimes should, modify its parameters. C’s scanf( ) function is a good example. Scanf( ) reads data from the standard input stream and assigns the data to one or more variables, which are passed by the caller through pointer arguments. At the same time, scanf( ) returns an integer value indicating the number of values that were successfully read and assigned to variables.
To return to Steven Gray’s example, I propose that the GetCustDS function should return a value, as Steven Gray suggests, but I think the return value should be an indicator value to indicate success or failure. I agree with Barbara Morris that the customer data structure should be passed to the caller through a modified parameter.
The following copybook source member contains the CustDS data structure from Steven Gray’s original article and procedure prototypes to three function subprocedures. Notice that each function returns an indicator value:
D CustDS DS qualified D CustNum 5P 0 D Name 40 D Addr 40 D CityStZip 40 D CurBal 9S 2 D CredBal 9S 2 D D OpenCust PR N D D CloseCust PR N D D GetCustDS PR N D zCustNum 5P 0 Value D zDS LikeDS(CustDS)
As Steven Gray recommends, I have created a module from which a service program could be built in order to contain the implementation of the functions:
H option(*srcstmt: *nodebugio) H nomain FCustomer IF E K Disk UsrOpn /Copy Prototypes,CustRtns P OpenCust B Export D OpenCust PI N D C Open(E) Customer C Return Not %Error P E P CloseCust B Export D CloseCust PI N D C Close(E) Customer C Return Not %Error P E P GetCustDS B Export D GetCustDS PI N D zCustNum 5P 0 Value D zDS LikeDS(CustDS) D C zCustNum Chain cFmt C If %Found C Eval zDS.CustNum = CustomerNo C Eval zDS.Name = %trim(FName) + ' ' + C %trim(MidInit) + ' ' + C %trim(LName) + ' ' C Eval zDs.Addr = address1 C Eval zDs.CityStZip = %trim(city) + ', ' + C %trim(state) + ' ' + C %trim(zipcode) C Eval zDs.CurBal = Balance C Eval zDs.CredBal = Credit - Balance C Endif C Return %Found C P GetCustDS E
I have modified the GetCustDS function to return an indicator value, which lets the caller know whether the requested customer was found in the database. I have created two other functions to open and close the customer file. These return indicator variables advise the caller of the success or failure of their respective operations.
The following example shows how a caller would access these routines:
H Option(*SrcStmt: *NoDebugIO) D* *ENTRY PLIST D CustTest PR ExtPgm('CUSTTEST1') D parmCustNbr 5S 0 D CustTest PI D parmCustNbr 5S 0 D zCustNum S 5P 0 D IsOpen S N D CustomerExists S N /Copy Prototypes,CustRtns C Eval IsOpen = OpenCust C If Not IsOpen C* file did not open -- do something C EndIf C C Eval zCustNum = parmCustNbr C Eval CustomerExists = GetCustDS (zCustNum: CustDS) C If Not CustomerExists C the customer is not on file -- do something C EndIf C C CallP CloseCust C Eval *INLR = *On C Return
Notice that the OpenCust and GetCustDS functions are called with the EVAL operation. The return values are assigned to indicator variables IsOpen and CustomerExists. The programmer can use these values to proceed properly. Steven Gray’s original code indicated a not-found condition by setting the customer number to zero, a technique I used until I was faced with a situation in which all possible key values were valid.
The CloseCust function also returns a value, but since I don’t care whether it succeeds or fails, I’ve called it with CALLP instead of EVAL. In other instances, there may be reasons why I should care about the status of the close operation. In such cases, I would call CloseCust with EVAL.
One can eliminate the indicator variables by using the functions with other operations, such as DOW, DOU, and IF, as the following example shows:
C If Not OpenCust C* file did not open -- do something C Eval *INLR = *On C Return C EndIf C C If GetCustDS (zCustNum: CustDS) C* the customer is on file -- do something C Else C* the customer is not on file -- do something else C EndIf
It is unlikely, but possible, that the GetCustDS function could fail. It would not be inappropriate to say that the GetCustDS function needs to return two values–one to indicate whether the customer was found in the database and another to indicate whether the operation succeeded or failed. Yet a function can only return one value. That problem is easily resolved by letting the caller handle the error. In the following example, RPG’s MONITOR operation traps an error:
C Monitor C If GetCustDS (zCustNum: CustDS) C* the customer is on file -- do something C Else C* the customer is not on file -- do something else C EndIf C On-Error C Eval ErrorRaised = *On C EndMon
I like the idea of hiding I/O routines in subprocedures. Calling one well-designed routine from a lot of places sure beats coding the same logic in a lot of places. Thanks for the discussion.
–Cletus the Codeslinger