Guru: Qualified Files – Underused and Unappreciated
September 14, 2020 Ted Holt
When IBM adds a new feature to the RPG compiler, they do so for a reason. That’s why I try to learn new techniques. I hope they’ll improve the quality of the source code I write. One relatively new feature that I do not see widely used is the qualified file. In the following paragraphs, I’d like to tell you why I like qualified files and how to use them.
To understand the need for qualified files, it may be good to begin with a brief lesson on the history of the RPG language. When RPG was originally developed, the designers decided that memory would be allocated by variable name. If two files have a field named STATUS, and you read both files, the value of STATUS in memory will be the value from the latest read. If you need both values, you must save the value of STATUS from the first read into another variable before you execute the second read. This is fine, provided that you are aware that STATUS is altered by the second read. I confess that at times I have not been aware of such a situation, and only became aware through a lengthy debugging session. For good reason debuggers allow us to watch the value of a variable.
Why Qualify Files?
When you do not qualify a file, you’re telling the compiler to use this historic behavior. That’s fine, if that’s what you want to do. I have noticed, however, that that is often NOT what programmers want to do, as it is very common for RPG programmers to circumvent this behavior. A technique I have seen for many years is to add a prefix to each variable of a record format. I’ve seen many display files with prefixes like S1, S2, S3, etc. on the field names within the different formats of a display file. There’s nothing wrong with this technique. I’ve used it myself. Later IBM added the PREFIX keyword to do this work for us. PREFIX was a great improvement, but now we have an even greater improvement, namely the qualified file.
This story contains code, which you can download here.
When you add the QUALIFIED keyword to a file definition, RPG no longer copies data between the record format and memory by field name, because the compiler no longer generates input and output specifications. Instead, all I/O operations interact with data structures. One read will not overlay the data of a previous read unless both use the same data structure.
That’s reason number one to qualify files. But wait, there’s more! All fields of an unqualified file are global in scope. That is, they can be used anywhere in the program. The data structures through which qualified files pass data may be global as well, but they may also be local to subprocedures. Reducing global data improves program reliability.
There you have two good reasons to qualify files. So much for the why. Let’s talk about the how.
The Example
To illustrate the differences between unqualified and qualified files, I wrote a little green-screen program (WHS0020R) that uses a display file and a database file, then converted it to use qualified files (WHS0010R). This is by no means industrial-strength programming, but I wanted to keep the source code as small as possible.
The display file has four record formats — a subfile, a subfile control record, a command key legend and a window for data entry. Here’s the DDS for the unqualified display file, WHS0020D:
A DSPSIZ(24 80 *DS3) A REF(WAREHOUSE1) A R S1 SFL A OPTION 1A B 6 3 A ID R O 6 8 A NAME R O 6 12 A MFGWHS R O 6 38 A STATUS R O 6 43 A R C1 SFLCTL(S1) A SFLSIZ(0013) A SFLPAG(0012) A CA03(03 'Exit') A CA05(05 'Refresh') A OVERLAY A 62 SFLDSP A 61 SFLDSPCTL A 63 SFLCLR A 61 SFLEND(*MORE) A SCREEN 14A O 1 2 A 1 27'Warehouse Master Maintenance' A COLOR(WHT) A 1 72DATE EDTCDE(Y) A OPTIONS 78A O 3 2 A 5 2'Opt' DSPATR(UL) A 5 8'ID' DSPATR(UL) A 5 12'Name ' A DSPATR(UL) A 5 38'Mfg' DSPATR(UL) A 5 43'Status' DSPATR(UL) A R S2 A 23 5'F3=Exit F5=Refresh' A R W1 A CA03(03 'Exit') A CA12(12 'Cancel') A WINDOW(8 21 9 50) A 2 13'ID:' A ID R B 2 17 A 3 11'Name:' A NAME R B 3 17 A 4 3'Manufactures:' A MFGWHS R B 4 17 A 5 9'Status:' A STATUS R B 5 17 A 8 3'F12=Cancel'
And here’s what it looks like with all four formats on the screen:
Here’s the DDS for the database file.
A UNIQUE A R WAREHOUSE PFILE(WAREHOUSES) A ID A NAME A MFGWHS A STATUS A K ID
Notice that all four field names are used in three places: the database file, subfile format S1, and the window W1. The compiler allocates four areas in memory, and all three record formats will share the four. If I qualify the files, I will have 12 areas in memory instead.
And here, for your reference, is the RPG program without qualified file names.
H option(*srcstmt: *nodebugio) dftactgrp(*no) actgrp(*new) FWHS0020D cf e workstn sfile(S1: RRN1) FWarehouse1uf a e k disk D Reload s n inz(*on) D RRN1 s 4s 0 D Size1 s like(RRN1) D PSDS sds D PgmName 10A overlay(PSDS: 1) *inlr = *on; dow '1'; if reload; LoadS1 (); endif; *in61 = *on; *in62 = (Size1 > *zero); *in63 = *off; Screen = %trimr(PgmName) + '-C1'; Options = '2=Change, 5=Display'; write S2; exfmt C1; select; when *in03; leave; when *in05; reload = *on; other; ProcessS1 (); endsl; enddo; return; dcl-proc LoadS1; *in61 = *off; *in62 = *off; *in63 = *on; write C1; *in63 =*off; RRN1 = *zero; setll *loval warehouse; dow '1'; read warehouse; if %eof(); leave; endif; RRN1 += 1; write s1; enddo; Size1 = RRN1; Reload = *off; end-proc LoadS1; dcl-proc ProcessS1; dow '1'; readc S1; if %eof(); leave; endif; select; when Option = '2'; chain ID warehouse; if %found(); exfmt W1; else; // should never happen // do error routine endif; if not *in12; update warehouse; endif; when Option = '5'; chain(n) ID warehouse; if %found(); exfmt W1; else; // should never happen // do error routine endif; endsl; clear Option; update S1; enddo; end-proc ProcessS1;
Adding Qualification
So what do we need to do differently in order to use qualified files? Let’s begin with the display file. I did not specify the INDARA keyword in the display file originally, since many programmers don’t use it. When you qualify a display file, you must specify INDARA in the display file because you can’t have predefined indicators (in this example, 03, 05, 12, 61, 62, 63) in a data structure. Adding INDARA is the only change that must be made to the display file in this example.
Unqualified: A DSPSIZ(24 80 *DS3) A REF(WAREHOUSE1) A R S1 SFL . . . more code . . . Qualified: A DSPSIZ(24 80 *DS3) A REF(WAREHOUSE1) A INDARA A R S1 SFL . . . more code . . .
You’ll also need to add the INDDS keyword to the file specification in the RPG program.
Unqualified: FWHS0020D cf e workstn sfile(S1: RRN1) Qualified: FWHS0010D cf e workstn sfile(S1: RRN1) F indds(WsInd) dcl-ds WsInd len(99) qualified; ExitKey ind pos( 3); RefreshKey ind pos( 5); CancelKey ind pos(12); SflDspCtl ind pos(61); SflDsp ind pos(62); SflClr ind pos(63); end-ds WsInd;
The indicators for the display file are now accessed through data structure WSIND. (Unless I have a reason not to, I write new code in free-form RPG, even in source members that are completely fixed-format.) I chose to qualify the data structure because I prefer qualified data structures. The compiler doesn’t require the indicator data structure to be qualified.
The next change is not mandatory, but I highly recommend it. Wrap the main calculations in a subprocedure.
Unqualified: H option(*srcstmt: *nodebugio) dftactgrp(*no) actgrp(*caller) Qualified: H option(*srcstmt: *nodebugio) dftactgrp(*no) actgrp(*caller) H main(WHS0010R_Main) dcl-proc WHS0010R_Main; . . . more code . . . end-proc WHS0010R_Main;
Putting the main routine into a subprocedure lets me avoid defining any I/O data structures globally. That is, I can define all the data structures for I/O operations in subprocedures. To me, anything that eliminates global data is beneficial.
Now let’s look at the changes to the file specs.
Unqualified: FWHS0020D cf e workstn sfile(S1: RRN1) FWarehouse1uf a e k disk Qualified: FDisplay cf e workstn qualified F extdesc('WHS0010D') F extfile(*extdesc) F sfile(S1: RRN1) F indds(WsInd) F usropn FW uf a e k disk qualified F extdesc('WAREHOUSE1') F extfile(*extdesc) F usropn dcl-ds S1_t likerec(Display.S1: *all) template; dcl-ds C1_t likerec(Display.C1: *all) template; dcl-ds S2_t likerec(Display.S2: *all) template; dcl-ds W1_t likerec(Display.w1: *all) template; dcl-ds Warehouse_t likerec(W.Warehouse: *all) template; dcl-proc WHS0010R_Main; open Display; open W; . . . more code . . . close *all; end-proc WHS0010R_Main;
I went from two lines to a lot of lines. Is it worth it? Yes!
I added the QUALIFIED keyword to each file. This is not all or nothing. You can qualify some files and leave others unqualified.
Because of the MAIN keyword, I added the USROPN keyword so that I can control file open and close.
I also chose to give the files more manageable names — Display and W — since these are the names I use for qualification. This is not necessary, but I think it makes the code more legible. Since the file names are not the real file names, I added the EXTDESC and EXTFILE keywords to point to the real files.
I defined a template data structure for each record type. These serve as a pattern I can use to define data structures for I/O operations. I can also use these templates to define parameters when I/O data structures are passed between subprocedures. I like to end my template names with _t.
In each subprocedure in which there are I/O operations, I define data structures based on these templates. For example:
Qualified: dcl-proc WHS02R_Main; dcl-ds C1_rec likeds(C1_t); dcl-ds S2_rec likeds(S2_t); . . . more code . . . end-proc WHS02R_Main;
The indicators that control the display file are now defined in the indicator data structure, so the calculations that reference display file indicators must be revised.
Unqualified: *in61 = *on; *in62 = (Size1 > *zero); *in63 = *off; Qualified: WsInd.SflDspCtl = *on; WsInd.SflDsp = (Size1 > *zero); WsInd.sflClr = *off;
Make two changes to the I/O operations. First, qualify the record format name by prefixing the filename and a period. Second, add a data structure for the data.
Unqualified: write S2; exfmt C1; write s1; Qualified: dcl-ds C1_rec likeds(C1_t); dcl-ds S1_rec likeds(S1_t); write Display.S2 S2_rec; exfmt Display.C1 C1_rec; write Display.s1 S1_rec;
The compiler qualifies these data structures automatically because they’re based on other data structures that were defined with LIKEREC. Since the data is in qualified data structures, qualify all references to the fields with a data structure name and a period.
Unqualified: Screen = %trimr(PgmName) + '-C1'; Options = '2=Change, 5=Display'; Qualified: C1_rec.Screen = %trimr(PgmName) + '-C1'; C1_rec.Options = '2=Change, 5=Display';
So far I’ve shown differences in the display file. Since I qualified the database file, I have to make the same sorts of changes to work with it.
Unqualified: FWarehouse1uf a e k disk setll *loval warehouse; read warehouse; chain(n) ID warehouse; Qualified: FW uf a e k disk qualified F extdesc('WAREHOUSE1') F extfile(*extdesc) F usropn dcl-ds Warehouse_t likerec(W.Warehouse: *all) template; dcl-ds Whs_rec likeds(Warehouse_t); open W; setll *loval W.warehouse; read W.Warehouse Whs_rec; chain(n) S1_rec.ID W.Warehouse Whs_rec; close *all;
Do you see why I chose a single letter W as a file name? Using the true file name would have meant not only a lot more typing, but more cluttered and less legible source code.
Moving The Data
The field names in the database file and display file formats are the same, but they no longer overlay one another in memory. EVAL-CORR makes it easy to copy the data back and forth between the I/O data structures. I much prefer this method to copying prefixed fields one at a time.
Unqualified: exfmt W1; update warehouse; Qualified: eval-corr W1_rec = Whs_rec; exfmt Display.W1 W1_rec; eval-corr Whs_rec = W1_rec; update W.Warehouse Whs_rec; eval-corr S1_rec = W1_rec;
I have to shuttle the data back and forth myself, but it’s not a hardship.
Here’s the RPG program with qualified files:
H option(*srcstmt: *nodebugio) dftactgrp(*no) actgrp(*new) H main(WHS02R_Main) FDisplay cf e workstn qualified F extdesc('WHS0010D') F extfile(*extdesc) F sfile(S1: RRN1) F indds(WsInd) F usropn FW uf a e k disk qualified F extdesc('WAREHOUSE1') F extfile(*extdesc) F usropn dcl-ds S1_t likerec(Display.S1: *all) template; dcl-ds C1_t likerec(Display.C1: *all) template; dcl-ds S2_t likerec(Display.S2: *all) template; dcl-ds W1_t likerec(Display.w1: *all) template; dcl-ds Warehouse_t likerec(W.Warehouse: *all) template; dcl-ds WsInd len(99) qualified; ExitKey ind pos( 3); RefreshKey ind pos( 5); CancelKey ind pos(12); SflDspCtl ind pos(61); SflDsp ind pos(62); SflClr ind pos(63); end-ds WsInd; D Reload s n inz(*on) D RRN1 s 4s 0 D Size1 s like(RRN1) D PSDS sds D PgmName 10A overlay(PSDS: 1) dcl-proc WHS02R_Main; dcl-ds C1_rec likeds(C1_t); dcl-ds S2_rec likeds(S2_t); open Display; open W; dow '1'; if reload; LoadS1 (); endif; WsInd.SflDspCtl = *on; WsInd.SflDsp = (Size1 > *zero); WsInd.sflClr = *off; C1_rec.Screen = %trimr(PgmName) + '-C1'; C1_rec.Options = '2=Change, 5=Display'; write Display.S2 S2_rec; exfmt Display.C1 C1_rec; select; when WsInd.ExitKey; leave; when WsInd.RefreshKey; reload = *on; other; ProcessS1 (); endsl; enddo; close *all; return; end-proc WHS02R_Main; dcl-proc LoadS1; dcl-ds C1_rec likeds(C1_t); dcl-ds S1_rec likeds(S1_t); dcl-ds Whs_rec likeds(Warehouse_t); WsInd.SflDspCtl = *off; WsInd.SflDsp = *off; WsInd.SflClr = *on; write Display.C1 C1_rec; WsInd.SflClr =*off; RRN1 = *zero; setll *loval W.warehouse; dow '1'; read W.Warehouse Whs_rec; if %eof(); leave; endif; RRN1 += 1; eval-corr S1_rec = Whs_rec; write Display.s1 S1_rec; enddo; Size1 = RRN1; Reload = *off; end-proc LoadS1; dcl-proc ProcessS1; dcl-ds S1_rec likeds(S1_t); dcl-ds W1_rec likeds(W1_t); dcl-ds Whs_rec likeds(Warehouse_t); dow '1'; readc Display.S1 S1_rec; if %eof(); leave; endif; select; when S1_rec.Option = '2'; chain S1_rec.ID W.Warehouse Whs_rec; if %found(); eval-corr W1_rec = Whs_rec; exfmt Display.W1 W1_rec; else; // should never happen // do error routine endif; if not WsInd.CancelKey; eval-corr Whs_rec = W1_rec; update W.Warehouse Whs_rec; eval-corr S1_rec = W1_rec; endif; when S1_rec.Option = '5'; chain(n) S1_rec.ID W.Warehouse Whs_rec; if %found(); eval-corr W1_rec = Whs_rec; exfmt Display.W1 W1_rec; else; // should never happen // do error routine endif; endsl; clear S1_rec.Option; update Display.S1 S1_rec; enddo; end-proc ProcessS1;
Parting Thoughts
Please allow me to end with a few short observations.
First, when adding qualification to the files, I could have changed the F specs to free form, but I didn’t see a need to do so. Same for the H specs. I typically make new additions in free form and tend to leave old code as is unless I have a reason to change it.
Second, I didn’t try to eliminate global variables. I left the files at the global level. Same with the variables associated with the files, including the RELOAD variable. Moving files into subprocedures requires a different architecture, which is beyond the scope of this article and probably unnecessary for typical business applications.
Last, the RPG source code went from 103 to 161 lines, and many of the lines became more complex. This doesn’t bother me, because the extra code brings peace of mind. It’s similar to another characteristic of my programs: I could use the RPG cycle to write shorter programs, but I don’t.
RELATED STORIES
IBM Knowledge Center – QUALIFIED
Four Ways to Avoid Problems Caused by Global Data
Great article, Ted! Definitely food for thought for my next program.
Hi Ted. Good read, thanks. A totally off-the-subject, random observation if I may…
I noticed that you have the screen name and date in the screen heading, just as I always did, probably out of muscle-memory more than anything else. But having been away from the 400 for several years now, those two little innocuous fields jumped out at me, because it occurred to me that you never see a screen name on any GUI app or webpage — and no dynamic PC app has today’s date on it, because everyone has that displayed in their task bar. But yet, you’ll continue to see both on every green screen display. Muscle-memory…we just can’t help it. 😉
This is one of the better RPG articles (a top 10 contender), and your layout is easily understandable. This is the minimum level of coding we should be practicing (speaking for me). Reducing globals – YES.
Would love to see your take on pushing all display file I/O out of the application, into a service program, under the guise of “Separation of concerns”.