This page exists to summarize http://www.cis.temple.edu/~ingargio/cis71/software/roberts/documents/loopexit.txt. As everyone knows, GoTo is the bane of StructuredProgramming, and this fact was first laid down in a paper named GotoConsideredHarmful. Since then the entire topic of what constructs are and are not properly usable has been a subject of ReligiousDebate. Some people say that you should have one entrance and one exit to every loop. Some say that you should have one entrance but many exits are OK. Some say that you should AvoidExceptionsWheneverPossible. Others claim that you should UseExceptionsInsteadOfErrorValues. (I distrust people who tell me what I ''should'' do, but let us leave that.) How can rational people come to a reasonable opinion on topics like this? A good start is to read the original paper at http://www.acm.org/classics/oct95/ and understand it and then compare your understanding to other people's. My understanding is that Dijkstra's point of view starts with ProgramsRepresentMentalModels. Which immediately implies that you want a good match between how people can understand what is really going on, and how it is represented. Therefore the question of what programming practices are acceptable is fundamentally a question about human psychology and cognition - what do people find natural? He then argues that constructs that people wind up writing with GoTo do not map well onto how we think. By contrast what we write with a variety of structured constructs tends to be comprehensible to us, therefore by using those we make it easier to verify that the program represents the mental model we want and make it easier to figure out what mental model it represents. Let us apply this principle to the subject of internal loop exits. The paper I referred to above asks the reasonable question, ''Are we better off with or without internal loop exits?'' It does not try to argue either way based on first principles - indeed arguing on abstract principles misses the point that this is a question about human cognition. Rather it answers it by asking three questions: 1. Do people find it easier to produce correct solutions to common problems with or without internal loop exits? ''Yes! Even if it means using a feature they have no experience with!'' 1. In which case do others find those solutions easier to verify? ''The one with internal loop exits.'' 1. When they have it available, do students tend to abuse the feature in practice? ''It appears not.'' The specific type of common problem they researched is TheLoopAndaHalfProblem, which is the case where you have a loop in which some processing must precede the test. For instance you may have a sequential where you need to read data before you can proceed in test. Here is pseudo-code for two different natural strategies: loop read in a value; if value = Sentinel then exit; process the value endloop; read in a value; while value != Sentinel do begin process the value; read in a value end; The former, of course requires an internal loop exit. The latter requires repetition of code. Many dislike internal loop exits. Any decent programmer dislikes repeating code. Which is the lesser of 2 evils? Their answers were as I indicated above. Internal loop exits win in practice for real people. Lines of evidence for their conclusion include their teaching experience, literature on controlled tests of student performance with and without the feature, length of correct code in Pascal with and without an addition allowing this strategy, and the teaching strategies that textbook authors have had to resort to. Feel free to read the paper for details. The evidence presented was more than enough to convince me that InternalLoopExitsAreOk. -- BenTilly ---- Remember that the above loop could be written as: while ((read in a value) != Sentinel) { process the value; } Or if you don't like that you could try an ExternalIterator or InternalIterator that skips values equal to Sentinel. Really any time you start to think the ''your'' case is exceptional, you're probably missing an object. -- DavidSalamon I would write that using the exit, because I am strongly from Python background (and because it would just look '''weird'' with indentations, we don'''''''t get do...while loops). However, if I *HAD* to not use them: cond = 0; until (cond) { value = ReadInAValue(); if (value != Sentinel) process(value); else cond = 1;} ...or something like that. Anyway, it would be VERY EVIL, and I would shudder at it. ''If you think that internal loop exits should not be used, then please explain why they shouldn't be. Don't just say that you can do it another way. Yes, I know several other ways. Turing guarantees that you can always do it another way. Explain why it is BETTER to do it another way. "Right" and "wrong" here are not axioms, they are based on what people have found works and what doesn't.'' ''Alternately tell me why you think that my reasons for saying that this is a good way to do it are mistaken.'' ''As for saying that all problems should be solved with another object, I have seen this philosophy lead to disasters. Yes, adding an abstraction layer can solve any problem (other than the one of having too many abstraction layers). And adding them is fun. But there is a cost to introducing yet another object, yet another class, and yet another layer. Remember that YouArentGonnaNeedIt. If there is a natural direct solution, take that first to avoid PrematureGeneralization.'' ''-- BenTilly'' ''Are you suggesting that using an explicit loop-and-test (internal exit or not) is intrinsically more natural and direct that a solution using an internal or external iterator? If so, why? It seems as if this "religious debate" is intimately tied up with only being able to use 70's era imperative, procedural programming constructs. Personally, I find writing loops of any kind irksome, I find it much more natural to think (and therefore program) using internal iterators and other structures.'' It is my opinion that '''while(true)''' is extremely unexpressive. The standard format is to have the conditional logic at the start of the loop, and just as I wouldn't put a window's close button on the middle left side of the window, I'm not about to put the important logic in a hard to find place. Also note that in the case of InternalIterator''''''s you don't introduce another object, just another method. (Standard libraries will usually have this implemented though, as in SmalltalkLanguage and RubyLanguage) -- DavidSalamon ---- ''It seems to me that the only problem with the second loop is that it requires a priming read. I'm not sure that really qualifies as "duplicate code" but I suppose it could. I prefer the second option because it loops no matter what. The first option forces me to think about whether or not the end condition is met.'' If the read takes several lines, it does qualify as duplicate code. You can reduce the duplication with a function call. As for preferences, read the paper. The difference in the ability of students to come up with a working answer when they are and are not allowed to use internal loop exits is extreme. (100% with versus under 20% without, sample size a few dozen people in each group.) Given my druthers I prefer techniques that real people reliably find correct answers with. -- BenTilly Note that the techniques that are easy for beginners to learn are not always the same techniques that experienced people find easy to use. Also, code written by beginners is not necessarily the most maintainable code. I agree that we should prefer techniques that "real people" can use reliably, but I think the "real people" we should worry about are professional programmers, not students. (FWIW, I use internal loop exits, and don't think there is anything wrong with them. I just don't think that this experiment with students is very enlightening.) -- KrisJohnson -------- Hey, this is a LanguageSmell. In a functional language, where loops are TailRecursion and if expressions return values, the solution is clean and free of dogma. E.g. in SchemeLanguage: (let loop ((var init)) (if (equal? var sentinel) 'done (loop (next-value)))) ''The hardest dogma to see is always our own. Translate loops with recursion or iterators and you have transformed the problem, not removed it. (Note that TailRecursion only is special in that in some languages it is automatically optimized.) If you tried to complete your code you would realize that within next-value you run into an end of data condition. The question then raises itself as AreExceptionsOK? Incidentally, I think that exceptions are less OK than internal loops because even though the control implications are roughly equivalent, there is more ActionAtaDistance involved. -- BenTilly'' ''Within next-value you run into an end of data condition.'' Why so? The following seems to work pretty well (in MzScheme), without an exception in sight. (define (next-value) (random 10)) (define sentinel 5) (define (first-value) (random sentinel)) (let loop ((var (first-value))) (display var) (if (equal? var sentinel) 'done (loop (next-value)))) A few sample runs: 174828666271378210776122240036765 done 16715 done ''Hah, hah. Yes, that iterator will always have a next value. Now try it scanning a fixed array or file. -- BenTilly'' : I think the point of the sentinel is that it denotes the last value, like iterating over a 'C' string with the '\0' character being the sentinel. -- AlanDavies Ok, so I missed a case out (because I was trying to use if so more people would understand it). The correct code is: (let loop ((var init)) (cond ((equal? var sentinel) 'done) ((equal? var last-value) 'done) (else (loop (next-value))))) Notice the original imperative pseudo-code doesn't check for loop termination either. ''I did notice. Alternately you can say that an exit is embedded in the reading section. Either way the fact that you have 2 exit conditions is part of why it is natural in an iterative solution to have one of them be an internal loop exit. An ugly feature of this kind of control problem is that end of data is returned as data, which either means that you have a language special value to return, or else you introduce a SemiPredicateProblem. -- BenTilly'' ---- What is important is writing clear code. In all the examples given above, the code is clear both with and without internal exits, due to its brevity. The prohibition against internal exits comes from the good old days when people routinely wrote 50-line functions with several levels of nesting. Having a internal exit buried deep inside such a function is harmful. With more modern stylistic rules (shorter functions, iterators, exceptions, functional programming style, etc.), prohibitions against goto and other "non-structured" constructs are not as necessary. BadProgrammerConsideredHarmful is the modern principle. The important thing to remember about rules like "Never use GOTO", "Never use internal exits", etc., is that TheyreJustRules. You don't have to follow them blindly. If you don't like them, then ignore them or amend them to suit you. ''Agreed. Except that I find it dangerous to break rules without a good understanding of why the rule became a rule in the first place. -- BenTilly'' ---- I would like to argue in favor of GotoConsideredHarmful. This is the same search from the paper in C using multiple returns: int search( int [] list, int n, int key ) { for ( int i = 0; i < n; i++ ) if ( list[i] == key ) return i + 1; return 0; } And the same search from the paper in C without using multiple returns: int search( int [] list, int n, int key ) { bool found = FALSE; for ( int i = 0; i < n && !found; i++ ) if ( list[i] == key ) found = TRUE; return found ? i + 1: 0; } It has a possible bug, since i can be incremented or not when it goes out the loop... The following is possibly better: int search( int [] list, int n, int key ) { int ret = 0; for ( int i = 0; i < n && ret != 0; i++ ) if ( list[i] == key ) ret = i; return ret == 0 ? 0 : ret + 1; } ''Further evidence that InternalLoopExitsAreOk - the above version also has a bug: if list[0] == key, it will skip it and keep looking. Maybe this is better:'' int search( int [] list, int n, int key ) { int ret = 0; for ( int i = 0; i < n && ret != 0; i++ ) if ( list[i] == key ) ret = i + 1; return ret; } The paper states that having multiple returns is a requisite for clearer and less buggy code. As you can see, the paper is biased against GotoConsideredHarmful. ''The first version was indeed clearer, and keeping track of an extra variable is a potentially bug-causing hassle (in more complicated code). IMO, the basic rule is: people think linearly, anything else involves mental contortion and thus causes bugs or at least slows you down.'' I don't understand why the paper uses this example to draw this conclusion. I think this is the clearer version int search( int [] list, int n, int key ) { int i=0; while(iptr) { free(h->ptr); h->ptr=NULL; } } The point here is that the pointers are always NULL'd to ensure that deallocation happens only once. So adherence to this discipline with all deallocation leads to a capability to return early and still do (a potentially incomplete sequence of) deallocations in another function without complex interleaved logic chains. This pattern actually makes it pretty easy to convert most any pointer into a singleton or a buffer ring, which is another useful move to have easily available for the future. I think the best reason I like it is because it simplifies buggy crashes (less complex behavior with less non-zero state) and lets you catch "already freed" type errors with simple assert(ptr != NULL) type things much of the time. If you combine this technique with a ReligiousAdherenceToCallocOverMalloc, then you get the capability to detect "not yet allocated" type errors with that same assert. Another pragmatic trick I use (last resort) when that fails is atexit() for this same problem. -- RudiCilibrasi ---- ---- ''[Discussion moved from RefactorMatchLoopToUsage]'' Use the right type of loop. For loops with Exit constructs are not for loops at all. In this example, the programmer does not want to execute the loop ''for'' all numbers between 1 and 10, but only ''while'' something is not found. The second loop communicates much better. from... For i = 1 to 10 If isFound(i) Then Exit For Next i to... ysnFound = False i = 0 While (not ysnFound) And (i < 10) i = i + 1 ysnFound = isFound(i) Wend (you could do this with ''Do .. Loop Until'' as well) ---- I think the first loop is easier to read and communicates more clearly. (In particular, the second loop looks at first glance as if it's looping from 0 (inclusive) to 10 (exclusive), but it isn't. This isn't the only reason why the second form is less clear to me.) It's a tradeoff, of course: the programmer, contrary to the assertion above, wants to do something ''for'' values in a particular range, ''until'' a condition is met. You can choose to prioritize one or the other; in this instance, making the ''for'' iteration the primary one results -- in my opinion -- in code that's clearer as well as shorter. Since what's being done in that particular loop is a common idiom, it would be even better if you could say something like i = findFirst(1..10, isFound) instead. This is not practical in VisualBasic, for a variety of reasons: so much the worse for VB. (There are a bunch of not-quite-trivial design issues associated with a function like the one I propose. What to return if no matching element is found? Return the element or its index? What about having multiple sequences and a single predicate applied to elements from all the sequences together somehow? Should there be a findLast too, or a way of making findFirst work backwards? And so on. I commend to the reader's attention the FIND-IF and POSITION-IF functions in CommonLisp, and their associates.) Oh, and while I'm causing gnashing of teeth among those to whom all mention of Lisp is anathema, let me mention that CommonLisp offers a looping construct in which you can avoid privileging either the ''for'' or the ''until'' aspect: (loop for i from 1 to 10 until (isFound i) finally (return i)) I think that's rather elegant. -- GarethMcCaughan In AlgolLanguage you could say '''do''' '''for''' i := 1 '''to''' 10 '''while''' '''not''' isFound(i) but this syntax was regarded as "overblown" at the time. Pity. ---- CategoryCodingConventions CategoryCodingIssues CategoryLoops