DrDialog, or: How I learned to stop worrying and love REXX - Part 10
By Thomas Klein
Last time we started to look at the first two types of program flow types: Batch (sequence) and branches (conditional expressions). Today, we'll conclude this discussion by looking at iterations - or 'loops' if you prefer.
Loops are basically used to repeat a certain processing until a specific condition applies like reading each line (record) of a file from top to bottom until the end of the file is reached. Loops are found in all programming languages. Basically, they consist of three parts: Head, body and tail.
The body is made up of the command(s) to be repeated in each "lap" of the loop, while head and tail are used to control the iterative process.
This is where things need to be explained in detail: Head and tail are not meant to be specific statements of a programming language but rather show their function in a structured scheme of how the loop works. Basically, we must differ between to types of loops (or how loops behave).
'Head-controlled' means, that the condition for repeating the loop (execution of the body part) is checked each time BEFORE the body is executed.
...are different in the way, that the condition for repeating the loop (executing the body part) is checked each time AFTER the body was executed.
This means, that if the condition for repeating the body part does NOT apply, a tail-controlled loop will run 1 time, while a head-controlled loop won't be executed at all. In Rexx, a head-controlled loop looks like this:
DO WHILE <condition> <command-1> ... <command-n> END
A tail-controlled loop (where the condition is checked after the body is executed) looks like this:
DO UNTIL <condition> <command-1> ... <command-n> END
The catch: Although both types of loop carry their respective condition in their "top", it doesn't mean that they are both head-controlled, as this simply ain't the case and the behave completely different. To get the difference, let's assume a loop which is repeated until the value of a variable named XYZ is greater than 4. Within each lap of the loop, the value of XYZ will be displayed and then incremented by 1. So far so good... now let's assume, that XYZ already holds an initial value of 7 before the loop is started. A head-controlled loop checks the condition before the body part is run, thus, using a head-controlled loop should result in nothing to happen:
XYZ = 7 DO WHILE XYZ \> 4 SAY XYZ XYZ = XYZ + 1 END
Note that the condition is defined as "while xyz is not greater than 4") by using a backslash to "negate" the "greater" sign (">"). And in fact, if you run this piece of code, there'll be nothing displayed.
Now let's go for the tail-controlled type of the same loop (by using a condition made of UNTIL instead of WHILE)...
XYZ = 7 DO UNTIL XYZ > 4 SAY XYZ XYZ = XYZ + 1 END
In this case, we used "until xyz is greater than 4" which quite equivalents "as long as xyz is not greater than 4". So actually there should nothing happen as well. But... bad luck! The body part will be run one time, although the condition doesn't apply - because this is checked AFTER the execution of the body part.
That's somehow puzzling, isn't it? ;)
So far, this was about the basic components of "conditional loops".
Another "flavour" of loops is the one that will only run a specified amount of times. The REXX.INF file calls them "counter loops", but in fact they're nothing else but another kind of conditional loops as well with a "built-in" condition.
While conditional loops might run over and over until a certain condition applies (or no longer applies), those counter loops are repeated a fixed amount of times. The amount of repetition (the "laps") are specified in the loops head. Counter loops can be made up of a simple, direct amount of repetition like in...
DO <times> <command-1> ... <command-n> END
Where <times> is a number. Here's a simple example:
DO 3 say "hello" END
The other type of counter loop (which is a lot more versatile and thus the one used in most cases) comes with a loop variable (or "counter variable" if you prefer) which is incremented by a given amount during each lap. The full syntax is:
DO <variable> = <start-value> TO <end-value> [ BY <increment> ] <command-1> ... <command-n> END
DO laps = 1 TO 10 BY 1 say "This is lap no.:" laps END
Now, as you might recall from previous syntax diagrams, the square brackets denote an optional part. Thus, the incremental value does not need to be specified. In this case, an increment of 1 is assumed. The same behaviour as with the above example this can be achieved by coding:
DO laps = 1 TO 10 say "This is lap no.:" laps END
Note that if you want to do a "reverse" loop, you have to use a negative incremental value. If you don't, your loop will run forever, as the end-value for your counter loop won't ever be reached. Thus, if you want to have something like a "countdown loop", you go by this:
DO laps = 10 TO 0 BY -1 say laps END
Of course, the start, end and incremental values entirely depend upon your needs. Note that the end value does not need to be hit exactly - if the counter value either matches the end value or is "beyond" the end value, the loop is terminated - example:
DO myval = 7 TO 94 BY 17 say "current value is" myval END
This will result in the counter starting with 7 and turn into 24, 41, 58, 75, 92 and finally 109. But 109 is larger than 94 (the end value) and thus the loop will terminate with the value of 92 being processed.
Important note: If we add a final SAY after the end of the loop, you'll see that myval indeed was incremented to 109 but the loop was left prior to process that value:
DO myval = 7 TO 94 BY 17 say "current value is" myval END SAY "after loop:" myval
current value is 7 current value is 24 current value is 41 current value is 58 current value is 75 current value is 92 after loop: 109
Thus, a counter loop actually behaves like a "head-controlled" conditional loop. The condition is checked prior to execution of the loop body.
This behaviour is important to know if you plan to use the counter variable again after the loop has ended... which we'll do in a following sample.
The same above rules apply to a "reverse" loop: The final value does not need to be matched exactly, any value "beyond" the end value will also terminate the loop:
DO myval = 99 to 1 BY -15 say "current value is" myval END say "after loop:" myval
This will output:
current value is 99 current value is 84 current value is 69 current value is 54 current value is 39 current value is 24 current value is 9 after loop: -6
Well, that's it for the basic principles of loops... happy "looping"!
Extended loop control
Sometimes, one needs to react upon certain conditions while performing a loop. The most common cases are
- terminating a loop directly without "waiting" until the end value or end-condition was reached
A common example is looking up entries in a "table" until a match was found. In this case, you don't want to go through the rest of entries but rather "quit" immediately.
- skip a single lap (avoiding unneeded processing of the body part commands)
This comes in handy if a simple check can be used to determine whether the lap needs to be executed or not
Case 1: Terminating directly
Let's use an example. You have a "table" of names and phone numbers which is made up of eight entries. A compound variable named "NAME" stores 8 names and another compound variable named "PHONE" stores the phone numbers. They are matched by their corresponding entry number, thus phone#1 belongs to name#1 and so on. This could look like this:
name.1 = "Peter" name.2 = "Paul" name.3 = "Mary" name.4 = "Jack" name.5 = "John" name.6 = "Jim" name.7 = "Mom" name.8 = "Dad" phone.1 = "555-99887" phone.2 = "555-88776" phone.3 = "555-77665" phone.4 = "555-66554" phone.5 = "555-55443" phone.6 = "555-44332" phone.7 = "555-33221" phone.8 = "555-22110"
Now we want the program to start by saying "Please enter name", look up the name and display the phone number if a matching name is found. We assume the names to be unique (there can be only one "Peter" for example). Then, the program terminates.
Okay, we actually should use a loop here too which repeats the whole processing until "QUIT" was entered or something like this, but let's try to keep it simple at first:
After the above statements (the compound variable setup) was coded, here we go with the rest:
SAY "Please enter name" PARSE PULL input DO entry = 1 to 8 IF name.entry = input then say name.entry ":" phone.entry END
And that's it for the first attempt.
The program accepts the input and goes through all entries. If a name matches the input, the corresponding phone number will be displayed.
But: The loop goes on to process the rest of the entries, which actually is not necessary because if a name was found, there can't be another one as we said the names are unique. So what we might need is a way of terminating the loop directly once a match was found. Imagine a phone book of 500 entries or more and the first entry already matches the input, but the program keeps going through the rest of 499 entries... would you act the same way too?
And this is where REXX command LEAVE comes into action:
SAY "Please enter name" PARSE PULL input DO entry = 1 to 8 IF name.entry = input then DO say name.entry ":" phone.entry LEAVE END END
As we need more than one statement within the IF, we'll need to enclose them into DO...END (remember this from our previous article!). Now the loop will quit after displaying the match directly, skipping all remaining "laps". Hmm... I don't like it. If there's no match, nothing is displayed and the program just "silently" quits. No, no... we have to change it. Why not make use of what we know about loops so far? Take a look here:
SAY "Please enter name" PARSE PULL input DO entry = 1 to 8 IF name.entry = input then LEAVE END if entry > 8 then say "No match found for <"input">" else say name.entry ":" phone.entry
Yeah... that's a lot better... but what does it do?
First note that LEAVE will quit the loop with leaving the counter variable at its current value. Second, remember that if a loop was entirely processed, the counter variable is beyond the end value. Now all we did was make use of this behaviour:
The program goes through one entry after another, incrementing the counter value. If a match was found, the loop is left by using LEAVE. If no match was found, the loop is terminated as well, because the counter variable has exceeded the end value. After the loop was left (by either of the two ways), we simply figure out HOW it was left: If the counter value now has a value that exceeds the end value of the loop, it means that all entries have been compared but no match was found. If not, there must have been a match and the counter variable holds the entry number of the match.
One more note: The programs is not THAT good, as it doesn't match names entered in upper or lower case. If you want it to be more useful, just enter all names in uppercase when setting up the compound variables. Next, omit the PARSE command for querying a user input. This will make REXX turn all input into entire upper case letters and you'll have perfect matches, regardless of whether "Peter", "PeTeR" or "peter" was entered. In addition, you might want to remove any leading or trailing blanks from the user input... so here's the complete program sample for you to play around with it:
/* REXX loop sample 1 */ name.1 = "PETER" name.2 = "PAUL" name.3 = "MARY" name.4 = "JACK" name.5 = "JOHN" name.6 = "JIM" name.7 = "MOM" name.8 = "DAD" phone.1 = "555-99887" phone.2 = "555-88776" phone.3 = "555-77665" phone.4 = "555-66554" phone.5 = "555-55443" phone.6 = "555-44332" phone.7 = "555-33221" phone.8 = "555-22110" SAY "Please enter name" PULL input input = strip(input) /* remove leading and trailing blanks from input */ DO entry = 1 to 8 IF name.entry = input then LEAVE END if entry > 8 then say "No match found for <"input">" else say name.entry ":" phone.entry /* end of sample code */
Case 2: "Skip a lap"
Skip the processing within a loops body to go on with next lap is achieved by using REXX command ITERATE. The benefits from using ITERATE increase the more complex the processing is which is "skipped". This means, that if your loop body is made up of a simple command, there is actually no need to use ITERATE. However, if a time-consuming process is part of each lap, ITERATE might speed up your loop tremendously.
Anyway - to show you an example, let's recall what people say about the 1970's: "If you remember the 70s, you weren't there".
Okay - why not do them a favour? Let's do a loop that omits the seventies from last centuries list of decades (starting with the 1940's)...:
do decade = 40 to 90 by 10 if decade = 70 then ITERATE say "I still remember the" decade"'s" end
Run this code to see what'll come out... ;)
See how it works? All instructions within the body that follow ITERATE will be skipped.
In the above example, execution of the body (consisting of the text output) will be skipped if the counter variable equals 70.
Of course one could achieve the same without using ITERATE by coding:
do decade = 40 to 90 by 10 if decade \= 70 then say "I still remember the" decade"'s" end
In this case, the output text will only be displayed if the loop variable is NOT equal to 70.
ITERATE thus is only an explicit command to show that a lap should be skipped if a certain condition applies - a self-explaining command to provide better readability if you want. But at least you now know that there is something called ITERATE. This will leave you more "flexible" in constructing program (loop) logic.
Well, that's it again for this month. According to my own road map, we'll take a look at the wealth of REXX's string manipulating functions. And if there is enough time left, we'll get into the subject of the REXXUTIL function library. That'll be both interesting and fun. Then, we'll get back to the actual DrDialog programming and see what were able to do now that we learned some basic REXX skills... and we'll play around with special tricks.
See you next month, same spot, same time! ;)