Style Guide for Rexx
Last-modified: 01 Sept 2000
Style Guide for Rexx
This is a style guide for the Rexx programming language, primarily aimed at Rexx in the OS/390 environment, but application to Rexx on other platforms, and to variants of Rexx such as NetRexx. Many of the points here have been raised at some time on the TSO-REXX listserv mailing list.
Correctly commenting code is one of the most useful things you can do when writing code. Most style problems can be repaired with a formatter, but useful comments cannot be conjured out of thin air.
Comments come in two flavours, winged and boxed:
/* This is a winged comment */ /**************************************\ * This is a * * boxed comment * \**************************************/
Boxed comments should be reserved to highlight major structural elements, such as the start of procedures. See the Calculate_Factorial() example.
Winged comments are useful for describing the purpose of the statement(s) immediatly adjacent to the comment.
It may also be worth borrowing from the Javadoc idea. Comments enclosed by /** ... */ may be read by a suitable program, and the text within associated with the immediately following Rexx statement, and used to automate the creation of external documentation for the program.
The use of asterisks (*) as the box border can make the comment too heavy. Consider using minus (-) or another lighter character instead. Also, don't worry about closing the right hand vertical, as realigning the border after text insertion is a tedious affair. You may have a Rexx reformatter that deals with this problem.
A common use of boxed comments is for an informative header at the start of the exec. Here is an example:
/* Rexx ----------------------------------------- AUTOTOOL <A one line description for use by an indexing tool> ------------------------------------------------------- Copyright: <You may want a copyright notice> Change History: <yy/mmm/dd Userid VersId Description> Description: <A long description of the purpose of the exec. Include invocation arguments, examples of call syntax, returned values, etc.> ---------------------------------------------- MEMNAME */
It is extremely useful to have a tool to handle the creation of a standard header, and to handle change history information. Note the marker in the top right of the box that may be used by a tool to spot whether this is a standard header or not.
An indexing tool that reads the short description and change history is also useful, so make sure the header format supplies information that would be useful to an indexing tool.
In the change history, put the most recent changes at the top, so they are visible when the exec is opened. It may also be useful to have a version id to mark changed code in small comments later on.
It may not be worth putting some types of information in the header, particularly data that ages poorly. Dataset names and pointers to external information are particularly prone to becoming incorrect.
Things to avoid when creating comments:
- Stating the obvious. E.g. string = TRANSLATE(STRIP(string)) /* Strip and uppercase string */
- Using 'content free' phrases such as 'We call this Rexx program to get a returned value to the caller consisting of....'. This also applies to function names, E.g. In Perform_Update_Function(), the words 'perform' and 'function' tell us nothing useful, try Update_Table() instead.
- Using acronyms and abbreviations to the point of crypticness. E.g. /* prt opt pg len */ instead of /* print option - page length */.
Using Symbols and Variables.
The Rexx ANSI standard talks of Rexx 'symbols'. Here I shall use the more familiar terms 'variable', 'variable name' and 'variable value'.
Compound variables, stems, and tails refer to the whole and various parts of compound variables.
Always make your variable names useful and meaningful.
Some programmers use a form of Hungarian Notation to show when a variable is boolean, a loop index, numeric, etc. This may make your variable names not so easy to read, and has other dissadvantages.
Consider using i, j, k, etc as loop control variables. There are advantages and disadvantages to this:
+ Faster execution speed. Single character variable names show a performance improvment (My simple test measured nearly 10% improvement).
+ Compound variables names are shorter and less likely to make long statements cross onto multiple lines.
- You have to rely on the stem name to indicate the meaning of the data in the compound variable. E.g. Consider compound var names record.i and score.teamIndex.eventIndex . It is obvious that i is the record number, but would it be obvious from score.i.j that i is the team and j is the event?
Global variables can be handled in the following way:
First, in the opening section of your Exec, set a variable named global whose contents are the stem names of your global variables:
global = 'gl.' /* set a list of stems that are global variables */ gl.testVar = 'This is a sample global variable.'
Then, at each procedure start, expose this global, and its contents:
An_Internal_Function: procedure expose (global) say gl.testVar /* ==> This is a sample global variable. */ return 0
Do use the procedure keyword with every defined procedure. This hides the procedure's internal variables from the caller, and allows the use of i,j and k as loop control variables throughout your program without any side effects, as in this example:
do i = 5 to 10 say 'Number:'i 'Factorial:'Calculate_Factorial(2,i) end exit /* -------------------------------------- Return the multiple (factorial) of all numbers between arg1 and arg2. -------------------------------------- */ Calculate_Factorial: procedure parse arg startNum,endNum factorial = 1 do i = startNum to endNum factorial = factorial * i end return factorial
This common problem occurs when a variable is assigned a value, and the variable is then used in a compound variable:
stem.salutation = 'HELLO!' /* literally stem.SALUTATION = 'HELLO!' */
/* Output the intended result */ say stem.salutation /* ==> HELLO! */
/* Set the tail of the compound variable to have a value */ salutation = 'GOODBYE'
/* Now show an often unintended result */ say stem.salutation /* ==> STEM.GOODBYE */ exit
If you don't want this effect, you must use unassigned variable names in the compound variable. You could use non-alphanumeric characters such as ! or ? to prefix the variable name when its used in the compund tail, or use a numeric prefix, as symbols starting with numerics are, by definition, constants.
testVar = 'HELLO' stem.0testVar = testVar stem.!testVar = testVar
Numeric prefixes are better for code portability.
The ISPF editor picks special characters (such as !) up in HILTE mode.
Non-alphanumeric characters may not be portable to foreign EBCDIC character sets, and are not portable to some Rexx extentions, such as Object-Oriented Rexx.
Large blocks of variable assignations can be split into columns, with variable names, variable values, and winged comments:
gl.Bdebug = 0 /* Turns debugging on/off */ gl.!pageLength = 60 /* print option - page length */ gl.!pageWidth = 50 /* print option - page width */
Case, Indentation and Block Structure.
Here we discuss the case we could use for syntactic elements, what indentation is appropriate, and how we could break blocks of statements up.
But first, What are we trying to achieve, and how do we go about achieving our aim.
We are trying to increase the readability of the code.
We can improve readability by using techniques to differentiate and highlight elements within a statement, show relationships between statements, and lay out the code so that these relationships and statement elements are visible to the reader.
The target audience should be anyone likely to need to change your code in the future, including yourself. Don't assume any knowledge of the inner workings of your code, it may be some years before you revisit it. A reasonable knowledge of Rexx may be assumed, even though the reader may be new to the language. Everyone has to learn something sometime.
Here are some elements that go to make up statements:
- Keywords such as IF, SELECT and CALL.
- Variable names such as USERNAME, or USERCARDNUMBER.
- Major Blocks such as DO count - END, SELECT - END and label: PROCEDURE - RETURN.
- Contitional Blocks such as IF cond THEN DO - END and WHEN cond THEN DO - END.
- Natural Blocks of statements that together perform a particular function.
- Builtin function names such as POS(), D2B(), and TRANSLATE().
- Implementation supplied builtin external functions such as MVSVAR() and LISTDSI().
- User supplied external functions such as MYFUNC().
- Internal functions such as MYINTERNALFUNCTION().
- Commands to external environments such as ADDRESS TSO "LISTCAT"
- Literal text.
Here are some techniques to improve readability using case:
- UPPER, lower and Mixed case.
- Concatenated words with case differentiation such as ConcatenatedWords.
- Underscore separated words such as underscore_separated_words.
Other techniques to improve readability are:.
- Indenting blocks or statements within blocks.
- Aligning the start of a block with its end.
- Splitting statements across more than one line.
- Inserting blank lines between blocks.
- Using boxed comments before major structural items.
We are also concerned with breaking up the code into manageable and reusable units. We can do this by using procedures, and by surrounding blocks of statements that perform a task with white space.
Constraints and Influences on Style Guide Rules
There are some facts about the programming environment that influence what techniques may be most effective.
External function names are related to file names in a file system, or member names in partitioned datasets. MYFUNC() may refer to PDS 'user.EXEC(MYFUNC)' or file name MYFUNC.REXX, depending on your Rexx environment. By using upper case for external function names, you avoid having to quote function names to maintain lower case in the file name. You then, of course, must use upper case in the file names of external functions.
Screen size influences how much code can be on the screen at one time. Using too many blank lines or splitting statements over multiple lines can move code out of the viewable area. Techniques that disperse statements over several lines work well on window based systems that allow the viewable area to be adjusted, but are restricted by the fixed terminal size of 3270 systems. The same is true for boxed comments.
Some editors are Rexx aware and can hilite Rexx code to show keywords, quoted strings and other elements. A Rexx aware editor such as ISPF EDIT with the HILITE utility, can for instance turn all keywords red, and all comments blue. There's no point in using upper case to highlight keywords, when the editor can pick them out with colour.
Keywords appear in isolation in the text, and also tend to appear at the start of statements, so they stand out without any help from the programmer.
Function and variable names tend to appear close together in code. We need to be able to differentiate variable names from function names, and also differentiate between the various flavours of function; internal, external, builtin, etc. For example:
datasetName = STRIP(TRANSLATE(GetDsn(ddName)),,"'") or datasetName = STRIP(TRANSLATE(Get_Dataset_Name(ddName)),,"'")
Literals should be quoted, unless you can be sure that the literal hasn't been used as a symbol elsewhere.
Commands being passed to external environments often have single quotes in them. E.g. address TSO "ALLOC FI(TEST) DA('MY.DATASET') SHR REUSE".
NetRexx (and presumably other close relatives of Rexx) are sufficiently different from Rexx to make consideration of styles that apply to NetRexx irrelevant. Having said that NetRexx is a case insensitive language, and most style rules should apply to it.
An Example Set of Style Rules.
Here is programming style that meets the above criteria in varying degrees. It tries to keep the code dense for 3270 displays, and relies heavily on a highlighting editor to differentiate between keywords, quoted text, variable names, and other statement elements.
Rules specifically related to case:
- Keywords in lower case. Let the editor highlight them. E.g. select do if end
- Variable names starting in lower case with the initial letters of concatenated words in upper case. This differentiates them from function names, which will all start with an upper case character. E.g. myName counter fishType
- Internal function names with the initial letters of individual words in upper case. Optionally, you can also use underscore separators E.g. MyFunction() MyOtherFunction() Your_Function()
- External function names in upper case. This saves having to quote them. E.g. MYFUNC() EXTFUNC()
- Builtin functions in upper case. This rule is pretty arbitrary, as the highlighting editor will mark them in a different colour to user supplied functions anyway. E.g. SUBSTR() D2B() MVSVAR()
- External environment names in upper case. Note that address treats the environment name as a constant, and does not perform symbolic substitution. E.g. address TSO "X", address ISPEXEC "FTCLOSE", address SH "cat myfile"
- Commands for external environments preferably in upper case, unless the environment is case sensitive. E.g. address TSO "ALLOC FI("ddName") SHR REUSE DA("dsName")"
Rules for quoting:
- Enclose commands to external environments in double quotes, coming out of quotes to pass variables into the command. E.g. address TSO "ALLOC FI("ddName") SHR REUSE DA("dsName")"
- Always quote literal strings. Never rely on the string not being used as a variable name. E.g. myVar = 'test'. Never use myVar = test
- Use variables for quotes if building strings containing both single and double quotes to avoid the opening and closing becoming confusing: E.g. myVar = dq||sq||'test'||sq||dq rather than myVar = '"'"'test'"'"'
Rules for procedures:
- Use the procedure keyword wherever possible, to avoid collisions between variable names in different procedures. E.g. My_Function: procedure expose (global).
- Use commas and not spaces to delimit your arguments. See the Calculate_Factorial demo for an example.
- Use a comment in the style of javaDoc before each procedure, so that possible future 'rexxDoc' implementations will be able to pick up your comments. See the LZ78 demo for an example.
Rules for comments:
- Use a standard machine readable header to document a synopsis of the exec, change information, and any other useful information. Create a tool to manage and extract useful information from these headers.
- Comment logical groups of statements, indicating what they do. E.g. do loops, select blocks.
- Aim your comments at a reasonably competent programmer who is not familiar with your exec.
- Comment what end statements are ending. E.g. end /* select */.
- Indicate what variables are to be used for when they are first assigned. See the example in an earlier chapter.
- Use a boxed comment to mark where procedures begin. See the LZ78 demo for an example.
Other miscellaneous rules:
- Use i, k, j, etc. as loop control variables. This can make your code run faster, and helps keep statements short..
- Indent do, select, and other similar structures with between 1 and 3 spaces.
An aside on following a ruleset: One important comment that must be made, is that it is not so important that one set of rules is followed in all cases, but it is important that a set of rules is followed consistently in a piece of work. Leaping from one style to another mid-procedure can hinder a readers understanding of the program.
Here's an example program, with two similar internal functions present, in two slightly different styles. The compress function follows the above example set of guidelines. The uncompress function follows a second consistent set of guidelines. Note how the uncompress function is easier to read when no colour highlighting is used. Copy the program into a Rexx sensitive editor, and the compress function becomes easier to read.
/* REXX ---------------------------------------------------------- SYNOPSIS : LZ78 compressor/decompressor demo VERSION : 1.0 CREATED : April 1999 NOTES : From the uberFish Rexx Style Guide. --------------------------------------------------------------- */ /* Demo normal text compress/decompress */ say 'Normal text demo.' inString = 'If_I_were_a_Linesman,_' , || 'I_would_execute_the_players_who_applauded_my_offsides.' say 'Compressing 'LENGTH(inString)' byte string .....' say inString compString = Compress_LZ78(inString) say 'Compressed as.....' say compString decompString = Uncompress_LZ78(compString) say 'Decompressed as.....' say decompString /* Demo Highly redundant text compress/decompress */ say say 'Highly redundant demo.' inString = 'Oogachacka,Oogachacka,Oogachacka,' inString = inString||inString||inString||inString inString = inString||inString||inString||inString say 'Compressing 'LENGTH(inString)' byte string .....' say inString compString = Compress_LZ78(inString) say 'Compressed as.....' say compString decompString = Uncompress_LZ78(compString) say 'Decompressed as.....' say decompString exit 0
/** ================================================================ Demo LZ78 compressor. In reality, the output would be formatted to take up less space then the original, E,g, in 9bit bytes, with the high order bit set on when the char is a dictionary reference, or something. Oh, and it doesnt like spaces, cos I use WORDPOS() to search the dictionary. ----------------------------------------------------------------- */ Compress_LZ78: procedure parse arg inString /* Initialise the dictionary, output string, etc */ outString = '' /* output string */ d = '' /* Dictionary. Blank delimited 'words' */ w = '' /* Last used phrase */ outCode = '' /* Our current dictionary reference for this phrase */ /* For every char in the input string, output the char, or a dictionary reference */ do i = 1 to LENGTH(inString) /* Get next input char, and make phrase */ thisChar = SUBSTR(inString,i,1) thisPhrase = w||thisChar /* If the new phrase is in the dictionary, queue up this dictionary reference to go into the output string */ if WORDPOS(thisPhrase,d) > 0 then do if outCode = '' then outString = LEFT(outString,LENGTH(outString)-1) outCode = '<'WORDPOS(thisPhrase,d)'>' w = thisPhrase end /* If the new phrase wasnt in the dictionary, output it */ else do outString = outString||outCode||thisChar outCode = '' d = d thisPhrase w = thisChar end end /* return the compressed string */ return outString||outCode
/** ================================================================= Demo LZ78 uncompressor. Here Ive switched to another style for comparison. ----------------------------------------------------------------- */ Uncompress_LZ78: procedure parse arg inString /* Initialise stuff */ outString = '' d = '' w = '' /* Loop across each char in input string, building dictionary and output string as we go */ Do i = 1 To Length(inString) /* Get next char from input string */ thisChar = Substr(inString,i,1) /* If the char is a code, then look up code in dictionary, add new phrase to dictionary, add phrase to output */ If thisChar = '<' Then Do thisCode = Substr(inString,i+1,Pos('>',inString,i)-i-1) thisOut = Word(d,thisCode) d = d || ' ' || w || Left(thisOut,1) w = thisOut i = i + Length(thisCode) +1 outString = outString || thisOut End /* Else add phrase to dictionary and output the character */ Else Do d = d || ' ' || w || thisChar w = thisChar outString = outString || thisChar End End
/* Return the uncompressed string */ Return outString
Spot the line that is longer than 80 characters. In ISPF it should be wrapped in whatever way is consistent with your treatment if IF THEN DO constructs:
if outCode = '' then outString = LEFT(outString,LENGTH(outString)-1)
Enforcing Programming Standards.
There's little point in setting guidelines for writing programs if they're going to be ignored. You have to make people want to follow your guidelines.
Here are some ways of encouraging the use of programming standards:
- Allow individuals to follow the guidelines as they see fit.
- Use management control.
- Automate the process by supplying easy to use tools to manage internal documentation and format code.
Relying on individuals can fail if lazy or naive programmers fail to follow guidelines. It does allow creative and expert programmers the freedom to use systems they are comfortable and productive with.
Management control is particularly suited to organizations that have a Quality Assurance program.
A tools led approach can be combined with other approaches. There is some development effort to get tools into place, and the tools must be flexible and fit for their purpose. Generally, a handful of ISPF EDIT macros to perform various formatting tasks will suffice.
There are two schools of thought on whether to explicitly use the string concatenation operator ( || ) or not.
say 'Explicit use of operators' newString = oldString || ' Some Text ' || MYFUNC('ham','eggs') say Status_Char(userStatus,'TEXT') || ': ' , ||userName || ' is ' || userStatus
say 'Omission of operators.' newString = oldString 'Some Text' MYFUNC('ham','eggs') say Status_Char(userStatus,'TEXT')':' userName 'is' userStatus
There are some good reasons to use concatenation operators, and some good reasons not to.
Points for the use of the concatenation operator:
- Programmers supporting your code often do not use Rexx regularly.
- Component elements of the string will be explicit to the reader.
- Unexpected spaces can creep in when ommitting concatenation operators.
Points for ommiting the operator whenever convenient:
- Less is more. The resulting code is tidier.
- The Hilite function of the editor makes the component elements clear enough anyway.
- There is less chance of the statement overrunning onto the next line.
- You don't have to explicitly include spaces inserted between the strings. E.g. var1 || ' ' || var2 versus var1 var2
Some Other Dos and Don'ts
Don't use too many levels of indirection.
For instance, there is often little use in storing ISPF panel names in variables when address ISPEXEC "DISPLAY PANEL(MYPANEL)" pinpoints exactly where in your exec that panel MYPANEL is actually used. Having functions call functions that call functions can equally make debugging an exec very tiresome indeed, so make sure that you comment what is going on if you do have to create a complicated structure.
Do make sure abstracted procedures allow full access to the underlying data.
For instance, say you put a nice shell exec around TSO LISTCAT. Your shell program should be able to control all arguments for LISTCAT, and return any information returned by LISTCAT to the caller. Otherwise you may have to create a new interface for your old shell if new LISTCAT data is needed. Adding new functionality to old code creates backwards compatibility problems, and maintenance problems as the code becomes 'hairy' from unnecessary revisions.
Don't hide information in variables set far away from where they are used.
Its one thing to have a nice block of commented assignations near the top of an exec or procedure, but its another thing entirely to place assignation statements in obscure parts of the exec, far away from where they will actually be used.
Do end all internal procedures with an unconditional RETURN.
This makes it clear to the reader when a procedure ends and may help code formatting tools to spot the end of internal procedures.
Please drop me a line, and tell me about any improvements you think could be made to this page.
Was the information useful?
Was the information complete and concise?
Did you understand the information?