Loop Exits and Structured Programming: Reopening the Debate (1995) [pdf]
www-cs.stanford.edu
www-cs.stanford.edu
It's clearly very easy to write bad code in many ways, but (very) judicious use of goto is occasionally far clearer than assuming goto is always bad and building something complex out of bools, break and continue. It seems to be a test of common sense: if goto is clearer than the alternative, use goto.
Of course, thinking about labelled break is just dreaming when required/choosing to work in a language without it.
In any case, I strongly disagree that labelled breaks aren't better than regular breaks: the chains of booleans required otherwise are very annoying and error prone.
IMO there is no good substitute for gotos when writing transpilers (and certain interpreters) and low-level concurrency code. The Common Lisp prog macro is the best way that I have seen to use gotos (https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node91.html).
while ... {
bool shouldBreakOuter = false;
while ... { ... shouldBreakOuter = true; break; }
if (shouldBreakOuter) break;
}
since that what the original comments description sounded like. Being a rather general control flow mechanism, there are of course other uses of goto which this doesn't cover.That said, I'm not sure I understand the comparison: the lexical scoping doesn't seem to be ambiguous in Java. Could you expand on what you mean?
I wouldn't mind it if the language expressly made me call such a function from a library of other 'unsafe' operations. Optional things that you can include when you really know what you're doing and use them in a limited way.
It's one of those tools that is powerful, dangerous, and best avoided when another will suffice. Yet like explosives, sometimes dangerous is the best tool for a job.
In the case of a programming language goto can be used to emulate features and/or paradigms that a language doesn't have or doesn't quite do sufficiently. It should ALWAYS have a comment that annotates the intent.
Exceptions, on the other hand, or non-local goto, has a good chance of seriously scrozzling compilers and causing a lot of optimizations to go away.
All that being said, the potential to break human understanding of the code with gotos is serious.
I think programming should really be taught starting with Asm, and thus, a language where gotos are the norm. Flowcharting should be considered an essential skill for designing and describing algorithms. Control structures like if/else, do/while, while, for, etc. should be taught as abstractions that are to be used where it would simplify the code (much like all abstractions should be used for), but if your algorithm doesn't fit these structures 100%, it's better to use gotos than to try to "force" it to fit by creating a "spaghetti of booleans".
State machines are another use-case where goto makes perfect sense: instead of storing state in additional variables, the current position in the code is enough. It makes debugging much easier when you are actually jumping between the states as you step through the code, rather than looping back up into a switch or a series of if/else checks on another variable.
And, sometimes, even in small programs, knowing where you are after some jumps is a messy thing.
I just don't have all that rant against jumps (or goto's). I know where they live, what they eat, when they are useful and when are not. :)
void foo()
{
do {
for (...) {
if (...) {
break;
}
}
if (...) {
...
if (...) {
...
break;
}
}
/* even more nested loops and conditionals with break sprinkled all over */
} while (0);
...
}
PS: IMHO this code goes too far with the "goto considered harmful" philosophy. The "while (0)" is serving the purpose of a label. An explicit label instead would be more readable, IMHO.Dogma has no place in coding. Best practices give you a sense of smell. They don't tell you how to factor.
It can be cleaner to cache the conditions in a few flags if you want to do it that way, though. I'm not saying deeply nested conditionals are pretty, just that there are some cases where they're quicker. (Of course, if you're scraping the bottom of the barrel for that kind of speedup then chances are, there are algorithmic improvements that will yield orders of magnitude more gains.)
Even more of an alternative (and would only be a good idea in certain circumstances) is, in a language with destructors, to make an object created at the top with a destructor for that last block of code, so you can throw out the loop and use return instead of break. This would also work with Golang defer statements. But again, only a good idea if that makes some sort of sense in your program, and isn't just a work around for the lack of gotos.
Gotos make me cringe, a "while(0)" hack makes me cringe just as much. The whole problem with goto is it confuses the reader as to where the control flow is going, but this is just as bad because until you read to the end you're under the impression those statements will be run multiple times.
The problem with goto is that it is opaque. It has no structure, it has no constraints, and it communicates no intent. It says "execute the code behind this label," and gives no clue what the code is or what it does.
But if you give goto a different name that reflects how you're using it, that solves the problem! If your goto is named "break" and it occurs inside a loop, that tells you what the goto is trying to achieve. It tells you at a glance what code you expect to find when you jump, and its relationship to the other code in the program. In short, it communicates intent.
The same is true when the name is "continue" or "throw": you know where in the program that goto leads and what sort of code you expect to find when you go there. You don't need to read it to find out. And if you do read it, you have a better idea of whether it's right or not.
(Also note that criticism of goto applies a lot more to historical gotos, whose argument was an arbitrary memory address. Modern gotos aren't nearly as bad because they generally take a label rather than a number, and because their jump targets are often restricted to the immediate lexical environment.)
> Modern gotos aren't nearly as bad because they generally take a label rather than a number, and because their jump targets are often restricted to the immediate lexical environment.
That doesn't actually apply for your examples of "continue" or "throw" though. "continue" has no label, numeric or symbolic; its target is implicit from the lexical scope. "throw" has a sort of label (the type/class of the given value/exception), but its target is determined by the dynamic environment. Given a function like:
function foo() {
throw Bar;
}
There is nothing in the lexical environment (i.e. the blocks within which it is written) that tells us where this will jump to. That information depends on what the stack looks like at the call site, which may be written by someone else, on the other side of the world, 20 years from now.Unless it was a tail call...
Really, throw and return are quite similar, both invoke an externally provided continuation and abandon the current one.
In a pure CPS setting, the caller's "only" responsibility is to give the right arguments (in scare quotes because getting a continuation right is usually trickier than the "plain" values usually given to functions).
In languages with functions, the caller is also responsible for handling/ignoring any return value and carrying out any subsequent computations, if the procedure returns.
In languages with exceptions, the caller is also responsible for handling/passing-on any exceptions.
Out of these three sorts of language, my current preference is for the second. It's probably fair to say that most procedures in a codebase will return a value (i.e. they're not continuations) and will not throw an exception (they're exceptional, after all). Hence it seems empirically desirable to allow `return`: it alters the semantics from a pure CPS scenario to one where we must always be vigilant that a procedure may return; but since most procedures (in such languages) return, that's not an edge case (in fact it's continuations, procedures without `return`, that are rare!); hence the benefits seem worth the cost.
When `throw` is introduced, we again must be vigilant when we're calling procedures. However, since most procedures don't `throw`, the benefit of having this ability is less than that of `return`. Also, since there may be many occurrences of `throw`, with different exception types, the cost of looking out for and handling these is greater than for `return`.
Hence my (current) opinion that `return` is very much a "good GOTO", whilst `throw` is a "bad GOTO" :)
Then we get `call-cc`... ;)
'call-cc', the control operator from hell that puts 'spaghetti' goto to shame.
2. Is the code relatively readable by someone unfamiliar with the problem?
If 1 && 2 then stop tearing your robes like Pharisees and go solve another problem.
Do Forever
If A Then Leave
If B Then Call X
If C Then Iterate
Call Y
Leave
End
Ah, that style of loop
writing may cause some
purists to scream bloody
murder!Some people seem determined to cause facial tics in everyone they meet.
Maybe so, but not me.
So, what is the justification for the Do Forever loop above?
First, I illustrate that we can want to exit the loop for (A) whatever conditions are discovered in the loop and (B) at any point in the loop and not just the beginning or end which is the usual situation, e.g., for
Do i = 1, 10
or Do While X
If I write Do I = 1, n
does the loop logic deciding when to exit ever form n + 1? If n is the largest integer, then maybe we'd like to know. With Do Forever we may have more control, enough more to avoid forming n + 1 unless we really want to.Second, when I start writing a loop, I may not have yet worked out all the details or how to get what I do want just from some
Do While X
Etc.
So, I just start the loop with Do Forever and work out the exit conditions as I keep typing.Third, with Do Forever can often get the results of using a GOTO without actually doing so. So, if the target of the GOTO would be just after the loop End, then can get there within the loop with just a Leave.
Of course, when write such a loop with Do Forever, should write some comments that explain (A) what the purpose of the loop is and (B) how the code in the loop does accomplish that purpose. That is, should not force someone reading the code to guess at (A) and confirm (B).
For proofs of correctness based on the usual mathematical induction, I'm not convinced that loops with Do Forever are fundamentally more difficult.
> Some people seem determined to cause facial tics in everyone they meet.
Maybe so, but not me.
Best practices usually apply in general, but the smart person who coined them cannot know all possible situations and the parrots who blindly repeat them don't know what they're talking about. Think for yourself, know your tools, and use them effectively to make your code as simple as possible.
I desperately want a 'break break' command. Not a label break, which is painful to work with, but an instruction to break the current loop and the outer loop.
Rust really likes to bail out of functions when something goes wrong. That's what "try!()" does.
Allowing multiple exits from a loop is no longer controversial. Multiple entries into a loop are still considered undesirable.
From a proof of correctness standpoint, the topology requirement is that there be some single place within the loop through which control must pass. That's where the proof inductive step takes place, and where you prove loop termination by showing that some measure is decreasing. Restrictions stricter than that are more stylistic than formal.
Once I got a phone interview from Google. An early question was,
"What is your favorite programming language?"
Sure, right away, PL/I! Opps, (from a movie) "way wrong answer!". Apparently the only right answer was C++. Gee, I didn't want to lie. Besides, to me saying C++ instead of PL/I should cause me to lose a full letter grade!
So, PL/I has descendancy, static and dynamic. The static version is from the nesting in the static source code. The dyanmic version is from functions, subroutines, etc. that have been called (are active) but have yet to return.
Then with such descendancy and entry variables, can get some interesting situations, design patterns!
I did that once and avoided a total mess in the IBM AI language KnowledgeTool!
if (a)
return -1;
if (b)
return -1;
return -1;
if (c)
return -1;
return 0;
And literally the exact same issue would occur, even though goto wasn't used.Thus you can implement 'if' as a user defined function in eg Haskell.
(But I'd advise against the totally if-less style. Pattern matching is just too convenient. (And ifs are just a special case of pattern matching on booleans.))
Pure lambda calculus doesn't I have ifs. If you want to go truly crazy, check out SKI calculus. It doesn't even have lambda abstraction.
If I may guess: you want to make gotos central? That only holds for compiling to von-Neumann machines. Some other computational structures might not even have instruction pointers to move around.