DrDialog, or: How I learned to stop worrying and love REXX - Part 10

From EDM2
Revision as of 08:42, 15 September 2018 by Ak120 (Talk | contribs)

Jump to: navigation, search

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 loops

'Head-controlled' means, that the condition for repeating the loop (execution of the body part) is checked each time BEFORE the body is executed.

Tail-controlled loops

...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

Example:

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

...will output:

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! ;)

References:
GuiObjectREXX Yahoo! group: http://groups.yahoo.com/group/GuiObjectREXX/
News group for GUI programming with REXX: news://news.consultron.ca/jakesplace.warp.visualrexx
Download from Hobbes: http://hobbes.nmsu.edu/cgi-bin/h-search?key=drdialog&pushbutton=Search
IBM Redbook - "OS/2 REXX: From Bark to Byte"