A good procedure is not only bug-free, but is also readable and maintainable by others. REXX is such a powerful language, and allows such a freedom in coding styles that each programmer can easily create his/her own 'dialect'. This may result in procedures that can no longer be maintained by others, or even yourself after a while, especially if the procedures are large and complex. This chapter proposes some of our standards when coding REXX procedures and is based on our long experience with this language.
We are hereby guided by the following considerations:
The procedure is written and maintained on-line on a 3270 screen with a limited size. Our experience is that if a subroutine or logical entity can hold on a single screen page, it can better be maintained. Our mind is easily disturbed when we have to page forward and backward to have a complete view of the logic. The more compact the coding style is, the better in this respect.
REXX Procedures are in most cases programs that result in a series of commands addressed to an external environment (CMS, XEDIT, etc.). Chapter 3 will explain in detail why CP and CMS commands have to be coded with uppercase characters. Other environments may have the same requirements. We therefore promote the rule that you should code the commands to the external environments in uppercase, and everything that is to be handled by REXX in lowercase. The result of applying this coding style is that it is possible to have rapidly a broad idea of what the procedure is really doing to the system, as the important commands are highlighted due to the uppercase characters.
Structuring the program becomes important when it is a larger piece of code. We have however the impression that some coding styles and structuring rules are imposed by people with a more theoretical background and less practical programming experience. The rules are frequently based on a managerial approach, or in some cases on extrapolations of program analysis sheets. Working on paper is very different from working on the screen. Exaggeration in the structure of the programs leads again in complex coding styles, while the same problem can sometimes be solved in a simple compact subroutine.
Over-structured programs tend to resemble a table of contents and an index of very little pieces of logic, while we definitely prefer a program that reads like a novel. We use subroutines only when;
they are re-usable by different parts of the program logic, or
result in blocks of program logic that must be kept limited in size to hold on a screen.
There are hundreds of programming languages. Each language has its typical characteristics, pro's and con's. REXX is a language that is not strong-typed (you don't have to define the variables), with only limited syntactical rules (almost no punctuation or other special characters), and a great freedom in coding style or variable naming (you can define a variable if or do if you like to).
Let's turn these characteristics into our advantage and not impose stringent coding styles and programming structures. Let's only focus on efficiency for the programmer. Another person that has to maintain the program will not get lost more when the style is compact and straightforward.
Look at some examples.
This is all of course our own opinion. You remain free to have another one.
Comments are very useful in procedures. Although REXX is a high level language and thus its syntax self-explanatory to some extent, the functions you execute in the procedure can be unclear to others.
A REXX procedure has to start with a comment to indicate to CMS that it is not an EXEC1 or an EXEC2 procedure.
On OS/2, the first REXX comment line has to start
in column 1.
This first comment should at least contain a very brief explanation of the purpose of the procedure and an indication of the environment where it has to be used. For example, a RXSQL procedure should indicate that it needs access authority to SQL databases.
Other useful indications at the beginning of the procedure can be :
Your PROFILE XEDIT could prepare a standard initialization when you code a new procedure.
Comments have almost no influence on the
execution speed of the procedure.
They only use space on disk and make the loading time a little bit longer.
When you invoke a REXX procedure for the first time
(after creation or modification), the procedure is tokenized
by OS/2. It means, the procedure is kind of pre-compiled, and comments
are removed. All subsequent invocations will use the tokenized version
for quicker loading. The tokenized version is stored in the Extended
Attributes.
Use of mixed case highly augments the readability of the procedure. But here too, some consistency is useful. We propose the following conventions :
We already mentioned that the advantage is that the key functions of the procedure will rapidly become evident to the reader as they are highlighted by the use of uppercase characters. Anyway, chapter 3 will give you other good reasons to code the commands in uppercase.
Remember, the variable names are not case sensitive, which means that the variable name counter is equivalent to COUNTER or Counter or any other combination of upper- and lowercase characters. Internally, REXX uses only the uppercase form of the variable names. It is important to remember this when accessing REXX variables from outside environments, such as EXECIO or Assembler programs.
We for our part will try to be consistent with our convention in our documents. We have set up our XEDIT PROFILE so that it immediately sets CASE to MIXED for REXX procedures.
In a larger piece of code, variable names should be as explicit as possible to give the reader a clue of what it is supposed to contain. Short, one character variable names are good for temporary use, such as the variable "a" here:
a=wordpos(day,'MONDAY TUESDAY WEDNESDAY THURSDAY FRIDAY SATURDAY SUNDAY') if a=0 then say 'Bad day name' else DayOfWeek=word('Mon Tue Wed Thu Fri Sat Sun')
Typical is to use one character variables to DO-loop counters.
In the past, we used more explicit variable names by combining several words, separated by underscores, such as day_of_week. It is very clear what the meaning of the variable is, but the typing of the underscores is cumbersome (at least in Europe where it is an uppercase character on the keyboard), and the reading may lead to confusion with blanks.
Since some time now, we prefer the more compact form, but with a judicious mix of uppercase and lowercase characters, such as our DayOfWeek in the previous coding example.
Don't exaggerate in lengthy variable names either. If it is obvious from the context that you are working with day-names, then Dow may do the trick as well.
REXX allows structured and modular programming. Why not using that ? The editing on screen can also be structured by using indentation. This is most useful when you have to match the end statements with the beginning of the do or select blocks. If everything is coded aligned at the left margin, then finding the matching end of a block is rather cumbersome.
So, indent each underlying coding block by 1 to 3 positions.
If a great number of sub-blocks are needed, indentation with 3 characters can make you end up at the right margin. In this case, your coding is probably too complex and you'd better use calls to subroutines.
In order to limit our building blocks as much as possible to a
single screen size, we personally have opted for a bit more condensed
style. In the next few paragraphs, we
give you examples of these.
This is a first simple case:
do i=1 to 5 j=j+1 end /* i */
If the block is large, indicating the comment in the end statement can be useful when matching it with the start of the block. It is also allowed to use the DO repetitor as parameter of the END, like here :
do i=1 to 5 j=j+1 end i
Remember that XEDIT provides the very powerful ALL macro. If you use the command (might assign this to a PF-key when editing a REXX procedure) :
ALL /DO / | /END / | /SELECT /
you will see only those statements containing one of these three words on your screen (use set shadow off to suppress shadow lines). Verifying matching blocks becomes very easy now. If you want to see the whole procedure again, just enter the command ALL without parameters.
The goodies contain a highly enhanced version
of the ALL command. If you have the time, you should have
a look at
its possibilities (HELP ALL).
Simple case.
if i>0 then j=7
This is an IF statement without ELSE clause nor DO block. We have seen people promoting the rule that an ELSE clause is mandatory, even if it results in a nop statement. We find this a non-sense.
With ELSE clause.
if i>0 then j=7 else j=3
Aligning THEN and ELSE clauses makes a program more readable. Note also that the ELSE clause is a separated clause, which means that if you want to code it on the same line in the procedure, you would need to separate it from the THEN clause by a semicolon (statement separator).
With DO blocks.
if i>0 then do | if i>0 then do j=7 | j=7 a='Hello' | a='Hello' end | end; else do else do | j=3 j=3 | a='Goodbye' a='Goodbye' | end /*else*/ end /*else*/ |
In this case we prefer to code the END clauses under the IF statement instead of under the DO statement as then we can better see the blocks. You could read it as do this if i>0 and do that in other case. It is just a matter of a little mind switch, one that you can learn quickly.
The right part of the figure shows that you can also win a line by coding the END of the THEN block at the same line as the beginning of the ELSE clause, but don't forget the semicolon to make a separate statement of it.
Compare readability of previous example with the following one :
if i>0 then do j=7 a='Hello' end else do j=3 a='Goodbye' end
or even the classic C-programmers coding style...
if i>0 then do j=7 a='Hello' end else do j=3 a='Goodbye' end
30% more lines needed for just this trivial IF clause.
SELECT statements need an END clause to terminate the block. It is a common error to forget it and debugging can be cumbersome. A good practice is to code the select and accompanying end at the same time and then to add some records in between to include the logic.
A SELECT block is scanned only once, so that, as soon as one of the selected conditions is true, the related statements are executed and the rest of the selections are skipped. This is really better performing than coding several IF statements. It is also much more readable, so try to use SELECTs whenever applicable. Coding the most frequent condition as the first WHEN only slightly impacts the performance.
select when i<0 then say 'Too low' when i=0 then do say 'You win' exit 0 end when i>0 then say 'Too high' end /*select*/
This piece of code could be part of a little game where you have to guess for a number. There is no OTHERWISE clause as at least one of the three tested conditions will be true. If however, the WHEN conditions do NOT cover all cases, an OTHERWISE is needed ! Neglecting to code it will make your procedure go in abend.
Rule |
---|
A SELECT block needs at least one true condition !
The REXX Compiler that is available on the mainframe operating systems even requires the OTHERWISE clause. A compiler can indeed not know beforehand if all conditions are covered by the WHEN clauses. |
Again we prefer to code the END under the WHEN and not under the DO, hence we consider is to be a do...when block in analogy to our IF constructs, but that's just a matter of taste.
Next example uses an OTHERWISE clause.
select when i<0 then do say 'Guess too low' say 'Try with a higher number' end when i=0 then do say 'Congratulations, you found it in 'step' steps !' exit 0 end otherwise say 'Guess too high' say 'Try with a lower number' end /*select*/
| Remark that the OTHERWISE implicitly defines a DO-block and that no END is necessary (in fact the END of the SELECT serves also as END of the OTHERWISE). |
Now compare the SELECT solution with the following equivalent IF sequences :
if i<0 then say 'Guess too low' else if i=0 then do say 'You won !' exit 0 end; else say 'Guess too high'
Readability and performance have suffered !
One could say that the next coding would also do the job :
if i<0 then say 'Guess too low' if i=0 then do;say 'You won !' exit 0 end if i>0 then say 'Guess too high'
That's true, but problems arise when the THEN statements of one of the IF blocks modify the value of the variable i ! Look at following piece of code :
if i<0 then say 'Guess too low' if i=0 then do;say 'You won !' answ='' do while answ='' say 'Do you want to play again (Y/N) ?' parse upper pull 1 answ 2 . end if answ='Y' then i=1 /*reinitialize i*/ else exit 0 end if i>0 then say 'Guess too high'
You should verify that the second condition would make the third one true if the player answers Yes to the question. In that case, the message Guess too high would also appear on the screen...
In a SELECT construct, REXX will execute only one true condition and then skip to the END, so that it definitely is an advantage to use the SELECT construct, while not harming readability.
Although REXX procedures can contain statements with a record length of up to 64K characters, editing becomes cumbersome as soon as the statements are longer than the width of the screen area (hence 73 characters as there are 2 attribute characters and the prefix area).
It is possible to split statements on several lines, like this :
if length(input_line)>80 & substr(input_line,7,2)='AB' then do ...
There is no harm in separating the THEN clause from the IF. Even this is possible :
if length(input_line)>80 & , (substr(input_line,7,2)='AB' | , substr(input_line,7,2)='BA') then do ...
The comma is the continuation character.
This was an example of splitting statements, but it is not a very good coding example as in this case, the substr() function needs to be executed twice for evaluating the IF condition. It would therefore be better for performance, to put the result of the substr() function in a variable, and to test this variable.
Here's another example :
say 'The filename is' left(fn,8) 'while the filetype is', left(ft,8) '!'
The SAY statement is continued on the second line. The result on the screen could be something like this :
The filename is PROFILE while the filetype is XEDIT !
Remark that indentation has no effect on the number of spaces in the output string. The continuation character will be replaced by one and just one space character and be followed by the first not-blank character of the next line. Indentation again serves readability as one can see immediately that the say instruction is continued.
|
A comma within a string constant will of course not have the effect of continuation, thus the following piece of code would not have the expected result : say 'The filename is' left(fn,8) 'while the filetype is,' left(ft,8) '!' The comma is within a string constant and is not treated as a continuation character. The line with left(ft,8) is a new statement for the procedure and will probably result in an execution error (the filetype gets executed by CMS... !). |
To continue a clause without intervening blank character, use the concatenation character, such as
say 'The filename is' left(fn,8) 'while the file'||, 'type is' left(ft,8) '!'
When coding long strings, remember the REXX rules : concatenation can be done by :
The following illustrates the basic rules :
Concatenation examples |
---|
type='BALL' stuff='MEAT' something='MEAT'type /* --> "MEATBALL" this is abuttal */ samething='MEAT'||type /* --> "MEATBALL" REXX concat operator */ different='MEAT' type /* --> "MEAT BALL" 2 tokens */ different='MEAT' type /* --> "MEAT BALL" only 1 blank kept */ samething='MEAT'||'BALL' /* --> "MEATBALL" concatenated */ different='MEAT''BALL' /* --> "MEAT'BALL" 2 quotes result in 1 */ something=stuff||type /* --> "MEATBALL" concatenated vars */ |
We don't like to code much || orders, we use abuttal whenever possible. The concatenation operator is needed only when :
We come to another very important coding rule here :
Rule |
---|
Code everything that is NOT variable as a literal string ! |
Let's explore this a little bit. Each word in a statement is considered to be a variable or REXX instruction except if it is coded enclosed by single or double quotation marks (quotes) ('...' or "..."), in which case it is called a literal string.
The value of a
literal string is of course exactly what's
in between the quotes.
The value of a variable is either:
Now, let's have a look at next example :
/* sample procedure */ a=17 ... copyfile profile xedit a oldprof xedit a
The result of REXXs' interpretation will be :
COPYFILE PROFILE XEDIT 17 OLDPROF XEDIT 17
This will be passed to CMS as it is not recognized as a REXX statement, and CMS will of course have problems.
What happens is that the parameters a in the copyfile statement are recognized as a variable whose value was already set to 17. The other words in the statement are also treated as variables because they are not coded as a literal, but as they never were initialized, their value is their own uppercased name.
So, let's correct the previous procedure :
/* sample procedure */ a=17 ... copyfile profile xedit 'a' oldprof xedit 'a'
Now the parameters a become literals (constants) and are not changed during interpretation. So, the result will be :
COPYFILE PROFILE XEDIT a OLDPROF XEDIT a
This will work..., unless :
An address command is coded prior to this statement (Chapter 3 will cover the details of this). In this case, the lowercase characters are no longer translated to uppercase by CMS, and COPYFILE doesn't accept lowercase filemodes.
You modify your procedure some days later, create a variable profile, and initialize it to some value. Your procedure will again give problems.
So, we repeat : Code everything that is NOT variable as a literal string to make your procedure foolproof ! (You could enforce this rule by the use of signal on novalue as we will learn in a later lesson).
The DO statement can be used in several ways (forever, while, until, ...), but typical is that this can lead to an endless loop. Avoiding endless loops remains the responsibility of the programmer, but we will give you some hints and techniques to help you coding better DO loops.
DO WHILE and DO UNTIL are interesting because these allow leaving the loop conditionally. We probably teach you nothing when saying that the difference between both is :
A DO WHILE tests the condition at the beginning of the loop. Consequently, the loop may never be executed if the condition appears to be true the very first time the DO statement is executed. It also means that the variable that is tested in the condition needs to exist (be initialized).
A DO UNTIL tests the condition at the end of the loop, so that the loop is processed at least once. Problems can occur at the end of the loop if the tested variable is not properly initialized.
Both ways can be used to code procedures that can be read like a novel, from top to bottom. We call this straight forward programming, isn't it ? The main advantage is that when you have to analyze the procedure, your mind doesn't have to jump mentally each time your program logic jumps.
A typical example follows here :
answ='' /* initialize answ to null string */ do until answ='Y' | answ='N' say 'Do you want to play another game (Y/N) ?' parse upper pull 1 answ 2 . end if answ='Y' then do ....
You can use this sample when you have to accept a simple user response from the terminal. Here, the parse instruction only accepts the first character and immediately translates it to uppercase.
Do you know what will happen if the user enters one or more blanks before the YES ? If not, code this simple procedure, add a trace ?i before the do until and run it.
WHILE or UNTIL let you terminate (leave) the loop when the condition is true. Sometimes, you may however have several points in the loop where you want to leave it.
The LEAVE statement can be used in that case. It will branch to the instruction immediately following the END of the loop.
In the case where you have nested loops, the LEAVE will terminate the loop at the current level and all its lower levels. If an inner loop has to terminate an outer loop, you can add a parameter to the LEAVE statement that refers to the iteration variable used in the outer loop. For example:
do i=1 to 10 do j=1 to 20 do k=1 to 30 if ... then leave j end end say '....' end
The leave j will branch to the say instruction, effectively terminating both the k- and the j-loop. If the i-loop is not finished yet, it will of course be iterated again.
As you can see from previous example, matching do and end statements is never easy.
When your DO loop uses a variable (like do i=...), then you can code end i. Otherwise, it's good practice to add a comment to the end statement (especially if your loop doesn't hold on one screen). This is an example:
do i=1 to 10 do forever do k=1 to 30 if ... then leave i end k end /* do forever */ say '....' end i
Just as the LEAVE statement can let you exit from the loop, the ITERATE statement can let you branch directly to the end statement of the loop, and thus start a new iteration of the loop. For example :
do i=1 to 10 if (i//3)=0 then iterate k=k+1 end
Here, the variable k will not increment if the loop variable i is a multiple of 3.
We want to spend one final topic on generalized exit routines. The purpose is to have only one single point where the procedure terminates and returns to the caller.
Let's start with an often used but less elegant programming style :
/* our REXX exec : CASE 1 */ ... if SomeThing=wrong then do Say 'Sorry' SomeThing 'is wrong' exit 1 end ... if SomeThingElse=NotWhatWeWant then do Say 'Sorry' SomeThingElse 'is wrong, we stop' exit 1 end ...
Many statements are needed each time an error condition is detected that must terminate the procedure. In this case, it is still simple, but you may have to add statements that close files, detach devices, clean log files, drop lines from the stack, etc.
It is therefore better practice to call a common exit routine. Let's optimize our previous example a bit :
/* our REXX exec : CASE 2 */ ... if something=wrong then do emsg='Sorry' something 'is wrong' retc=1 signal errexit end ... if somethingelse=notwhatwewant then do emsg='Sorry' somethingelse 'is wrong, we stop' retc=1 signal errexit end ... ERREXIT: /* exit with errormessage */ say emsg exit retc
We can improve even more, as follows:
/* our REXX exec : CASE 3 */ ... if SomeThing=wrong then call errexit 1,'Sorry' SomeThing 'is wrong' ... if SomeThingElse=NotWhatWeWant then call errexit 1 , 'Sorry', SomeThingElse 'is wrong, we stop' ... ERREXIT: /* exit with errormessage */ parse arg erc , emsg /* get rc & errormsg */ parse source . . myname mytype . mysyn . /* what's our name ? */ say myname':' emsg /* tell what's wrong */ exit erc
What we specially like in last case is that :
we avoid then do ... end constructs. It is often a problem to relate the do statements with their corresponding end statements, so readability improves if we can avoid some.
we don't use REXX variables just to store stuff that's used only once. It costs a little bit to acquire the storage for the variable, to put the value in it, to add the name of the variable to the variable index string, just to look it up and extract it only once before leaving definitely the procedure.
we do not use a signal instruction, which should be avoided in good programs.
and the first 2 reasons slightly improve the performance of the procedure.
|
We do not use the label error: for our error exit routine, as this label is reserved for use with the signal on error instruction. |
We can still go one step further. Sometimes the program may need to display more than one error message. This is easy to solve, as you can see in the example below:
/* our rexx exec: CASE 4 ----*/ ... if thing=wrong then call errexit 1,'Sorry,' thing 'is wrong',, 'We stop for now' , 'Enter HELP MYEXEC before retrying.' ... ERREXIT: /* exit with errormessage(s) */ parse source . . myname mytype . mysyn . /* who are we ? */ do n=2 to arg() /* get all errormessages to show */ say myname':' arg(n) /* give the n'th message */ end n exit arg(1)
The output on the screen may become something like this :
CASE4: Sorry, QzRLBm is wrong CASE4: We stop for now CASE4: Enter HELP MYEXEC before retrying. Ready(0001);
|
The above techniques are based on the fact that REXX allows passing up to 20 parameters to subroutines or functions, by separating them with commas. |
A parameter may be a whole phrase, not just one word or variable. In our example, the first parameter is the exit return code, the second parameter is the first error message, etc.
One can get these parameters with the parse arg parm1,parm2,... statement or with the arg(n) function.
Note the use of the commas in the call and the
parse instruction. Remark also the 2 ending comma's in the call
to the exit routine :
this is not a typing error !
The first comma is a separator between the parameters, while the
second comma is a continuation character.
Please note that our exit routine signs the messages with the filename of the EXEC, what makes it easier to analyze problems. The use of filename depends on the platform being used; to support CMS, TSO, and OS/2 for example, this code could be used:
/*-----------------------------------------------------------------*/ ERREXIT: /* general errorexit routine */ /*-----------------------------------------------------------------*/ /* Source can be: "TSO COMMAND AVETEST SYS00132 ? ? TSO ISPF ?" ! or: "TSO COMMAND ? SYS00136 ECEMTSO.AVEINIT.EXEC ? TSO ISPF ! First line is for running "membername" from SYSEXEC, second ! is for running something like "EXEC 'ECEMTSO.AVEINIT.EXEC'" */ parse upper source OpSys . myname . myDSN . if OpSYS='OS/2' then parse value filespec('N',myname) with myname '.' else if OpSYS='TSO' then if myname='?' then myName=myDsn do n=2 to arg() /* give errormessages (if any) */ ............
To terminate this discussion, we'll leave you one last, more complete
example to study :
Error exits, extra security feature. |
---|
/* our REXX exec: CASE 4 */ .... if something=wrong then call errexit 1 ,, 'Sorry' something 'is wrong',, 'We stop for now',, 'Enter HELP MYEXEC before retrying' .... 'MAKEBUF' /* make ourself a STACK buffer */ 'Q DISK R/W (STACK' if rc¬=0 then call errexit rc 'DROP' ,, 'You have no READ/WRITE disk',, 'We can''t work' parse pull . /* skip header */ parse pull label . Say 'Your first R/W disk is:' label 'DROPBUF' /* quit our STACK buffer */ .... call exit 0 /***************************** subroutine *****************************/ EXIT: ERREXIT: /* exit with errormessage(s) */ parse source . . myname mytype . mysyn . /* what's our name */ parse arg erc drop . /* get some info out or parameter 1 */ if drop='DROP' then 'DROPBUF' /* perform DROPBUF if wanted */ do n=2 to arg() /* get all the errormessages to show */ say myname':' arg(n) /* give the n'th message */ end n exit erc |
Remark that the EXIT: and ERROR: routines actually result in the same coding, which is perfectly valid for REXX.
This concludes our lecture on coding styles. Chapter 3 is of utmost importance, so take an appropriate amount of time to study this text.
The character used for concatenation (and for logical or) must have the
value x'4F'. On our host
terminals, this is represented by an exclamation
sign and not by a vertical bar, due to national language translations.
On
however, the vertical bar must be used.
Back to text