Guru: Global Variables in Modules
December 13, 2021 Ted Holt
When I first learned to program computers (RPG II, COBOL 74), the only kind of variables I knew of were global variables. Any statement within a program was able to use any variable. It was not until I started my computer science degree that I found out about local variables, which are known to only part of a program. Since that time, it has been my practice to use local variables as much as possible and global variables only when necessary.
Ideally an RPG program, service program, module, or subprocedure would have no global variables at all, but I don’t live in an ideal world. Today I want to write about an appropriate use of global variables in a module. When you consider that the typical RPG program or service program in most shops is built from only one module, what I have to say applies to RPG programming in general, not just *MODULE objects.
For an illustration, I’ll use the topic of inventory. Let’s say that we work for an organization that stores items in warehouses. Each item has attributes, of course, among them weight and dimensions (length, width, height). Here’s a simplified item table with the columns we need for this illustration.
create table items ( ID char ( 6) primary key, Description for Descr varchar (25), Weight dec ( 9, 3), Height dec ( 7, 3), Width dec ( 7, 3), Length dec ( 7, 3)); insert into items values ('AB-101', '#10 Widget', 2.1, 7, 6, 10.2);
Since I am American, I’ll say that we store weights and measures in U.S. customary units, or as I usually call them, English units. However, just because the data is stored in English units does not mean that the user should have to deal with English measurements. One of the best features of a database management system (as opposed to data files, like the System/36 files we had to use in antiquity) is that we can perceive the data in ways that differ from how the data is stored. (This is the concept behind views.) Therefore, even though the weight and dimensions of an item are stored in English measurements, the user should be able to view them in metric units instead.
Let’s use the same concept in our programming. Suppose we have a service program of subprocedures that retrieve and manipulate item data, similar to the inventory service program I described recently. Among the item-related subprocedures are some that return the weight and measurements of an item. For example, there’s a WeightOf subprocedure that operates like a built-in function. Give it an item number and it returns the weight of the item. There are also LengthOf, WidthOf, and HeightOf subprocedures that return the various dimensions of the item. There is a subprocedure that returns all three dimensions in parameters.
Since I want these subprocedures to be able to return the data in either English or metric units, I would need to pass a parameter to each subprocedure to specify which system of measurements to use. There would be nothing wrong with that approach.
ItemWeight = WeightOf (SomeItem: ‘KG’);
However, an alternative is to tell the service program once which system to use and be done with it. For that, we can define a global variable in the module. All the weight-and-measure-related subprocedures can check the value of the global variable and behave accordingly.
Here’s part of copybook INVITEMS.
**free dcl-s MeasurementSystem_t char(1) template; dcl-c AmericanSystem const('0'); dcl-c MetricSystem const('1'); dcl-s Weight_t packed(9:5) template; dcl-s Dimension_t packed(7:3) template; dcl-s ItemNumber_t char(6) template; dcl-pr WeightOf like(Weight_t); inItemNumber like(ItemNumber_t) const; end-pr; dcl-pr HeightOf like(Dimension_t); inItemNumber like(ItemNumber_t) const; end-pr; dcl-pr SetMeasure; inMeasurementSystem like(MeasurementSystem_t) const; end-pr; dcl-pr GetMeasure like(MeasurementSystem_t); end-pr;
First is an enumerated data type that lists the supported measurement systems. It’s in this copybook because the callers need to reference the constants, and may need to reference the template.
Next are some templates for common data. As I wrote recently, these definitions are how the caller perceives the data, which is not necessarily how the data are stored in the database. In this case, the data definitions match the definitions in the database.
Last are enough procedure prototypes to illustrate the concepts. Notice WeightOf and HeightOf. They return data, but there is no parameter to specify which system of weights and measures to use. Instead, the caller calls the SetMeasure subprocedure to let the subprocedures know which system to use.
Now that we understand the interfaces, let’s see the implementation. This is module INVITEMS.
**free ctl-opt nomain option(*srcstmt: *nodebugio); /include prototypes,invitems /include prototypes,assert dcl-s MeasurementSystem like(MeasurementSystem_t) inz(AmericanSystem); dcl-c LBS_TO_KG_FACTOR const(0.4535924); dcl-c INCHES_TO_CM_FACTOR const(2.54); dcl-c C_SQLEOF const('02000'); dcl-proc WeightOf export; dcl-pi *n like(Weight_t); inItemNumber like(ItemNumber_t) const; end-pi; dcl-s ItemWeight like(Weight_t); exec sql select it.Weight into :ItemWeight from items as it where it.ID = :inItemNumber; select; when SqlState = C_SQLEOF; clear ItemWeight; when SqlState > C_SQLEOF; assert (*off: 'Error in Weight function.'); endsl; if MeasurementSystem = MetricSystem; eval(h) ItemWeight *= LBS_TO_KG_FACTOR; endif; return ItemWeight; end-proc WeightOf; dcl-proc HeightOf export; dcl-pi *n like(Dimension_t); inItemNumber like(ItemNumber_t) const; end-pi; dcl-s ItemHeight like(Dimension_t); exec sql select it.Height into :ItemHeight from items as it where it.ID = :inItemNumber; select; when SqlState = C_SQLEOF; clear ItemHeight; when SqlState > C_SQLEOF; assert (*off: 'Error in Height function.'); endsl; if MeasurementSystem = MetricSystem; eval(h) ItemHeight *= INCHES_TO_CM_FACTOR; endif; return ItemHeight; end-proc HeightOf; dcl-proc SetMeasure export; dcl-pi *n; inMeasurementSystem like(MeasurementSystem_t) const; end-pi; MeasurementSystem = inMeasurementSystem; end-proc SetMeasure; dcl-proc GetMeasure export; dcl-pi *n like(MeasurementSystem_t); end-pi; return MeasurementSystem; end-proc GetMeasure;
The variable MeasurementSystem is declared before (i.e., outside of) the subprocedures. This means that the subprocedures can reference it.
There are two ways for a caller to change the value of the MeasurementSystem global variable.
The first way, which I don’t like, is to export the variable in the module and import it in the caller.
In the module:
dcl-s MeasurementSystem like(MeasurementSystem_t) export;
In the callers:
dcl-s MeasurementSystem like(MeasurementSystem_t) import;
With this method, the caller changes the MeasurementSystem variable as it would any other variable.
MeasurementSystem = MetricSystem;
Now that you’ve seen it, I recommend you forget it.
The second way, which I do like, is to use a “setter” routine. In this module, the setter is subprocedure SetMeasure. A caller passes a parameter to SetMeasure, which changes the value of the global variable. If a caller needs to know the value of a global variable, it uses a “getter”. I didn’t make this up. Getters and setters are common in object-oriented languages like Java and C++.
The assert subprocedure came from here, which is where I normally get it when I need to install it on another system.
Here’s a short calling program that uses these routines.
**free ctl-opt actgrp(*new) option(*srcstmt: *nodebugio) bnddir('SYSTEM'); dcl-f qsysprt printer(132); /include prototypes,InvItems dcl-s UOM like(MeasurementSystem_t); dcl-s ItemWeight like(Weight_t); dcl-s ItemHeight like(Dimension_t); *inlr = *on; UOM = GetMeasure (); writeln ('1. UOM=/' + UOM + '/'); SetMeasure (AmericanSystem); ItemWeight = WeightOf ('AB-101'); writeln ('2. Weight=/' + %char(ItemWeight) + '/'); ItemHeight = HeightOf ('AB-101'); writeln ('3. Height=/' + %char(ItemHeight) + '/'); SetMeasure (MetricSystem); ItemWeight = WeightOf ('AB-101'); writeln ('4. Weight=/' + %char(ItemWeight) + '/'); ItemHeight = HeightOf ('AB-101'); writeln ('5. Height=/' + %char(ItemHeight) + '/'); UOM = GetMeasure (); writeln ('6. UOM=/' + UOM + '/'); return; dcl-proc writeln; dcl-pi *n; inString varchar(132) const; end-pi; dcl-ds ReportLine len(132) end-ds; ReportLine = inString; write qsysprt ReportLine; end-proc writeln;
And here’s the output.
1. UOM=/0/ 2. Weight=/2.10000/ 3. Height=/7.000/ 4. Weight=/.95254/ 5. Height=/17.780/ 6. UOM=/1/
I’ll leave it to you to validate that the code worked correctly.
I can’t say enough bad about global variables. They have been the source of innumerable bugs and wasted so much of my time. To say that I hate them is to put it mildly. But I freely admit that they have their uses.
Editor’s Note: Don’t miss Ted’s special note in this issue of The Four Hundred.
RELATED STORIES
Guru: Enumerated Data Types In RPG
Guru: Abstract Data Types and RPG
The RUN Utility: Call a Program with Correctly Formatted Parameters
Are module-global variables safe to use with a named activation group?
Yes. Activation group is irrelevant.