First, I try to limit uses of next, last, and redo because they tend to be hidden in the middle of a loop (making the loop control conditions less than obvious) and they transfer control to places that can be less than obvious (it can take some real visual searching to figure out which surround block(s) are loops, often having to check both top and bottom of each successive surrounding block).
I prefer to only use them in quite simple loops (and to find them only used in such). So, for example, I make the complex loop control more obvious by simplifying the loop by moving most of the non-control complexity into subroutines. It is often even better to replace last with the use of early return from a subroutine. Since last is by far the most-used of these constructs, early return is often enough to completely eliminate them from your code.
Second, and more importantly, I don't consider the use of a bare block as a "loop" to be a wise choice. This is what is very much like using goto, even worse in some respects.
A bare block is quite unlike a loop. The only thing that makes it even slightly like a loop is that these loop control keywords (next, last, and redo) do something in them. But they don't really work like loop control in these cases, they work exactly like goto. Let us only consider last and redo in bare blocks since, in a bare block, next does the same thing as last and would be a more confusing choice.
A bare block w/o last nor redo doesn't loop at all (and is a fine construct if used for reducing the scope of some lexical variables or pragmas). A bare block with (only) a redo is exactly like a goto to the opening brace. The closing brace is completely pointless to the so-called "loop". So compare:
{ # body redo if ...; # tail } # post # vs TOP: # body goto TOP if ...; # tail # post
The latter is actually a lot clearer. It is easier to see that the top of the "loop" might have control transfered to it. It is easier to determine where the goto is sending control than to where the redo is going. There is no obfuscatory splitting of the "tail" and "post" parts of the code. Note that if there is no "tail" portion, then the following improvement is possible:
{ # body redo if ...; } # post # becomes: do { # body } until( ! ... ); # post
A bare block with (only) a last is exactly like a goto to the closing brace. The opening brace is completely pointless to the so-called "loop". So compare:
# bare block "loop": { # head last if ...; # body } # post # vs goto: # top? # head goto END if ...; # body END: # post # vs if: # head if( ! ... ) { # body } # post
The clearest choice there seems obvious to me.
Now consider a bare block with both redo and last. It is exactly like two labels with gotos except that it is often less clear. Is isn't much like a loop:
{ # top last if ...; # middle redo if ...; # tail } # post TOP: # top goto END if ...; # middle goto TOP if ...; # tail END: # post
The former can easily become quite obfuscated. The latter is at least clear in the flow control (the labels being obvious to find and to match up). The only advantage I see to the former is that it doesn't require one to come up with unique label names, but if that becomes a problem then I suspect the code has become quite difficult to follow indeed.
If I find myself creating code that flows like that, then I don't use a bare block as a "loop" and kid myself that my code is clear. I instead realize that I'm using goto-like flow and I need to rethink a bit and probably refactor. The solution is usually to create a small subroutine (or two) that can be used in a conditional for a "real", obvious loop and/or a small subroutine that does an early return. Sometimes not even that is required.
A big problem with goto, IMHO, is that it is too easy to use it to achieve your first idea of how to do flow control, not encouraging you to come up with a simpler or clearer expression of the required flow. In (tye)Re: Closures as Itterators I show an easy solution to something that several people were solving in complex ways using bare-block "loops". By sticking to "real", obvious loops, I forced myself to rethink how the code is flowing and produced clearer code.
In (tye)Re: crafting a decent CLI, I talk more about this stuff as well.
In the original use of bare-block plus redo, the sample code is so small that the potential for lack of clarity is not very well demonstrated. But it is easy to replace the code with something that doesn't contain such an easy-to-abuse and easy-to-become-hard-to-understand construct:
LOOP: { try something; if (!success) { wait; redo; } } # becomes: while( 1 ) { try something; last if failed; sleep $seconds; }
But even better is to factor out what is being tried so your code becomes even clearer:
while( ! try_something() ) { sleep $seconds; }
- tye
In reply to Re^3: Re Execute Lines of Code ("loops")
by tye
in thread Re Execute Lines of Code
by btobin0
| For: | Use: | ||
| & | & | ||
| < | < | ||
| > | > | ||
| [ | [ | ||
| ] | ] |