Friday, 30 May 2014

Loops and conditionals: Control your loop

I've used loop controls once and I can't remember why, but I know it was useful...

Loop controls, as it indicates in the name, give you more control over the flow of a loop. Without them, you just have to accept the way that for, if, while etc. loops function and work around them to serve your purpose. With them, you can modify the way a loop works, for example you might want to jump out of a loop completely for reasons other than the initial condition not being satisfied.

There are three loop control operators:

Last
Next
Redo

These operators can be applied to the five kinds of loops there are in Perl:
  • for
  • foreach
  • while
  • until
  • naked block
NB You can't apply them to an if statement because it's not a loop and it really won't make sense, the code blocks in if statements are only run zero times or once. 

Wait, what is this naked block thing? It sounds kind of weird...

This block is naked! It has nothing in front of its curly braces :-o

{
    code goes here
}

A naked block is just a block of code surrounded by curly braces, with no keyword or condition. It runs only once, as if the curly braces weren't there so in my opinion, it's not really a loop. You might wonder what the point is of putting curly braces around code. They can be useful if you want to have a variable that is only in scope in part of the code because the guideline is to declare a variable in the smallest scope available.

Here's how the loop control operators work:

Last
This operator immediately ends execution and breaks out of the loop, no more iterations happen, even if you haven't finished going through all the elements in the array or the condition still evaluates to true etc.

If you're used to how perl works, it may look a bit strange in the code below because it's a bareword (i.e. has no prefix or suffix, or sigil, it's just a word on it's own). Barewords usually aren't allowed but there are a few key words, including the loop control words, that are. If you try to type in a bareword that isn't one of these key words, you will be complained at.
1.   foreach(@myarray) {
2.     if( $_ eq "end") {
3.         last;
4.     else {
5.         print "$_\n";
6.     }
7. }
This code goes through the array called myarray and prints out each element until it gets to an element that is the string "end". End will not be printed and the loop will stop, no further elements will be printed.


Next
This operator is used when you want to jump out of the current iteration but you then want the next iteration to continue afterwards. When next is hit, it jumps to the end of inside the current loop (at the closing curly bracket) and then the next iteration starts.
I've nicked this example straight from "Learning Perl" by Randal Schwartz so thanks people that wrote it :-)
1.   while (<STDIN>) {
2.     foreach my $word (split(' ', <STDIN>)) {
3.         $total++;
4.         next if /\W/;
5.         $valid++;
6.     }
7. }
The code above basically looks at words in a file and counts how many words in total there are and of these, how many are real words. Here is a mored detailed explanation if you'd like to know more:

Lines of input are being read one at a time through <STDIN>. The split function splits the line up using spaces so individual words will be looked at. Then for each of the words in the line, the count of words is upped one ($total) and then on line 4, the word is looked at to see if it contains any non-word characters (anything but letters, numbers and underscores) This is done by /\W/ where 'W' matches any non-word character. If the word contains any non-word characters, next is invoked, the iteration exits and the next word is looked at. If the word doesn't contain any non-word characters, the valid count will be incremented.
Hope that makes sense!

You may have noticed I've sneakily glossed over a few things like how the input is being taken from <STDIN> but just trust me (and the people that wrote the example) for the moment, that it works and does what it's supposed to.

Redo
This operator means go back to the beginning of the iteration you are currently in and do it again, without testing the condition. Why would we need this? Here's an example, again, curtesy of "Learning Perl", but slightly modified - thanks guys!
1.   my @words = qw( accommodate believe colleague disappear embarrass);
2. my $errors = 0;
3.
4. foreach my $word (@words) {
5.         print "Type the word '$word':  ";
6. chomp(my $try = <STDIN>);
7. if ($try ne $word) {
8.     print "Sorry - that's not right.\n";
9.     $errors++;
10.     redo;
11.     }
12. }
13. print "You completed the test with $errors errors.\n";
Ok so what does this mean? It's basically a spelling test and you have to keep writing the words in the @words array until you spell them correctly. It's not the best spelling test in the world because you are shown the word you need to type, but you get the idea.

So, for each of the words in the words array, the command line will display a prompt for the user to type in the word. Then line 6 uses the chomp operator, which cuts off any newline characters from the end of the word that the user has typed in (this is from pressing enter to submit the word they have spelt). The user input is assigned to $try. Then $try is compared to the correct version of the word from @words on line 7. If the word is spelt correctly, the next iteration begins, giving the user a new word to spell. If they have spelt it wrong however, the terminal asks them to spell it again, then adds one to the error count and on line 10, redo tells the programme to do the iteration again.


All of the above examples contain nested loops and the inner one is always an if statement. I'm trying to think of examples where you use a loop control operator without a nested if statement and I'm not sure if they would work without one. Please let me know if you have any examples.


Labelling Blocks

Labels are very helpful, they can actually control where you will be taken next. They also allow anyone else reading your code - or even yourself if you've forgotten - to see clearly what is going on.

Sometimes you will be nesting loops and blocks of code inside of each other and it can be hard to figure out where the loop controls will take you next. You can use labels to help with this, especially when you want to work with a loop block that's not the innermost one.

I've also been told that it's best practise to use labels even if you think it's easy to follow where the loop will go. This may be true for you but you need to bear in mind that other people may look at your code and not understand or this loop could eventually be expanded and adding a label now will make it clearer for the future.

Labels are another example of a bareword and they are named like any other identifier in perl; they can include letters, digits and underscores, but can't start with a digit. Larry Wall has actually recommended that labels are all upper case and this is what I've come across in real-world code so far. Perl is case sensitive and having an uppercase label will avoid having a label with the same name as a built-in function or even one of your own subroutines.

To add a label, first you define the block of code that you want to label by writing the label name, followed by a colon, followed by the loop:

ELEMENT: foreach (condition) {
    code block
}

This has labelled the whole foreach loop as "ELEMENT", (not just the line) and it includes everything inside the curly braces, as well as the foreach statement. After labelling your loop, then you decide which of the operators you are going to use and where in the code block you are going to put it.
This example is curtesy of Andrew Solomon, who has taught me a lot about perl and taught me about loop labels.
1.   sub pick_random_colour { 'purple' };
2. my %betters = (
3.     amy => [qw/green/],
4.     ben => [qw/ blue purple yellow/],
5.     chloe => [qw/red orange pink/],
6. );
7.
8. my $win = pick_random_colour();
9.
10. HOORAY: foreach my $better (sort(keys(%betters))) {
11.     foreach my $ticket (@{$betters{$better}}) {
12.         if ($ticket eq $win) {
13.             print "Hooray! $better wins with $ticket\n";
14.             last HOORAY;
15.         }
16.         print "Nope, $better has no luck with $ticket\n";
17.     }
18. }
Ok, this is my largest example so far, but I think it really shows how labels work. Try it for yourself, run it, then take the label out, and then run it again and see the difference. Or you can look below and see the results:

With labels:
Nope, Amy has no luck with green
Nope, Ben has no luck with blue
Hooray! Ben wins with purple

Without labels:
Nope, Amy has no luck with green
Nope, Ben has no luck with blue
Hooray! Ben wins with purple
Nope, Chloe has no luck with red
Nope, Chloe has no luck with orange
Nope, Chloe has no luck with pink

Notice Ben has no luck with yellow is not printed out.

The basic gist of this example is kind of like a lottery where each person can buy a ticket with a unique colour on it. Amy has bought one ticket (green) and Ben and Chloe have bought three tickets each (blue purple and yellow and red, orange and pink respectively). A random colour is picked and the winner is the one with the matching ticket.
I've hacked it a little bit and written the pick_random_colour subroutine so that purple is always picked instead of a random colour. This is just so I can be certain of the outcome each time and can really see the difference when I take the labels out.

So here is the full breakdown, again, skip this if you feel you understand the example already.
Line 1, this is where my pick_random_colour subroutine is written that picks purple every time. I'll explain subroutines in more detail in a later post but for now, they are just pieces of code written in one place and called in another. You can see it being called on line 8. I've then declared an array, %betters, and assigned it names as keys and different lottery tickets as bets, each with a different, unique colour. Then the pick_random_colour subroutine is called on line 8 and of course, it's going to pick "purple". Now comes the exciting bit - the loop. So, we've got for each better in the hash and then for each ticket the better has, the ticket is compared to $win, which is purple. I've used sort on line 10 just so that the betters will be looked at in the same order each time. If the ticket is equal to purple, then the string on line 13 is printed and then we get to line 14 with the loop control and label. This indicates that we should cut out of the inner loop as well as the loop that is labelled with "HOORAY". This means that the program has finished. If the ticket is not equal to purple, the string on line 16 will be printed and the search will continue as to who has the winning ticket.
If we take out the labels, but leave in the loop control, only the inner loop will be exited when the winner is found and the next better will be looked at. You can see from the results above that once the winning ticket from Ben has been found, Ben's remaining tickets are not looked at, but then Chloe's tickets are looked at.


2 comments: