Chapter 5. Control flow
WHAT YOU WILL LEARN IN THIS CHAPTER
Using if/elsif/else expressions
Understanding different types of for loops on lists
Understanding the while iterator
Using Perl’s switch statement (Perl version 5.10 and above only)
Statement modifiers and when to use them
You understand some of the basics of Perl, but now we’re going to get closer to the heart of programming. When you program, you constantly make decisions loop over data based on those decisions. That’s what this chapter is all about: how Perl makes decisions and looping over data.
The if statement
We’ll start with boolean logic. As explained in Chapter 4, Working With data, the following values are considered “false” in Perl:
undef""(the empty string)00.0"0"(the “string” zero)
Some languages have specific Boolean objects, or TRUE and FALSE identifiers, Perl does things a little differently. As you work through the examples, try to see what Perl is doing and why. If you have experience with programming languages that have a different approach, consider the strengths and weaknesses of the different approaches. You’ll come to appreciate what Perl does and why.
Basic conditionals
A basic if statement looks like if ( EXPRESSION ) BLOCK:
if ( EXPRESSION ) {
# 0 or more statements
}An expression might have a simple boolean operation in there. For example, the following guarantees that $x will not be less than $y by swapping their values if $x < $y evaluates as true.
if ( $x < $y ) {
( $y, $x ) = ( $x, $y );
}You can even put compound conditionals in there if you like:
if ( $x < $y && $y > 10 ) {
# do something
}That works because the < and > operators have a higher precedence than the && operator (see chapter 4). However, many programmers prefer parentheses to be explicit:
if ( ( $x < $y ) && ( $y > 10 ) ) {
# do something
}Optionally, you can use the and, or, xor and not forms of various boolean operations because those have the lowest precedence of all operations and you don’t have to memorize the precedence order:
if ( $x < $y and $y > 10 ) {
# do something
}It reads nicer, too. Just remember the gold rule of precedence: when in doubt consider using parentheses to force precedence. Even if you get it right another programmer reading your code might not.
Note
Some programmers familiar with other languages ask why Perl requires curly braces around the body of code associated with the if statement. For some programming languages, the curly braces are optional if only a single statement is executed:
if ( x == y )
x++;For Perl, the curly braces are always required. A common source of bugs in programming languages like C is a developer trying to add an extra statement to an if block, not noticing that there are no curly braces delimiting the block:
if ( x == y )
x++;
y++;Never again will you try to add an extra statement to an if block and wonder why your code is broken.
Because this is Perl, we’re not limited to simple boolean constructs in if statements. The expression inside of the parentheses in the if statement is evaluated in scalar context and the result is then evaluated as true or false (this is sometimes referred to as “boolean context”). Consider the following:
if (@names) {
# do something
}As you may recall, an array evaluated in scalar context returns the number of elements in the array. In the case of if (@names) BLOCK, if the @names array is empty, the if block will not execute. This works for hashes, too:
if (%names) {
# will not execute if %names is empty
}Note
The if (%names) { ... } construct is a bit weird. Internally, a hash creates “buckets” that are used to determine where a given key’s values are to be found. In scalar context, a hash returns a string with the number of buckets used, followed by a forward slash, followed by the number of buckets allocated for that hash. This is sometimes useful for debugging hash problems, but otherwise has little practical use.
However, if the hash is empty, it returns 0 (zero) in scalar context, allowing the if (%names) { ... } construct to work. The following prints 3/8 - 0 and should make it clear what’s happening.
my %hash1 = ( foo => 1, bar => 2, baz => 3 ); my %hash2; my $scalar1 = %hash1; my $scalar2 = %hash2; print "$scalar1 - $scalar2";
You can also use the if statement with assignment. The following will evaluate to false if no customer is returned:
if ( my $customer = get_customer($id) ) {
# only executed if $customer evaluates to true
}else/elsif/unless
Sometimes you want to take a different action depending on whether or not a value is true or false. You can follow the if block with an else block.
if ( $temperature > 0 ) {
print "The temperature is above freezing: $temperature\n";
}
else {
print "The temperature is not above freezing. Exiting the program.";
exit;
}Or if you want to test other conditions if the first if has failed, you can use an elsif block.
if ( $temperature >= 100 ) {
print "It's boiling in here!\n";
cool_things_down($temperature);
}
elsif ( $temperature < 0 ) {
print "It's freezing in here!. Exiting.\n";
exit;
}
elsif ( $temperature > 13 and $temperature < 21 ) {
print "It's perfect weather for outdoor exercise. Impromptu holiday!\n";
exit;
}
else {
print "The temperature is acceptable. Proceed.\n";
}The final else is optional:
if ( $customer_is_male ) {
redirect_to_male_apparel();
}
elsif ( $customer_is_female ) {
redirect_to_female_apparel();
}Many (including your author) recommend that a final else block be supplied - even if it does nothing - to make it clear to other programmers who work on your code that you did not make a mistake and overlook a condition. Adding a comment to that else block makes it even more clear.
if ($customer_is_male) {
redirect_to_male_apparel();
}
elsif ($customer_is_female) {
redirect_to_female_apparel();
}
else {
# TODO: implement redirect_to_unisex_apparel()
}You can also use multiple elsif statements:
if ( !$color ) {
print "No color found";
}
elsif ( 'blue' eq $color ) {
print "#0000FF";
}
elsif ( 'green' eq $color ) {
print "#00FF00";
}
elsif ( 'red' eq $color ) {
print "#FF0000";
}
else {
print "I don't know what to do with color ($color)";
}Long if/elsif/else chains should be avoided, if possible, because they start to make to code harder to read. For example, with the above code, it’s better to use a hash:
my %color_code_for = (
blue => '#0000FF',
green => '#00FF00',
red => '#FF0000',
);
if ( !$color ) {
print "No color found";
}
elsif ( my $code = $color_code_for{$color} ) {
print $code;
}
else {
print "I don't have a code for the color '$color'";
}With using the hash, if you want to support new color codes, you can just add a new entry to the hash rather than create new elsif blocks for every color.
my %color_code_for = (
black => '#000000',
blue => '#0000FF',
green => '#00FF00',
red => '#FF0000',
white => '#FFFFFF',
);
if ( !$color ) {
print "No color found";
}
elsif ( my $code = $color_code_for{$color} ) {
print $code;
}
else {
# print "I don't know what to do with color ($color)";
print "I don't have a code for color ($color)";
}Note
As with many languages, the whitespace is not particularly significant. Your author prefers “uncuddled” else statements because he finds them easier to read. Others prefer a more compact format:
if ( $temperature >= 100 ) {
print "It's boiling in here!\n";
cool_things_down($temperature);
} elsif ( $temperature > 0 ) {
print "The temperature is acceptable. Proceed.\n";
} else {
print "It's freezing in here!. Exiting.\n";
exit;
}Still others prefer all braces to be aligned vertically:
if ( $temperature > 0 )
{
print "The temperature is above freezing: $temperature\n";
}
else
{
print "The temperature is not above freezing. Exiting the program.";
exit;
}All of these are perfectly acceptable and arguments for or against one notwithstanding, don’t stress about it. Just pick one style and stick with it.
Of course, as already seen, you can also reverse the sense of a condition with the ! or not operators:
if ( !$allowed ) {
print "You can't do that!";
}
if ( not $found ) {
print "I didn't find it!";
}Perl also has a rather curious unless statement. It’s the opposite of the if statement. The above statements can be rewritten as follows:
unless ( $allowed ) {
print "You can't do that!";
}
unless ( $found ) {
print "I didn't find it!";
}As with the if statement, you can use elsif and else, but as you might imagine, it can be confusing:
unless ($condition) {
# ...
}
elsif ( $some_other_condition ) {
# ...
}
else {
# ...
}The use of the unless check is strongly discouraged in all but the simplest cases. The logic gets confusing and many developers will cheerfully fantasize about using pliers to extract your fingernails if you abuse the unless statement.
The Ternary Operator ?:
As with many other programming languages, Perl also provides a ternary operator as a “shortcut” for an if else statement. The ternary operator’s syntax looks like this:
VALUE = CONDITION ? IFTRUE : IFFALSE
You can write this:
my $max = ( $num1 < $num2 ) ? $num2 : $num1;
That’s the same as writing:
my $max;
if ( $num1 < $num2 ) {
$max = $num2;
}
else {
$max = $num1;
}But as you can see, the ternary operator is much more compact. Also note that with the ternary operator, you don’t need to predeclare the $max variable because the ternary operator does not introduce a new scope.
You can also chain ternary operators:
my $max = ( $num1 < $num3 and $num2 < $num3 ) ? $num3
: ( $num1 < $num2 ) ? $num2
: $num1;With good formatting this construct is very easy to read and it has the advantage of the final “else” being required and it’s a syntax error if you omit it. The only caution is to be careful about your logic because it is easy to get wrong.
for/foreach loops
Being able to make decisions in our code is important. We also want to work with our data structures to manipulate them. Loops are often used to do this. There are a variety of ways to accomplish this in Perl, along with a lot of tips and tricks to be aware of. We’ll start by looking at for/foreach loops with arrays and lists.
Arrays
A for loop is used to iterate over every element in an array or list. A basic for loop in Perl looks like this:
for my $number (@numbers) {
print "$number\n";
}There’s also a foreach version:
foreach my $number (@numbers) {
print "$number\n";
}In Perl, for and foreach are identical. There is no difference aside from the spelling. Your author likes foreach because he feels it reads better, but honestly, it’s a matter of personal preference.
Note
Please note that if you read perldoc perlintro and perldoc perlsyn, there’s a strong implication that for and foreach loops are somehow different. The docs generally describe for as being used with C-style for loops (covered later) and foreach loops for lists. Unfortunately, the documentation is misleading on this point. There is no difference between the two.
The for/foreach loop is one builtin which assigns to the $_ by default. If you don’t specify a variable name, $_ is assumed. The following prints the numbers 5, 6, and 7.
my @numbers = ( 5, 6, 7 );
foreach (@numbers) {
print "$_\n";
}When combined with builtins which operate on $_ by default, it can shorten your code a bit. The following will remove newlines from each element and, if the element evaluates as true, prints the element.
foreach (@names) {
chomp;
if ($_) {
print;
}
}By contrast, here’s the same code using a variable (remember that the foreach here could be written as for):
foreach my $name (@names) {
chomp $name;
if ($name) {
print $name;
}
}Whichever method you prefer, just be aware that it’s very common to see experienced Perl developers know when the $_ variable is used and take advantage of this fact.
One important thing to remember about for loops is that the variable used to designate each element, whether it’s $_ or a named variable, is an alias to the element in question. This allows you to modify an array in place. For example, if you want all elements in an array that are less than zero to be set to zero, you can take advantage of aliasing:
my @numbers = ( -7, -5, -1, 0, 3, 6, 29 );
foreach my $number (@numbers) {
if ( $number < 0 ) {
$number = 0;
}
}
print join ',', @numbers;That code snippet will print 0,0,0,0,3,6,29. If you want to manipulate the value but not change the original array, just assign the element to a new variable. This is one case where the $_ default can be clearer.
my @numbers = ( -7, -5, -1, 0, 3, 6, 29 );
foreach (@numbers) {
my $number = $_; # don't use an alias
if ( $number < 0 ) {
$number = 0;
}
}
print join ',', @numbers;Running that will show you that the array has escaped unchanged.
Warning
A subtle trap occurs here when you forget that the list elements are aliased:
for my $number (1) {
$number++;
}The above code, while appearing to be legal Perl, will generate the following error are runtime:
Modification of a read-only value attempted at ...
This is because the 1 is a hard-coded literal and its value cannot be changed. If 1 was assigned to the scalar $number, $number++ would be valid. However, because it’s aliased to that value, your code will break.
Lists
The for loops is useful for arrays, but we can use them with anything that returns a list.
my %economic_description = (
libertarians => 'Anarchists with jobs',
anarchists => 'Libertarians without jobs',
randroids => 'Closet libertarians',
democrats => 'the tax and spend party',
republicans => 'the tax cut and spend party',
);
foreach (sort keys %economic_description) {
my $description = lc $economic_description{$_};
$_ = ucfirst;
print "$_ are $description.\n";
}And that allows you to offend just about everyone by printing:
Anarchists are libertarians without jobs. Democrats are the tax and spend party. Libertarians are anarchists with jobs. Randroids are closet libertarians. Republicans are the tax cut and spend party.
Note that even though we have the $_ = ucfirst line in there, this code does not change the hash keys, even though the for loop aliases its arguments. This is because keys() (like the sort() function in the loop) returns a new list.
Range operators, when used in list context, also return a list.
for my $number ( -10 .. 10 ) {
$number++;
print $number;
}That prints the numbers from -9 to 11. Note that even though it may appear that we have numeric literals here and thus $number++ should throw a Modification of read-only value error, we don’t have that problem. This is because the range operator returns a list. If the values of the list are not assigned to anything, they are anonymous variables. This means that they can be changed like any other variable, even though it looks strange.
Note
OK, for those who really have to understand why the range operator works even when we’re modifying the variable, here’s some advanced magic for you.
perl -MDevel::Peek -e 'Dump(1)'
That should output something similar to:
SV = IV(0x100827d10) at 0x100827d20 REFCNT = 1 FLAGS = (IOK,READONLY,pIOK) IV = 1
The Devel::Peek module was released with Perl in version 5.6.0. It exports a Dump() function which allows you to “peek” into a scalar and see what it looks like to Perl. In this case, we call Dump() on the literal value 1 and you’ll note on the FLAGS line that it says READONLY.
Now we’ll try this again with 1..1. That range operator will return a 1 element list containing the number one. Here’s the code:
perl -MDevel::Peek -e 'Dump(1..1)'
And here’s the output:
SV = IV(0x100802f98) at 0x100802fa8 REFCNT = 1 FLAGS = (IOK,pIOK) IV = 1
You’ll note that FLAGS does not contain READONLY and thus can be modified. See perldoc Devel::Peek for more information.
C-Style
Of course, there’s also the C-style for loop, with the syntax:
for (EXPRESSION ; EXPRESSION ; EXPRESSION) BLOCK
This, for example, prints the numbers 0 through 9:
for ( my $i = 0; $i < 10; $i++ ) {
print "$i\n";
}For those not familiar with this style of loop, the three semicolon separated expressions correspond to loop initialization, the loop test and the loop change.
Note that all three of these expressions are optional. The following is almost equivalent to the previous code, except that the $i variable is no longer lexically scoped to the for loop.
my $i = 0;
for ( ;$i < 10; ) {
$i++;
print "$i\n";
}In fact, you can even omit that loop test with a last() command. We’ll cover that a bit later.
C-style for loops are not very popular in Perl and they’re often not needed. For example, sometimes you need the index of an array, so you do this:
for ( my $i = 0; $i < @array; $i++ ) {
print "$i: $array[$i]\n";
}But that’s more cleanly written and commonly seen like this:
for my $i ( 0 .. $#array ) {
print "$i: $array[$i]\n";
}The special $# syntax at the front of the array name means “the index of the last element of an array”. So if an array is 4 elements long, $#array will return 3.
Warning
You should only use the $#some_array for iterating over the indexes of an array, as shown previously. Inexperienced Perl programmers sometimes write code like the following and wonder why it seems to randomly fail.
if ( $#array ) {
# do something with array
}The $#array syntax returns a true value (-1) if there are no elements in the array, a false value (0) if there is one element in the array and a true value (1 or greater) if there is more than one element in the array. The following example should make this clear:
#!perl -l
print "$#array\n";
@array = ('fail!');
print "$#array\n";
push @array, 'not fail!';
print "$#array\n";That prints -1, 0, and 1. If you want to know if an array is empty, just use the array name in scalar context:
if (@array) { ... }One time when a C-style for loop comes in handy is when you must iterate over a range of numbers that are not easily generated by the range operator. The following prints a vertical sine wave in your terminal:
for ( my $i = 0 ; $i <= 25 ; $i += .25 ) {
my $amplitude = int( 40 + 35 * sin($i) );
print " " x $amplitude;
print ".\n";
}You could write this without the C-style for loop, but you might find it harder to understand.
for my $i ( 0 .. 100 ) {
$i = $i / 4;
my $amplitude = int( 40 + 35 * sin($i) );
print " " x $amplitude;
print ".\n";
}Or perhaps the variable increment is set within the program:
for ( my $i = 7; $i < 10; $i += $user_choice ) {
print "$i\n";
}Which you prefer in any context is just a matter of preference.
Note
You might wonder why $seen{$element}++ does not issue a warning about incrementing an uninitialized value. In fact, the following three statements have identical behavior, but only the last one issues an uninitialized warning:
$seen{$element}++;
$seen{$element} += 1;
$seen{$element} = $seen{$element} + 1;The first two do not issue a warning as described in the Declarations section of perldoc perlsyn:
If you enable warnings, you'll be notified of an uninitialized value
whenever you treat undef as a string or a number. Well, usually.
Boolean contexts, such as:
my $a;
if ($a) {}
are exempt from warnings (because they care about truth rather than
definedness). Operators such as "++", "--", "+=", "-=", and ".=",
that operate on undefined left values such as:
my $a;
$a++;
are also always exempt from such warnings.If you must use a statement that might issue a warning and you do not want that warning, you can do the following:
{
no warnings 'uninitialized';
$seen{$element} = $seen{$element} + 1;
}The no warnings 'uninitialized' statement will disable uninitialized warnings in the scope of that block. We deliberately use a block here to ensure that we don’t suppress other uninitialized warnings that we care about. You could also do this:
$seen{$element} ||= 0;
$seen{$element} = $seen{$element} + 1;In Perl, there are usually multiple ways to get the job done.
Note
What’s a “future programmer”? It might be the person the company hires after they promote you for having the foresight to read this book. However, that future programmer might be you! Just because the code is clear now doesn’t mean it’s going to be clear six months from now. This means that your code should be as clear as possible and you should really try to avoid clever tricks in your code. In fact, you’ll find that many of the best programmers out there write code which looks very simple because they know that being able to read code is just as important as being able to write it.
while/until loops
The while statement has the general syntax of while ( EXPRESSION ) BLOCK. The block is executed while the EXPRESSION is true.
my $i = 10;
while ( $i > 0 ) {
if ( rand(3) > 2 ) {
$i++;
}
else {
$i--;
}
print $i,$/;
}That code will gradually lower the value of $i until the expression $i > 0 evaluates as false.
It is important to recall that the main difference between while loops and for loops is that while loops iterate until a condition is false, while for loops iterate over a list.
The while loop in Perl is commonly used with iterators. The one you know now is the each() iterator for hashes.
my %odd_couples = (
'Abbott' => 'Costello',
'Martin' => 'Lewis',
'Lemmon' => 'Matthau',
);
while ( my ( $star1, $star2 ) = each %odd_couples ) {
print "$star1: $star2\n";
}We’ll see more of while loops as we go through the book. Iterating over lines in a file will be covered in Chapter 9, Files and Directories and we’ll see other forms of iterators as we work through various examples.
The opposite of the while loop is the until loop. The syntax is the same, replacing while with until. The while loop iterates while its condition is true and the until loop iterates while its condition is false. Here we compute the factorial of the number 5 (5 * 4 * 3 * 2 * 1).
my $factorial = 1;
my $counter = 1;
until ( $counter > 5 ) {
$factorial *= $counter++;
}
print $factorial;Like the unless statement, it’s recommended to be cautious in use of the until statement due to the potential to confuse programmers. The above code is probably better written as:
my $factorial = 1;
my $counter = 1;
while ( $counter <= 5 ) {
$factorial *= $counter++;
}
print $factorial;Lists
Programmers often try to use while or until loops with lists instead of iterators or boolean conditions. You can do this but it is fraught with danger and should be avoided. Here are several ways you can fail spectacularly.
my $total = 0;
while ( my $price = shift @orders ) {
$total += $price;
}
print $total;Most of the time, that’s going to work just fine. Until you have a sale item with a price of zero.
my @orders = (5,5,0,5);
my $total = 0;
while ( my $price = shift @orders ) {
$total += $price;
}
print $total;That will print 10 instead of the (probably) desired 15. So you decide to get clever and make sure the price is defined:
my @orders = ( 5, 5, 0, undef, 5 );
my $total = 0;
while ( defined( my $price = shift @orders ) ) {
$total += $price;
}
print $total;That’s also going to fail because we’ve managed to sneak an undefined value into the array. If you need to use a while loop here, do it like this:
my @orders = ( 5, 5, 0, undef, 5 );
my $total = 0;
while (@orders) {
my $price = shift @orders;
$total += $price;
}
print $total;If you insist upon using a while/until loop here (perhaps because you want the array empty at the end), you should still consider rewriting with a for loop.
my $total = 0;
for my $price (@orders) {
$total += $price;
}
@orders = ();As you can see, the for loop is shorter and easier to read.
last/next/redo/continue
When you’re working with loops, it’s often useful to have fine-grained control over how the loops behave. The last(), next(), redo() and continue() builtins help with this. The last() builtin will automatically exit a loop. For example, to find the first perfect square (a square number that is the square of an integer) in an array, you could do the following:
my @numbers = ( 3, 7, 9, 99, 25 );
my $first;
for my $number (@numbers) {
my $root = sqrt($number);
if ( int($root) == $root ) {
$first = $number;
last;
}
}
if ( defined $first ) {
print "The first perfect square in the array is $first\n";
}
else {
print "No perfect square found in array\n";
}That will exit the loop when $number equals 9 and it will print:
The first perfect square in the array is 9
It’s very handy when you want to process a loop until you reach a desired condition and then terminate the loop.
The next() statement is useful when you want to skip the processing of some elements. We could use this to rewrite the above code to find all perfect squares in a loop.
my @numbers = ( 3, 7, 9, 99, 25 );
my @perfect_squares;
for my $number (@numbers) {
my $root = sqrt($number);
if ( int($root) != $root ) {
next; # skip the rest of the loop BLOCK
}
print "Found perfect square: $number\n";
push @perfect_squares, $number;
}The continue statement is not common, but it’s useful it you have a block of code that must be executed every time through a loop, before the loop check occurs again. The syntax looks like this:
for (EXPRESSION) BLOCK continue BLOCK while (EXPRESSION) BLOCK continue BLOCK
Regardless of a next or last statement in the loop body, the continue will always execute after the last statement in the loop body is executed.
use strict;
use warnings;
my @numbers = ( 3, 7, 9, 99, 25 );
my @perfect_squares;
for my $number (@numbers) {
my $root = sqrt($number);
if ( int($root) != $root ) {
next; # skip the rest of the loop BLOCK
}
print "Found perfect square: $number\n";
push @perfect_squares, $number;
}
continue {
print "Processed $number\n";
}That example will print:
Processed 3 Processed 7 Found perfect square: 9 Processed 9 Processed 99 Found perfect square: 25 Processed 25
The redo statement is even less common. What it does is redo the body of the loop without testing the condition or executing the continue block. It’s a bit confusing to people and even the perldoc -f redo documentation sheds little light on the matter. It’s used seldom enough that we won’t mention further, aside from using it in one of the exercises at the end of this chapter.
Labels
When we listed examples of the for/foreach/while/until syntax, we omitted labels. Labels can be very useful for cleaning up code. A label is a bare identifier followed by a colon. The next, last, and redo builtins take an optional label as an argument. If that label is present, control jumps to that label. They can be used to make your code a bit more self-documenting:
NUMBER: foreach my $number (@numbers) {
# lots of code
if ($some_condition) {
next NUMBER;
}
# more code
}However, their real power lies in controlling the behavior of next, last and redo when using nested loops. Let’s say that you have two arrays of strings, @strings1 and @strings2 and you want to find any strings in the first array that are substrings of any strings in the second array. Here’s one way of writing that:
my @strings1 = qw( aa bb cc dd ee );
my @strings2 = qw(
an
intelligent
robber
needs
a
good
ladder
);
my @found;
DOUBLED_LETTER: foreach my $double (@strings1) {
foreach my $word (@strings2) {
if ( index($word, $double) != -1 ) {
push @found, $double;
next DOUBLED_LETTER;
}
}
}
print "@found";That prints bb dd ee. If the next DOUBLED_LETTER statement was not present, we would continue searching for words containing the doubled letter, even though it was already found. If the arrays were very large, this could be extremely inefficient.
Statement Modifiers
As an alternative to the previously described if/while/for blocks, you can add the if/while/for to the end of a single statement:
print "We can haz cheez" if $trite;
You may find them a bit cleaner then:
if ($trite) {
print "We can haz cheez";
}Types of Statement Modifiers
The allowed modifiers are:
STATEMENT if EXPR; STATEMENT unless EXPR; STATEMENT while EXPR; STATEMENT until EXPR; STATEMENT for LIST; STATEMENT foreach LIST;
Unlike the normal usage of these keywords, parentheses are optional around the EXPR or LIST. For example:
print "We have a valid user: $user\n" if $user;
When using a for/foreach loop, $_ is aliased to the variable. The following prints the numbers 1 through 5 on successive lines.
my @array = ( 1 .. 5 ); print "$_\n" foreach @array;
The while and until loops behave similarly. Note that the EXPR is evaluated before the statement. Thus, the following prints 9 through 0, not 10 to 1.
my $countdown = 10; print "$countdown\n" while $countdown--;
Note that the STATEMENT may be a compound statement. The example from perldoc perlsyn is the following:
go_outside() and play() unless $is_raining;
That reads nicely, but it does have a subtle trap. The play() subroutine will not be called if go_outside() returns false. You can replace the and with a comma if you want to avoid this:
go_outside(), play() unless $is_raining;
Statement modifiers should be used sparingly. It’s recommended that you use them when the emphasis is to be placed on the statement and not on the modifier.
print "Using config data" if $config;
For the preceding code, printing Using config data is the expected behavior and is what the programmer should focus on when skimming code. The if $config modifier is easily overlooked. If if $config is a normal condition that the programmer should be more aware of, avoid using the modifier.
if ($config) {
print "Using config data";
}Use of keyword (EXPRESSION or LIST) BLOCK versus a statement modifier is largely a matter of preference, but if you have a compound statement or the condition is what needs the emphasis, avoid the statement modifier.
do while/do until
We won’t cover the do builtin (perldoc -f do) much in this book, as the common uses for it belonged to Perl version 4, which should have been put to death when Perl 5 was released in 1994, but there is one form of the do builtin which is still very much in use:
do BLOCK
This form of do executes the statements in the block, return the value of the last executed expression. It is most commonly used with a while or until statement modifier. The grammar looks like:
do BLOCK while EXPRESSION; do BLOCK until EXPRESSION;
For example:
my $factorial = 1;
my $counter = 1;
do {
$factorial *= $counter++;
} while $counter <= 5;
print $factorial;The do/while, do/until syntax has two major differences between while and until statements. First, it guarantees that the BLOCK will be executed at least once. Second, it’s not really a loop. Many people mistakenly think it’s a loop, but it’s just a standard do BLOCK statement followed by a statement modifier. As a result, next, last, redo and continue statements do not apply.
given/when
Many languages offer what is called a switch statement. It tends to look like the following:
switch(number) {
case (0):
printf("The number is 0");
break;
case(1):
printf("The number is 1");
break;
case(2):
printf("The number is 2");
break;
default:
printf("The number is unexpected");
}There are a number of historical reasons why a switch statement tends to be written in this manner, but we’ll skip over those and go straight to Perl’s given/when statement. It is available in Perl version 5.10.0 or better.
Basic syntax
The syntax of given/when looks like this:
given (EXPRESSION) BLOCK
And BLOCK is composed of zero or more when statements:
when (EXPRESSION) BLOCK
Those can be followed by a default BLOCK statement. The previous switch statement can be written in Perl as follows:
use 5.010;
my $number = 1;
given ($number) {
when(0) { print "The number is 0"; }
when(1) { print "The number is 1"; }
when(2) { print "The number is 2"; }
default { print "The number is unexpected"; }
}If you read the code aloud, it actually reads much better than the switch version. For some languages, the switch statement can only operate on integers (part of the historical discussion we’re side-stepping). In Perl, the given keyword assigns the value of EXPRESSION to $_ and the EXPRESSION in when (EXPRESSION) BLOCK tests the value of $_. Thus, you can do things like this:
given ($number) {
when ($_ < 0) {
print "The number is negative";
}
when ($_ > 0) {
print "The number is positive";
}
default {
print "The number is 0";
}
}If you actually want the when statement to test subsequent when statements, you can use the continue keyword.
given ($word) {
when ( lc $_ eq scalar reverse $_ ) {
print "'$word' is a palindrome\n";
continue;
}
when ( length($_) > 10 ) {
print "The length of '$word' is greater than 10 characters\n";
}
}Warning
Without going into too much detail, let’s just say that you should be cautious about using given/when for the time being. Here’s a blog post to explain a bit more (though it’s probably a bit advanced for you at this point):
http://blogs.perl.org/users/komarov/2011/09/givenwhen-and-lexical.html
If you wish to avoid bugs, you can usually replace the when with a for and it works just fine:
for ($number) {
when ($_ < 0) {
print "The number is negative";
}
when ($_ > 0) {
print "The number is positive";
}
default {
print "The number is 0";
}
}Just make sure that you don’t use a variable name with the for loop to ensure you’re setting the $_ variable.
To understand more about given/when, you can read perldoc persyn if you have version 5.10.0 or better. Also, if you want to use given/when without the other useful features of newer versions of Perl, see perldoc feature.
The Switch Module
Don’t use this module.
Added in Perl version 5.7.2 and removed in Perl version 5.13.1, the Switch module allowed you to write switch statements in Perl:
use Switch;
switch ($val) {
case 1 { print "number 1" }
case "a" { print "string a" }
case [1..10,42] { print "number in list" }
case (\@array) { print "number in list" }
case /\w+/ { print "pattern" }
case qr/\w+/ { print "pattern" }
case (\%hash) { print "entry in hash" }
case (\&sub) { print "arg to subroutine" }
else { print "previous case not true" }
}Unfortunately, this was implemented as something known as a “source filter”. Source filters rewrite your code before it’s compiled, but due to the heuristic nature of Perl’s parser, they’re considered extremely unreliable. In fact, the Switch module, which generally being useful, has a variety of bugs and limitations that, while obscure, are nonetheless difficult to work around.
Switch was eventually removed from the Perl core as its functionality is replaced with the given/when statement. Your author strongly recommends that you do not use the Switch module.
Summary
In this chapter you’ve learned the basics of control flow in Perl. The if statement, for and while loops make up the bulk of control flow for Perl, though there are many variations. Control flow is what allows your programs to make decisions about what to do and how to do it.
Exercises
Q: | 1. What does the following line of code do? How might you improve it? print for 1..10; |
Q: | 2. The following code has a syntax error. Fix it. my $temperature = 22;
print $temperature < 15? "Too cold!\n"
: $temperature > 35? "Too hot!\n"; |
Q: | 3. Create an array called |
Q: | 4. For those new to Perl but who have experience with languages such as Java or C, they might write the following bit of code. However, it has a logic error. Explain what the logic error is and what the programmer might have done to see the logic error when running the code. Then rewrite the code to be simpler. my @array = qw( fee fie foe fum );
my $num_elements = @array;
foreach ( my $i = 0; $i <= $num_elements; $i++ ) {
print "$array[$i]\n";
} |
Q: | 5. You’re writing a game and you want to randomly generate a character’s statistics for strength, intelligence and dexterity. Each statistic is determined by summing the values of 2 rolls of a six sided die. For example, if you are determining the character’s strength and you roll the die twice and get the values 2 and 6, the characters strength is 8 (2 + 6). Write the code to generate a new character. Remember that the code to simulate one roll of a six sided die is my %stat_for = (
strength => undef,
intelligence => undef,
dexterity => undef,
);
# add your code here
print <<"END_CHARACTER";
Strength: $stat_for{strength}
Intelligence: $stat_for{intelligence}
Dexterity: $stat_for{dexterity}
END_CHARACTERFor extra credit, imagine that the character is considered “exceptional” and you don’t want to allow any stats with a value less than 6. Hint: this is one case where a |
What You Learned in This Chapter
Topic | Key Concepts |
|---|---|
if/elsif/else | Switching on true/false values |
?: | The ternary operator: a handy shortcut for if/else |
Foreach | The basic way to iterate over lists |
While/until | Looping using an iterator or Boolean condition |
Last/next/redo/continue | Loop control statements |
Statement modifiers | Conditions or loops put after the statement |
Given/when | Perl’s “switch” statement |
Answers to exercises
What does the following line of code do? How might you improve it?
print for 1..10;
That code will print
12345678910. If you want those numbers on separate lines, you can do this:print "$_\n" for 1..10;
The following code has a syntax error. Fix it.
my $temperature = 22; print $temperature < 15? "Too cold!\n" : $temperature > 35? "Too hot!\n";Remember that the ternary operator requires both the “if true” and “if false” conditions, but we’re missing that for the second ternary operator.
my $temperature = 22; print $temperature < 15? "Too cold!\n" : $temperature > 35? "Too hot!\n" : "Just right!\n";Create an array called
@numbersand assign some numbers to it. Write the code to print the average value of the numbers.As usual, there are many ways of doing this. If you get the correct answer, your code will still probably look different from this answer. Here’s one way.
my @numbers = qw< 3 9 0 7 8 >; my $total = 0; $total += $_ foreach @numbers; my $average = $total / @numbers; print "The numbers are: @numbers\n"; print "The average is $average\n";
Remember that an array in scalar context returns the number of elements in the array, so
$total/@numbersevaluates to27 / 5.For those new to Perl but who have experience with languages such as Java or C, they might write the following bit of code. However, it has a logic error. Explain what the logic error is and what the programmer might have done to see the logic error when running the code. Then rewrite the code to be simpler.
my @array = qw( fee fie foe fum ); my $num_elements = @array; foreach ( my $i = 0; $i <= $num_elements; $i++ ) { print "$array[$i]\n"; }The logic error is in the loop terminating condition of
$i <= $num_elements. This is called an “off by one” error (because the final value of$iwill be one greater than the final index in@array). This is a common problem with C-style for loops. Changing that to$i < $num_elementsfixes the problem.The programmer would see the error at runtime by adding a
use warningsstatement at the beginning of their program. You’ll get a warning similar to:Use of uninitialized value within @array in concatenation (.) or string
However, the above code is much simpler if you use a Perl-style
for/foreachloop (remember that they’re exactly the same thing).my @array = qw( fee fie foe fum ); for my $word (@array) { print "$word\n"; }The Perl-style loop not only avoids “off by one” errors, but they execute faster, too.
You’re writing a game and you want to randomly generate a character’s statistics for strength, intelligence and dexterity. Each statistic is determined by summing the values of 2 rolls of a six sided die. For example, if you are determining the character’s strength and you roll the die twice and get the values 2 and 6, the characters strength is 8 (2 + 6). Write the code to generate a new character. Remember that the code to simulate one roll of a six sided die is
1 + int(rand(6))(from Chapter 4). We use a “heredoc” (see Chapter 3, Variables) to print the character’s statistics.my %stat_for = ( strength => undef, intelligence => undef, dexterity => undef, ); # add your code here print <<"END_CHARACTER"; Strength: $stat_for{strength} Intelligence: $stat_for{intelligence} Dexterity: $stat_for{dexterity} END_CHARACTERFor the “add your code here” bit, you might write something like this:
foreach my $stat (keys %stat_for) { my $random = 2 + int(rand(6)) + int(rand(6)); $stat_for{$stat} = $random; }For extra credit, imagine that the character is considered “exceptional” and you don’t want to allow any stats with a value less than 6. Hint: this is one case where a
redo()statement can come in handy. Here’s the full code:my %stat_for = ( strength => undef, intelligence => undef, dexterity => undef, ); foreach my $stat (keys %stat_for) { my $random = 2 + int(rand(6)) + int(rand(6)); redo if $random < 6; $stat_for{$stat} = $random; } print <<"END_CHARACTER"; Strength: $stat_for{strength} Intelligence: $stat_for{intelligence} Dexterity: $stat_for{dexterity} END_CHARACTER





Add a comment



Add a comment