http://www.perlmonks.org?node_id=133899

Most people know perl's range operator in its list context:

@digits = (0..9);

but in scalar context, the range operator acts like a flip-flop. The manpage will give you the gory details of how it calls the terms on either side, and by the time I was done reading, I had no idea why anyone would ever want such a thing. I filed it away in the back of my mind, though, and later, while trying to unroll the mysteries of asynchronous socket code, had an epiphany.

Trying to loop over code that can fail is annoying. Error-handling code likes to branch, and loops prefer linear code. Reconciling the two tends to be work. It's much easier to to put the code that can fail into the loop condition, because then you can write the body of your loop for data that passes all the essential sanity/hygiene tests.

That idea leads to code like so:

while ($data = &func) { &munge ($data); }

but when you're dealing with systems that can fail on setup, you have to embed the loop in a conditional:

if (&setup) { while ($data = &advance) { &munge ($data); } &teardown; } else { ## life sucks }

which is still kinda clunky.

That's when the lightbulb went on.

The scalar range operator tests its left argument on the first and last passes through a loop, and its right argument on all the remaining passes. Therefore, I reasoned, one could put the setup and teardown code to the left, and the advancement code to the right. I tried it:

while (&setup_and_teardown .. &advance) { &munge (&get_data); }

and to my mild surprise, it worked.

It still didn't send me to nirvana, though. The structure above demands shared variables among the functions called in the loop, and I have this screaming distaste for linking functions together with globals. Fortunately, objects provide exactly the right kind of encapsulation for the job.

The results follow, and if nothing else, help make some sense of the scalar range operator.

#!/usr/bin/perl -w package Loop_control; ## define some constants used in the simulation: $LIMIT = 5; ## maximum number of iterations $START_ERR = 0.25; ## 25% chance of failing to start $LOOP_ERR = 0.1; ## 10% chance of failing per iteration $FATAL = 0.15; ## 15% chance of fatal errors ## new (nil) : Loop_control_ref # # yer basic constructor. bless the ref, call init(), and return # the result. # # general note regarding style: i've outdented all the # print() statements that generate tracking output. it's a # personal thing.. i find s/^print/# print/ easier than sifting # through the code trying to find that one blasted debugging # statement. # sub new { print "-- Loop_control::new\n"; my $self = bless {}, shift; return ($self->init); } ## init (nil) : Loop_control_ref # # sets up a couple utility variables, but doesn't do anything # earth-shattering. # sub init { print "---- Loop_control::init\n"; my $self = shift; $self->{'state'} = 'new'; ## condition register $self->{'msg'} = ''; ## this object's $! return ($self); } ## updown (nil) : boolean # # the range operator calls this routine on the first and last # passes through the loop. this routine delegates control to # setup() or teardown(), based on the contents of the state # register. # sub updown { print "-- Loop_control::updown\n"; my $self = shift; if ($self->{'state'} eq 'new') { ## have we been here yet? $self->{'state'} = 'running'; return ($self->setup); ## no.. set things up } else { return ($self->teardown); ## yes.. tear things down } } ## setup (nil) : boolean # # set up the data source. this could be any procedure that # might fail, like opening a file or a network connection. # this toy version just fails randomly so you can see the # overall system work. # # this routine returns TRUE if the setup succeeds, thus # making the range operator test TRUE the first time it's # polled. # sub setup { print "---- Loop_control::setup - "; my $self = shift; if (rand() > $START_ERR) { $self->{'count'} = 1; ## trivial setup print "TRUE\n"; return (1); } else { print "FALSE - FAIL - FAIL - FAIL -\n"; $self->{'state'} = (rand() < $FATAL) ? 'fatal' : 'error'; $self->{'msg'} = 'failed during setup'; return (0); } } ## teardown (nil) : boolean # # shut down the data source. this routine terminates the loop, # but shouldn't fail in any way that will ruin the data. # # this routine returns FALSE, thus making the range operator # test FALSE as well, thus ending the loop. # sub teardown { print "---- Loop_control::teardown - FALSE\n"; return (0); } ## advance (nil) : boolean # # this routine fetches the next chunk of data. it can fail # in ways that will ruin the transaction, so once again we # simulate failure by rolling dice. # # this routine returns FALSE on success, which seems wierd # until you recall that the range operator is asking, # "have we hit a stopping point yet?" # # $self->{'data'} is a read-only inspection variable. it # does the same thing an accessor method get_data() would, # but doesn't require a function call. it's an indulgence # i grant myself when i'm damsure i can get away with it. # no code anywhere in this package reads $self->{'data'}, # so even if a user does screw around with it, their change # will have no effect on the object's behavior. # sub advance { print "-- Loop_control::advance - "; my $self = shift; if (rand() < $LOOP_ERR) { ## short-circuit on error print "TRUE - FAIL - FAIL - FAIL -\n"; $self->{'state'} = (rand() < $FATAL) ? 'fatal' : 'error'; $self->{'msg'} = "failed during pass $self->{'count'}"; return (1); } $self->{'data'} = $self->{'count'}; if ($LIMIT > $self->{'count'}) { $self->{'count'}++; print "FALSE\n"; return (0); } else { $self->{'state'} = 'done'; $self->{'msg'} = 'normal termination'; print "TRUE\n"; return (1); } } package main; ## # # now for the simulation. we fill a list with numbers, then # iterate using that list as a queue. items that fail with # recoverable errors get pushed back on the queue for another # try, and items with fatal errors get dropped. # # the real point of this whole mess is to see the tracking # statements for each pass through the loop. you can see # the order in which functions are called, and the TRUE/FALSE # results that go back to the range operator each step of the # way. you'll see a TRUE (FALSE)+ TRUE FALSE sequence when # everything works, and the range operator maps that to the # sequence (TRUE)+ FALSE. # ## @list = (1..10); while (@list) { $i = shift @list; print "======== trying $i\n\n"; ## create a control object and run the loop @cache = (); $obj = new Loop_control; while ($obj->updown .. $obj->advance) { push @cache, $obj->{'data'} * $i; } ## then decide what to do with the results print "\n## $obj->{'msg'}: "; if ($obj->{'state'} eq 'done') { print join (', ', @cache), "\n\n"; } elsif ($obj->{'state'} eq 'error') { print "recoverable. re-queueing $i\n\n"; push @list, $i; } else { print "fatal error. giving up on $i\n\n"; } print "======== end $i\n\n"; }