Take advantage of global, static, and non-static variable attributes within subroutines and procedures.
IBM has always been good at giving us all the tools that we need in one box and ensuring that they run on the box reliably and efficiently. Since RPG has evolved into the ILE environment, we have been given more capabilities in the realm of modular coding and encapsulation. With these new capabilities, you have to learn about what you're gaining, what you're losing, and what all the nuances are of the different coding styles. In this article, I intend to describe the scoping and attributes of variables with RPG procedures.
Procedure Encapsulation
One of the objectives of procedures is to provide the programmer with an encapsulated black box of functionality with no fears of having external resources reach beyond the protection of the procedure to cause unpredictable results. This is accomplished by making the internal variables of the procedure private to the procedure.
When the variables are made private to the procedure, they are no longer visible to code outside of the procedure. And if the same variable name is used within the procedure as a variable that is named outside of the procedure, they will each be allocated their own memory space and be independent of one another.
Procedure Encapsulation Versus Subroutine Global Variables
Encapsulation is a common goal for modern programming techniques, but it does take away some basic capabilities that you wouldn't have initially thought of before it became available. For example, suppose you want to keep track of how many times a subroutine was called. To provide a real-world example, let's say we have an account maintenance program that tracks how many accounts were updated during the duration of the execution of the program.
Using a Subroutine with Access to Global Variables
A simple program that counts the number of updates using subroutines could look like the following code:
D dataStruct DS
D counter 7S 0
D otherVar 7S 0
D displayBytes S 52A
/free
counter = *ZEROS;
//---------------------------------------------------
// User does maintenance, then updates account
exsr updateAccount;
displayBytes = 'Number of Records Updated: '
+ %trim(%editc(counter: '3'));
dsply displayBytes;
//---------------------------------------------------
// Lather, Rinse, Repeat
exsr updateAccount;
displayBytes = 'Number of Records Updated: '
+ %trim(%editc(counter: '3'));
dsply displayBytes;
//---------------------------------------------------
// Improper reuse of global variable
// Because available, another programmer has
// !!! Inadvertently reset the counter !!!
clear dataStruct;
dsply 'Unexpected Counter Reset.';
//---------------------------------------------------
// Lather, Rinse, Repeat
exsr updateAccount;
displayBytes = 'Number of Records Updated: '
+ %trim(%editc(counter: '3'));
dsply displayBytes;
*inlr = *ON;
//---------------------------------------------------------------
// updateAccount: Updates Physical File Record(s) for Account
// count: Counts the number of times executed
//----------------------------------------------------------------
begsr updateAccount;
counter = counter + 1;
//----------------------------------------------------------
// ... code ... code ... code ... code ....
// .........
// Do Lots of Time Consuming Logic to Update Account Here...
// .........
// ... code ... code ... code ... code ....
//----------------------------------------------------------
endsr;
/end-free
When we run the program, we will get the following results. Take note that, because the subroutine is using global variables, the counter was inadvertently reset by code outside of the subroutine.
> CALL PGM(MCP042RPGA)
DSPLY Number of Records Updated: 1
DSPLY Number of Records Updated: 2
DSPLY Unexpected Counter Reset.
DSPLY Number of Records Updated: 1
In this code, you can see that the counter variable is defined globally and is being used only within the subroutine. Because the counter variable is available globally, when another programmer comes into the program and clears the dataStruct data structure, the counter variable is reset to zero within the code, creating undesired results on the value of the counter accumulator.
Using a Procedure with Access to Global Variables
Next, I will convert the subroutine into a procedure. You can see that I am still accessing the global counter variable.
D dataStruct DS
D counter 7S 0
D otherVar 7S 0
D displayBytes S 52A
D* Prototype for updateAccount procedure
D updateAccount...
D PR 7S 0
/free
counter = *ZEROS;
//---------------------------------------------------
// User does maintenance, then updates account
counter = updateAccount();
displayBytes = 'Number of Records Updated: '
+ %trim(%editc(counter: '3'));
dsply displayBytes;
//---------------------------------------------------
// Lather, Rinse, Repeat
counter = updateAccount();
displayBytes = 'Number of Records Updated: '
+ %trim(%editc(counter: '3'));
dsply displayBytes;
//---------------------------------------------------
// Improper reuse of global variable
// Because available, another programmer has
// !!! Inadvertently reset the counter !!!
clear dataStruct;
dsply 'Unexpected Counter Reset.';
//---------------------------------------------------
// Lather, Rinse, Repeat
counter = updateAccount();
displayBytes = 'Number of Records Updated: '
+ %trim(%editc(counter: '3'));
dsply displayBytes;
*inlr = *ON;
/end-free
*-----------------------------------------------------------------
* updateAccount: Updates Physical File Record(s) for Account
* count: Counts the number of times executed
*-----------------------------------------------------------------
P updateAccount...
P B EXPORT
D updateAccount...
D PI 7S 0
/free
counter = counter + 1;
//----------------------------------------------------------
// ... code ... code ... code ... code ....
// .........
// Do Lots of Time Consuming Logic to Update Account Here...
// .........
// ... code ... code ... code ... code ....
//----------------------------------------------------------
return counter;
/end-free
P E
Even though we've created a procedure, our data is still not encapsulated because the procedure is dependent upon variables that are globally accessible throughout the program. So, when the counter variable is reset in the main program, it causes the expected results of the procedure to become broken.
Using a Procedure with a Protected Variable Inside the Procedure
In our next revision, I will define a local variable within the procedure.
*-----------------------------------------------------------------
* updateAccount: Updates Physical File Record(s) for Account
* count: Counts the number of times executed
*-----------------------------------------------------------------
P updateAccount...
P B EXPORT
D updateAccount...
D PI 7S 0
D* Procedure Variables
D counter S 7S 0
/free
counter = counter + 1;
//----------------------------------------------------------
// ... code ... code ... code ... code ....
// .........
// Do Lots of Time Consuming Logic to Update Account Here...
// .........
// ... code ... code ... code ... code ....
//----------------------------------------------------------
return counter;
/end-free
P E
When the counter variable is defined locally in the procedure, it will be reinitialized every time the procedure is called. Here are the results when this version of the program is run:
> CALL PGM(MCP042RPGC)
DSPLY Number of Records Updated: 1
DSPLY Number of Records Updated: 1
DSPLY Unexpected Counter Reset.
DSPLY Number of Records Updated: 1
The variable is now encapsulated, but it still does not give us our expected results. This is because the variables defined within the procedure are created on every call to the procedure and destroyed when it is complete. So the value of the counter accumulator never goes beyond one.
Using a Procedure with a Static Variable
Here is where the static variable becomes the solution to our problem. When we define a static variable, it is only initialized once and it is only available within the procedure.
D dataStruct DS
D counter 7S 0 inz(0)
D counterG 7S 0 inz(0)
D otherVar 7S 0
D displayBytes S 52A
D* Prototype for updateAccount procedure
D updateAccount...
D PR 7S 0
/free
//---------------------------------------------------
// User does maintenance, then updates account
counter = updateAccount();
counterG = counterG + 1;
displayBytes = 'Global: '
+ %trim(%editc(counterG: '3'))
+ ', Procedure Static: '
+ %trim(%editc(counter: '3'));
dsply displayBytes;
//---------------------------------------------------
// Lather, Rinse, Repeat
counter = updateAccount();
counterG = counterG + 1;
displayBytes = 'Global: '
+ %trim(%editc(counterG: '3'))
+ ', Procedure Static: '
+ %trim(%editc(counter: '3'));
dsply displayBytes;
//---------------------------------------------------
// Improper reuse of global variable
// Because available, another programmer has
// !!! Inadvertently reset the counter !!!
clear dataStruct;
dsply 'Unexpected Counter Reset.';
//---------------------------------------------------
// Lather, Rinse, Repeat
counter = updateAccount();
counterG = counterG + 1;
displayBytes = 'Global: '
+ %trim(%editc(counterG: '3'))
+ ', Procedure Static: '
+ %trim(%editc(counter: '3'));
dsply displayBytes;
*inlr = *ON;
/end-free
*-----------------------------------------------------------------
* updateAccount: Updates Physical File Record(s) for Account
* count: Counts the number of times executed
*-----------------------------------------------------------------
P updateAccount...
P B EXPORT
D updateAccount...
D PI 7S 0
D* Procedure Variables
D counter S 7S 0 static inz(0)
/free
counter = counter + 1;
//----------------------------------------------------------
// ... code ... code ... code ... code ....
// .........
// Do Lots of Time Consuming Logic to Update Account Here...
// .........
// ... code ... code ... code ... code ....
//----------------------------------------------------------
return counter;
/end-free
P E
In this program, a new variable called counterG has been added to show how the global variable is incremented versus the procedure static variable. Now when we run the program, we get the desired results on the procedure static variable.
> CALL PGM(MCP042RPGD)
DSPLY Global: 1, Procedure Static: 1
DSPLY Global: 2, Procedure Static: 2
DSPLY Unexpected Counter Reset.
DSPLY Global: 1, Procedure Static: 3
We have a protected variable within the procedure that has the same name as a global variable. When the global variable or the static procedure variable is changed, it has no impact on the other. So we've achieved encapsulation and we've also supported our accumulator functionality.
The Difference Between Global Variables and Static Procedure Variables
Global variables are considered to be static variables within the program because they are only initialized once, when the program starts. The difference is that the static variables will not be reset until the activation group has been reclaimed.
If we run the program a second time, we can see that the global counters have the exact same values as expected, but we can also see that the procedure static variables will pick up where they left off from the last time the program was called even though the *INLR indicator was set on at the end of the first call. So, instead of the static procedure variable restarting at 1, it continues where the previous call left off at 3 to begin at 4.
> CALL PGM(MCP042RPGD)
DSPLY Global: 1, Procedure Static: 4
DSPLY Global: 2, Procedure Static: 5
DSPLY Unexpected Counter Reset.
DSPLY Global: 1, Procedure Static: 6
If we reclaim the activation group, we can now see that the procedure static variables have been reset.
> RCLACTGRP ACTGRP(*ELIGIBLE)
Activation group QILE deleted.
> CALL PGM(MCP042RPGD)
DSPLY Global: 1, Procedure Static: 1
DSPLY Global: 2, Procedure Static: 2
DSPLY Unexpected Counter Reset.
DSPLY Global: 1, Procedure Static: 3
Other Potential Uses for Static Variables
Besides using static variables as simple accumulators as illustrated above, you could also use static variables to retain the last used value to return cached results. The sample code will be modified to store the last used account number to determine if the CPU-intensive logic needs to be executed. If the code could use data that is not required to be real-time data between calls, then you could just reuse the results from the last call.
…
/free
//---------------------------------------------------
// User does maintenance, then updates account
counter = updateAccount(400);
…
//---------------------------------------------------
// Lather, Rinse, Repeat
counter = updateAccount(400);
…
//---------------------------------------------------
// Lather, Rinse, Repeat
counter = updateAccount(400);
…
*inlr = *ON;
/end-free
*-----------------------------------------------------------------
* updateAccount: Updates Physical File Record(s) for Account
* count: Counts the number of times executed
*-----------------------------------------------------------------
P updateAccount...
P B EXPORT
D updateAccount...
D PI 7S 0
D argAccount 6S 0 const
D* Procedure Variables
D counter S 7S 0 static inz(0)
D svAccount S 6S 0 static inz(0)
/free
counter = counter + 1;
if (argAccount = svAccount);
dsply 'Saving Time Using Cache...';
else;
dsply 'Executing CPU Intensive Logic...';
//----------------------------------------------------------
// ... code ... code ... code ... code ....
// .........
// Do Lots of Time Consuming Logic to Update Account Here...
// .........
// ... code ... code ... code ... code ....
//----------------------------------------------------------
endif;
svAccount = argAccount;
return counter;
/end-free
P E
Now if you were to run the program to emulate a series of repeated calls to the same account, which could happen when you're running a report, then you could see that the CPU-intensive logic is only executed the first time; for all subsequent calls, it will reuse the values computed from the previous call. And it's all encapsulated within the subprocedure.
> CALL PGM(MCP042RPGE)
DSPLY Executing CPU Intensive Logic...
DSPLY Global: 1, Procedure Static: 1
DSPLY Saving Time Using Cache...
DSPLY Global: 2, Procedure Static: 2
DSPLY Unexpected Counter Reset.
DSPLY Saving Time Using Cache...
DSPLY Global: 1, Procedure Static: 3
Another possible use could be when you're using a buffer that is being used for a file read that is returning portions of the read file and advancing through the data with each call to the procedure.
Or another possible option that I have used static variables for would be to identify whether a multiple-member file was already open or not and then decide whether the file needs to be overridden to another member or can use the already-open file member from the previous read.
Download the Code
You can download the code used in this article by clicking here.
as/400, os/400, iseries, system i, i5/os, ibm i, power systems, 6.1, 7.1, V7, V6R1
LATEST COMMENTS
MC Press Online