Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl Monk, Perl Meditation
 
PerlMonks  

Loops, the scalar range operator, and objects

by mstone (Deacon)
on Dec 22, 2001 at 02:26 UTC ( #133899=perlmeditation: print w/ replies, xml ) Need Help??

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"; }

Comment on Loops, the scalar range operator, and objects
Select or Download Code
Re: Loops, the scalar range operator, and objects
by dmitri (Curate) on Dec 22, 2001 at 10:57 UTC
    Wow. You left nothing to discuss... :)

    On a serious note, I have only used .. like this:

    while (<>) { print if (/a/ .. /b/); }

    (Think sed).

Re: Loops, the scalar range operator, and objects
by vladb (Vicar) on Dec 22, 2001 at 19:57 UTC
    That's a good piece of rather in-depth discussion ;). And this is the first time that I understood the subtle workings of the range operator. It's really a 'flip-flop' kind of an operator. Here's the way I'd use it:
    -----------------
    #!/usr/local/bin/perl -w use strict; # TEST for '..' (and '...') operators # # Description of the range operator is found # at the END of this file. # print "TEST 1: Skipping perldoc comments.\n----------------\n"; while (<DATA>) { next if (/^=head/ .. /^=cut/); print $_; } __DATA__ # pretend this is a config file var = 0; var1 = 0; =head this should be skipped by the /^=head/ .. /^=cut/ range operator. some paragraph here some paragraph here =cut var2 = 0;
    -----------------

    Your example with data is also very neat one. I guess one could use the range operator with special objects that serve as data iterators (say, list iterators etc.)

    "There is no system but GNU, and Linux is one of its kernels." -- Confession of Faith
Re: Loops, the scalar range operator, and objects
by dragonchild (Archbishop) on Jan 02, 2002 at 19:06 UTC
    Very very nice! I only wish I could ++ more than once.

    I also very much like the use of an object to handle everything. And, if you use this as a Facade, then you get all sorts of reusability. To extend your idea just a little...

    • Loop_control would be composed of:
      • An error-handling object
      • The object you're working on (like a network socket)
      • The Strategy object you want to apply
    All Loop_control would do would be the client. :-)

    ------
    We are the carpenters and bricklayers of the Information Age.

    Don't go borrowing trouble. For programmers, this means Worry only about what you need to implement.

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlmeditation [id://133899]
Approved by root
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others about the Monastery: (10)
As of 2014-10-20 08:29 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    For retirement, I am banking on:










    Results (73 votes), past polls