HOME  →  Lazarus Tutorials TOC     Other material for programmers
Delicious.Com Bookmark this on Delicious     StumbleUpon.Com Recommend to StumbleUpon

The "fall-through loop": A useful structure

This page has good information, and a search button at the bottom of the page
Please don't dismiss it because it isn't full of graphics, scripts, cookies, etc!

This page is "browser friendly". Make your browser window as wide as you want it. The text will flow nicely for you. It is easier to read in a narrow window. With most browsers, pressing plus, minus or zero while the control key (ctrl) is held down will change the texts size. (Enlarge, reduce, restore to default, respectively.) (This is more fully explained, and there's another tip, at my Power Browsing page.)


I have one program which has been "growing" for more than 25 years. In 25 years, I've learned a thing or two, and someday I am going to re-write that program from scratch, and I'm going to use a "fall through" loop (my name) at the heart of it.

This essay explains what I mean by a "fall through" loop, and an explanation of how you might implement one in Delphi or Lazarus. There is nothing to stop you from using the same idea in other languages though.



The problem I wanted to solve

I'm going to describe the problem I had which could be solved with a fall-through loop. Don't get distracted by the details of what I wanted... many, many problems are similar. Use just a little imagination, and I expect that you will discover that a fall-through loop would be useful to you, too.

I have various electronic devices for taking measurements of my local weather. Temperature sensors (indoor, outdoor, water leaving central heating furnace, etc), barometric pressure, wind speed, humidity, etc, etc.

The heart of my program for monitoring those devices could have, in pseudo-code form, consisted of....

InitializeEverything;

Repeat
  ReadAndDealWithIndoorTture;
  ReadAndDealWithOutdoorTture;
  ReadAndDealWithFurnaceTture;
  ReadAndDealWithBarometer;
  ReadAndDealWithAnemometer;
  ReadAndDealWithHumiditySensor;
Until false;

Each line above (apart from the "Repeat" and "Until false" is just a call to a subroutine I would have to supply, to build the program this way.

By "ReadAndDealWith..." I mean to indicate that the subroutine would read the sensor, and take care of updating the display which the weather watching program creates. (My program also records the data it collects to a machine readable file.)

There are all sorts of problems with the structure above.

Windows, and other multi-tasking environments, don't do well if a program has a loop with no "breaks" in it. More modern versions of Windows do a good job of coping, but it is so much better if the programmer avoids creating loops which may try to monopolize the processor.

The problem of working in a multi-tasking environment proved bigger than I had realized. Several days into working on this tutorial, I began discovering that what I propose in the following just won't, quite, work as I wanted it to. That nearly infinite "repeat/until" loop was the heart of the problem. It is hard to get a Windows (or other multi-tasking environment) program to "play nicely" when you create such loops.

I can't at the moment afford the time to re-write all that you see here. For now, all I can say is that you can use the ideas presented in a Windows program. The block that is inside the "repeat" and "until" goes inside the handler for a timer. I don't really like that "solution", as it introduces "wasted" time, and will prevent the cycle from executing as fast as it might. But I haven't yet found a better solution. Sorry. The "wasted" time will not matter in many cases, but it is inelegant. Sigh.

The "bad plan" (above) can be made less bad by sticking an "application.processmessages;" statement in, just before the "Until false;" (While that might work, in theory, to make the program respond to the usual "kill program" Windows mechanisms (Clicking the red "x", upper left, pressing Alt-F4)... it doesn't. See "doing it in practice" at the bottom of the page.

Now, once in a while, at least, the program says to Windows, "Oh, if anyone else was trying to do something, tell me." But, as I said, it will only happen once in a while. Some of the subroutines which are executed each time you go through the loop may take a while to finish... and the cumulative delay may be too much. You could end up with a system which was annoyingly laggy. ("application.processmessages;" is a Delphi/ Lazarus specific command. If you are using a different language, it will have something equivalent.)

The bad plan also assumes that there is no common ground between the various sub-tasks... and there is, in the case of my weather logging program. Each time you read a sensor, the result needs putting on the screen, if only as a number, and it needs logging to a data file.

So we're going to "split out" those two activities.

To do it will mean that we need to establish at least three variable which will pass information between the different parts of the program. We'll come to them in a moment, but first the improved plan for the structure of our program....

InitializeEverything;

Repeat

  ReadIndoorTture;
  ReadOutdoorTture;
  ReadFurnaceTture;
  ReadBarometer;
  ReadAnemometer;
  ReadHumiditySensor;

  AddReadingToDisplay;
  SaveReadingInDataFile;

  application.processmessages;

Until false;

But! Either we need a lot of variables, or we need other changes... the "AddReadingToDisplay" and "SaveReadingInDataFile" speak of READING, singular. Does only the Humidity reading get reported and saved? Not if you modify the plan as follows. iWhichTask and iWhichTaskMax are new variables. I hope you can infer what they are for from what you see below...

InitializeEverything;
iWhichTaskMax:=60;//Value in iWhichTaskMax doesn't change
iWhichTask:=0;

Repeat

  if iWhichTask=10 then ReadIndoorTture;
  if iWhichTask=20 then ReadOutdoorTture;
  if iWhichTask=30 then ReadFurnaceTture;
  if iWhichTask=40 then ReadBarometer;
  if iWhichTask=50 then ReadAnemometer;
  if iWhichTask=60 then ReadHumiditySensor;

  (Maybe)AddReadingToDisplay;
  (Maybe)SaveReadingInDataFile;

  iWhichTask:=iWhichTask+1;
  if iWhichTask>iWhichTaskMax then iWhichTask:=0;

  application.processmessages;

Until false;

Whoa! THAT got "complicated" fast!

No, not really. Just take a deep breath, look at that again after reading a little further, think about it. Not so "complicated" really.

The contents of iWhichTask will "tick" upward by one each time you go through the loop. (Until it gets too big, at which point, it will be set back to zero.)

Don't worry that many times, nothing will happen during a pass through the loop. There are no moving parts to wear out, and we have built in "room to grow".

Don't, for the moment, worry about how the reading gets from, say, "ReadHumidity" to "AddReadingToDisplay".

Don't worry about the fact that we don't do AddReadingToDisplay or SaveReadingInDataFile each time we pass through the loop. (We'll get to when DO we do them in a moment.)

Do worry about the rest though! What is going on? Why?

The "complexity" which we have introduced has two huge benefits:

1) Now, each time through the "Repeat" loop, we are only doing, at most, one of the "Read sensor" tasks... so the loop should execute more rapidly,

2) Now it is quite easy to insert other tasks in the "grand scheme", or change the order or frequency of their execution. We could, for instance, change the heart of the "fall-through loop" as follows...

  if iWhichTask=10 then ReadIndoorTture;
  if iWhichTask=20 then ReadOutdoorTture;
  if iWhichTask=25 then ReadIndoorTture;
  if iWhichTask=30 then ReadFurnaceTture;
  if iWhichTask=35 then ReadIndoorTture;
  if iWhichTask=40 then ReadBarometer;
  if iWhichTask=45 then ReadIndoorTture;
  if iWhichTask=50 then ReadAnemometer;
  if iWhichTask=55 then ReadIndoorTture;
  if iWhichTask=60 then ReadHumiditySensor;

.. to make the program do ReadIndoorTture more frequently than it reads the other sensors.

(If you weren't quite clear about (almost) everything a moment ago, when I said "Take a deep breath", go back and GET clear on most things. In a moment I will talk about some variables we are going to use, and how the "maybe"s are implemented. Try to get clear on other aspects of all of this.

First... two little asides....

1) Yes, I know that "iWhichTask:=iWhichTask+1;" can be accomplished with "inc(iWhichTask);"

2) Likewise, I know that there are more elegant ways to use variables to pass things in and out of subroutines. More elegant than the clumsy way I am going to do it in a moment. Have a little pity on the newbies?



Data passing, including "flags"

I hope that you will agree that the structure we have given out program is elegant and clear. There aren't many "dark corners" for bugs to hide in, are there?

The trouble is, it may not yet be quite clear to you how our program can do what we want it to do.

What's missing so far are some variables. They will come in two broad classes. Some will hold data, like the reading from a sensor. Others will hold "flags"... numbers which aren't meaningful in the usual way, but will, rather, hold "codes" to carry messages from place to place.

Beware when you are using variables to carry messages. It is very easy to forget that a given variable is being used for a given job, and "upset" the value stored in the variable before you are done using the number you are changing. Good naming of variables helps you to avoid this problem. As does clear program structure, and good in-line documentation, i.e. "comments" or "rems" in the sourcecode.

We're going to introduce the following variables...

How will those help us? Before I discuss that, let's talk a little about the different sensors in connected to the computer which runs my weather monitoring system. (Most, by the way, are from the Dallas "1-Wire" family... but that doesn't matter for the purposes of our discussion.)

When I write the code for, say, ReadIndoorTture, I will need to direct the computer to access a particular temperature sensor. The code for, say, ReadOutdoorTture will probably access a routine shared with ReadIndoorTture, but the routine will merely be "pointed" at a different sensor. The indoor tture sensor and outdoor tture sensor will probably be the same sort of electronics, merely at different addresses.

Hopefully, most of the time, the code for either will lead to a number. The sensors I use convert temperatures to a 16 bit unsigned integer, so that "answer" from the chip could be put in the variable I've named iReading.

Some of the time, however, when the code for ReadIndoorTture is executed, something will go wrong. Suppose, for example, the wire to the temperature sensor has been broken.

In such cases, we have three variables we can use, as the creators of the ReadIndoorTture code, to send messages back to the "higher" levels of the program. Those variables are bError0, bError1, and sErrorMessage.

Let's go back to our main code, and modify it just to show what would be needed assuming we have "fixed" ReadIndoorTture to...

Put a number proportional to the tture into iReading if the attempt to read the sensor succeeds, or to put a number other than 0 into bError0 if the attempt fails. (In this instance, we are simply not using bError1, sErrorMessage and rReading. They are simply available for times when we need more channels for passing error messages, or have a reading which can't be stored in an "integer type" variable.)

... and we have fixed AddReadingToDisplay to take the value in bReading and display it nicely on the screen, and we have fixed SaveReadingInDataFile to save that number to the program's growing data file. (You should have some "But...?"s in your mind... bear with me for the moment.)

Here's a better version of what we've done so far. The rems (comments) are VITAL to this project staying sensible. All new or changed lines have asterisks at their right hand ends.

Not shown: Most of the code I will have to write to actually do ReadIndoorTture, and none of the code for ReadOutdoorTture... AddReadingToDisplay, or SaveReadingInDataFile;

InitializeEverything;
iWhichTaskMax:=60;//Value in iWhichTaskMax doesn't change
iWhichTask:=0;

Repeat

  bError0:=255//255 being a code which MUST be changed *
     //to something else if ANY of the "if" statements *
     //below are executed... even if in, say, trying *
     //to read the indoor temperature the code fails, *
     //or succeeds.

  if iWhichTask=10 then ReadIndoorTture;
  if iWhichTask=20 then ReadOutdoorTture;
  if iWhichTask=30 then ReadFurnaceTture;
  if iWhichTask=40 then ReadBarometer;
  if iWhichTask=50 then ReadAnemometer;
  if iWhichTask=60 then ReadHumiditySensor;

  if bError0<>255 then begin // *
     AddReadingToDisplay;
     SaveReadingInDataFile;
     end; // *

  iWhichTask:=iWhichTask+1;
  if iWhichTask>iWhichTaskMax then iWhichTask:=0;

  application.processmessages;

Until false;

//==========                                          *
//Start of what will be needed for ReadIndoorTture... *
//In ReadIndoorTture...                               *
//  bKindOfSensor will be set to 0
//  bChannel will be set to 0                         *
//  If the sensor is successfully read...             *
//    The reading will be in bReading                *
//    bError0 will be set to 0... otherwise...        *
//  If the sensor cannot be read                      *
//    bError0 will hold something other than 0 or 255 *
//    bReading can be used to pass messages about     *
//         what sort of error was encountered, as can *
//         bError1, sErrorMessage and rReading        *


Once ReadIndoorTture has been written, and is doing to the sundry variables what it should. a start can be made on AddReadingToDisplay and SaveReadingInDataFile can now be written.

First they will look at what's in bError0. If it holds zero, then the attempt to read the sensor was successful. Otherwise it wasn't. We'll set up two blocks in both AddReadingToDisplay and SaveReadingInDataFile.. one to handle displaying (or saving) a "good" reading, the other to handle giving the user (and datafile) indications that the attempt to read the sensor failed.

In each of those blocks, the code will look at bKindOfSensor. For the sensor "behind" ReadIndoorTture, we know that the tture will be passed to AddReadingToDisplay and to SaveReadingInDataFile in the variable iReading. For another type of sensor the reading might have to be passed in rReading.

It is likely that we may want to be able to connect several instances of the same sort of sensor, e.g. temperature sensor, to the system, but have readings from them reported separately. Hence the bChannel variable. If we'd chosen to set bChannel to 0 in ReadIndoorTture, then we might choose to set it to, say, 1, in ReadOutdoorTture... but, if we're using two instances of the same kind of sensor, then both will be able to send "the answer" back in iReading. AddReadingToDisplay will be written to look in iReading for a reading from that kind of sensor, but to display the readings of indoor temperatures and outdoor temperatures in different ways. The number in bChannel "tells" AddReadingToDisplay (and SaveReadingInDataFile) "where" the reading came from.

So... to reiterate the above, in more general terms:

At the start of the fall-through loop, bError0 is set to 255. If we execute ANY of the "if iWhichTask=... then..." then clauses, the first thing we should do in the relevant subroutine is to set bError0 to zero.

In the rest of the subroutine, bError0 should be left undisturbed, unless an error of some kind arises, e.g. could not read sensor. If errors arise, any numbers from 1 to 254 (inclusive) can be used to indicate what sort of error arose. We do not, at this point, try to "handle" the errors. We have "made a note" of the presence (and type) of the error, and will deal with it later. If there is additional information about the error, that additional information can be put in bError1, to augment the information conveyed by the error code in bError0.

When execution reaches "if bError0<>255 then begin.." there are three possible conditions...

bError0=255. This should only happen if, passing through the body of the loop this time, none of the "if..." statements were true. In which case we just skip over AddReadingToDisplay and SaveReadingInDataFile, as no Reading has even been attempted.

bError0 does not equal 255. This means that at least an attempt to take a reading was made. If the attempt was successful, bError0 will hold zero, and we proceed with AddReadingToDisplay and SaveReadingInDataFile "normally".

If bError0 does not equal 255, AND it does not equal zero, it means that an attempt to take a reading was made, but that it failed. So we will still probably want to put some kind of indication on the display, and in the datafile (or perhaps a separate log of errors encountered). The handling of the error can be inside AddReadingToDisplay and SaveReadingInDataFile.

That's about it...

That, folks, is just about it! Of course all of the subroutines would have to be written....

I'd like to stress that you really do want to, "need" to, write up at the start of each subroutine's code what variables it changes, and what values it may put in them. (I showed you what the relevant section of ReadIndoorTture would look like.) You may also want to make a note of expectations. For instance, AddReadingToDisplay expects to have meaningful values in bKindOfSensor, bChannel, and one (or maybe both, depending on the sensor type) of the result variables. And if bError0 does not equal zero, while bKindOfSensor must be used as it is when bError0 equals zero, what is in the other variables may have different meanings. It will all depend on how you wrote the "read sensor" subroutine. You must be careful to keep what is done to the "messenger" variables, the variables used to pass data between the subroutines under control. Rems (comment lines) are a big help when you are using these techniques.

Neither AddReadingToDisplay nor SaveReadingInDataFile would be trivial subroutines, but both are manageable, if you keep your code tidy and clear.

Perhaps I should digress for a moment, talk about the structure which will be needed in AddReadingToDisplay?

That's going to involve two main blocks. One will deal with adding "good" readings to the display, the other block will deal with errors as they arise. Within each, the code will first look at bKindOfSensor, as that will a) tell the next bit of code WHERE to find the "answer" from the sensor we've been dealing with (bReading or rReading) and b) determine how to interpret that number. The code in AddReadingToDisplay and SaveReadingInDataFile will also be considering the value in bChannel. At the very simplest level, you probably want to use different colors for the graphs of the indoor ttures and the outdoor ttures.

Actually Doing It...

Hmm. All of the above sounds so good, doesn't it? Sadly, it doesn't (quite) work... at least not exactly as shown. But it should! And here's something that DOES work, in Delphi and Windows, XP, anyway. It may not be the best way to do it, but it works.

The problem which the following overcomes is that the application.processmessages inserted inside the big "repeat/until" loop is not enough to give you a way to "kill" the program. So here's something which will work. I'd be interested in advice from anyone who knows how to do all of these bits BETTER. (At the same time, I'd like to know how to cause a "spash screen" to appear when the program is started. In the big project I am building up from these humble beginnings, the "init" processes can take quite some time, and I don't want the user wondering what is going on.) Also, with some of things I tried, although the program seemed to be running okay, the form was not being displayed!

Keep the application.processmessages... I think it has a role, even if it didn't do all I had hoped it would.

Create a global boolean variable called boDone.

Change the "until" condition to "until boDone=true".

Put the material which comes before the "repeat" into the main form's OnFormCreate handler, and include somewhere in that "boDone:=false;"

Put the "Repeat/Until" loop in the form's OnActivate handler.

Make the form's OnCloseQuery handler "boDone:=true;".

(You can add a "Quit" button to the form, too, if you wish. Make it's OnClick handler "boDone:=true;".)

I thought that would do it, and it almost does. It "works" in Delphi, anyway, up to a point. Sadly, when running thus, the program ignores, say, most button clicks. (I added a button to the form which should have drawn a line on an image. It didn't.) The program runs. The form is displayed. You can quit the program by the normal channels (click red "x" at upper right, or do Alt-F4, or click "Quit" button, if you provided one.)

I haven't tried the code yet, assigned to handlers as above, in Lazarus. I would be interested to hear what luck anyone has. It "should" work....

Thank you for reading!

... especially considering the major flaw: my failure, so far, to integrate this idea into a Windows program for you. The ideas in this are valid and valuable. I just need to write up a working Delphi program, to clarify for you how the "repeat/until" can be accomplished by using the event that goes with a timer. That approach DOES work, by the way!

This is the first in what I anticipate eventually being several essay on Structure, Planning and Testing.... a "different sort" of essay from what I usually do. I hope it was useful to you! Comments welcome, there's "how to contact me" just a little farther down the page.

You've done the "hard work" if you've mastered the above. I've written a "postscript" with more on using fall-though loops. It extends what is above; it introduces a useful frill. Useful for making a basically asynchronus program have bits which are tied to "real" (hours/minutes/seconds) time, as needed. A frill with other uses too.

The postscript isn't nearly as much work as you have done in reading this page!





Search across all my sites with the Google search...

Custom Search

Or search just SheepdogGuides.com with the Freefind tool...

   Search this site or the web          powered by FreeFind
  Site search Web search
Site Map    What's New    Search

This search merely looks for the words you enter. It won't answer "Where can I download InpOut32?"


If you visit 1&1's site from here, it helps me. They host my website, and I wouldn't put this link up for them if I wasn't happy with their service. They offer things for the beginner and the corporation.www.1and1.com icon

Ad from page's editor: Yes.. I do enjoy compiling these things for you. I hope they are helpful. However... this doesn't pay my bills!!! Sheepdog Software (tm) is supposed to help do that, so if you found this stuff useful, (and you run a Windows or MS-DOS PC) please visit my freeware and shareware page, download something, and circulate it for me? Links on your page to this page would also be appreciated!
Click here to visit editor's freeware, shareware page.

Link to Lazarus Tutorials main page
How to contact the editor of this page, Tom Boyd


Please consider contributing to the author of this site... and if you don't want to do that, at least check out his introduction to the new micro-donations system Flattr.htm....


Valid HTML 4.01 Transitional Page tested for compliance with INDUSTRY (not MS-only) standards, using the free, publicly accessible validator at validator.w3.org. Mostly passes. There were two "unknown attributes" in Google+ button code. Sigh.


If this page causes a script to run, why? Because of things like Google panels, and the code for the search button. Why do I mention scripts? Be sure you know all you need to about spyware.

....... P a g e . . . E n d s .....