-
Notifications
You must be signed in to change notification settings - Fork 0
/
012-control-flow.pl
executable file
·714 lines (590 loc) · 21 KB
/
012-control-flow.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
#!/usr/bin/env perl
# REF: http://modernperlbooks.com/books/modern_perl_2016/03-perl-language.html#Q29udHJvbEZsb3c
use 5.034;
use warnings;
use autodie;
use feature 'say';
use Test::More;
# Perl's basic control flow is straightforward.
# Program execution starts at the beginning
# (the first line of the file executed) and continues to the end:
{
say 'At start';
say 'In middle';
say 'At end';
}
# Perl's control flow directives change the order of what happens next in the program.
# ================================
# Branching Directives
# ================================
# The (if) directive performs the associated action
# only when its conditional expression evaluates to a true value:
{
my $name = 'Alice';
say 'Hello, Bob!' if $name eq 'Bob';
}
# This postfix form is useful for simple expressions.
# Its block form groups multiple expressions into a unit which evaluates to a single boolean value:
# {
# if ($name eq 'Bob') {
# say 'Hello, Bob!';
# found_bob();
# }
# }
# The conditional expression may consist of multiple subexpressions
# which will be coerced to a boolean value:
# {
# if ($name eq 'Bob' && not greeted_bob()) {
# say 'Hello, Bob!';
# found_bob();
# }
# }
# The block form requires parentheses around its condition,
# but the postfix form does not.
# In the postfix form, adding parentheses can clarify the intent of the code
# at the expense of visual cleanliness:
# {
# greet_bob() if ($name eq 'Bob' && not greeted_bob());
# }
# The (unless) directive is the negated form of (if).
# Perl will perform the action
# when the conditional expression evaluates to a false value:
{
my $name = 'Alice';
say "You're not Bob!" unless $name eq 'Bob';
}
# Like (if), (unless) also has a block form,
# though many programmers avoid it due to its potential for confusion:
# {
# unless (is_leap_year() and is_full_moon()) {
# frolic();
# gambol();
# }
# }
# (unless) works very well for postfix conditionals,
# NOTE: especially parameter validation in functions (Postfix Parameter Validation):
sub frolic {
# do nothing without parameters
return unless @_;
for my $chant (@_) { ... }
}
# The block forms of (if) and (unless) both support the (else) directive,
# which provides a block to execute
# when the conditional expression does not evaluate to the appropriate value:
sub demo_if_else {
my $name = 'Alice';
if ( $name eq 'Bob' ) {
say 'Hi, Bob!';
greet_user();
}
else {
say "I don't know you.";
shun_user();
}
}
# (else) blocks allow you to rewrite (if) and (unless) conditionals in terms of each other:
sub demo_unless_else {
my $name = 'Alice';
unless ( $name eq 'Bob' ) {
say "I don't know you.";
shun_user();
}
else {
say 'Hi, Bob!';
greet_user();
}
}
# However, the implied double negative of using unless with an else block can be confusing.
# This example may be the only place you ever see it.
# Just as Perl provides both (if) and (unless)
# to allow you to phrase your conditionals in the most readable way,
# Perl has both positive and negative conditional operators:
sub demo_ne {
my $name = 'Alice';
if ( $name ne 'Bob' ) {
say "I don't know you.";
shun_user();
}
else {
say 'Hi, Bob!';
greet_user();
}
}
# ... though the double negative implied by the presence of the (else) block may be difficult to read.
# Use one or more (elsif) directives to check multiple and mutually exclusive conditions:
sub demo_elsif {
my $name = 'Alice';
if ( $name eq 'Robert' ) {
say 'Hi, Bob!';
greet_user();
}
elsif ( $name eq 'James' ) {
say 'Hi, Jim!';
greet_user();
}
elsif ( $name eq 'Armando' ) {
say 'Hi, Mando!';
greet_user();
}
else {
say "You're not my uncle.";
shun_user();
}
}
# An unless chain may also use an (elsif) block, but good luck deciphering that.
# Perl supports neither (elseunless) nor (else if).
# Larry prefers (elsif) for aesthetic reasons,
# as well the prior art of the Ada programming language:
# {
# if ($name eq 'Rick') {
# say 'Hi, cousin!';
# }
#
# # warning; syntax error
# else if ($name eq 'Kristen') {
# say 'Hi, cousin-in-law!';
# }
# }
# ================================
# The Ternary Conditional Operator
# ================================
# An interesting, though obscure, idiom uses the ternary conditional to select between alternative variables,
# not only values:
# {
# push @{ rand() > 0.5 ? \@red_team : \@blue_team }, Player->new;
# }
# Besides allowing you to avoid potentially expensive computations,
# short circuiting can help you to avoid errors and warnings,
# as in the case where using an undefined value might raise a warning:
sub demo_short_circuiting {
my $bbq;
if ( defined $bbq and $bbq eq 'brisket' ) { ... }
}
# ================================
# Context for Conditional Directives
# ================================
# Perl has neither a single true value nor a single false value. Any number which evaluates to 0 is false.
# The empty string ('') and '0' evaluate to a false value, but the strings '0.0', '0e0', and so on do not.
# The idiom '0 but true' evaluates to 0 in numeric context—
# but true in boolean context due to its string contents.
# Both the empty list and undef evaluate to a false value.
# Empty arrays and hashes return the number 0 in scalar context,
# so they evaluate to a false value in boolean context.
# An array which contains a single element—even undef—
# evaluates to true in boolean context.
# A hash which contains any elements—even a key and a value of undef—
# evaluates to a true value in boolean context.
# ================================
# Looping Directives
# ================================
# This example uses the range operator to produce a list of integers from one to ten inclusive.
# The foreach directive loops over them,
# setting the topic variable $_ (The Default Scalar Variable) to each in turn.
# Perl executes the block for each integer and, as a result, prints the squares of the integers.
sub demo_for_looping {
# Square the first ten positive integers.
foreach ( 1 .. 10 ) {
say "$_ * $_ = ", $_ * $_;
}
# Like if and unless, this loop has a postfix form:
say "$_ * $_ = ", $_ * $_ for 1 .. 10;
# A for loop may use a named variable instead of the topic:
for my $i ( 1 .. 10 ) {
say "$i * $i = ", $i * $i;
}
}
# Many Perl programmers refer to iteration as foreach loops,
# but Perl treats the names foreach and for interchangeably.
# The parenthesized expression determines the type and behavior of the loop;
# the keyword does not.
# REF: https://stackoverflow.com/questions/2279471/whats-the-difference-between-for-and-foreach-in-perl/2279497#2279497
# When a for loop uses an iterator variable,
# the variable is scoped to the block within the loop.
# Perl will set this lexical to the value of each item in the iteration.
# Perl will not modify the topic variable ($_).
# If you have declared a lexical $i in an outer scope, its value will persist outside the loop:
sub demo_for_looping_scope_1 {
my $i = 'cow';
for my $i ( 1 .. 10 ) {
say "$i * $i = ", $i * $i;
}
is $i, 'cow', 'Value preserved in outer scope';
}
# This localization occurs even if you do not redeclare the iteration variable as a lexical,
# but keep the habit of declaring iteration values as lexicals:
sub demo_for_looping_scope_2 {
my $i = 'horse';
for $i ( 1 .. 10 ) {
say "$i * $i = ", $i * $i;
}
is $i, 'horse', 'Value preserved in outer scope';
}
# ================================
# Iteration and Aliasing
# ================================
# The for loop aliases the iterator variable to the values in the iteration
# such that any modifications to the value of the iterator modifies the value in place:
sub demo_for_looping_iterator_value_1 {
my @nums = 1 .. 10;
$_**= 2 for @nums;
is $nums[0], 1, '1 * 1 is 1';
is $nums[1], 4, '2 * 2 is 4';
# ...
is $nums[9], 100, '10 * 10 is 100';
}
# This aliasing also works with the block style for loop:
sub demo_for_looping_iterator_value_2 {
my @nums = 1 .. 10;
for my $num (@nums) {
$num**= 2;
}
}
# ... as well as iteration with the topic variable:
sub demo_for_looping_iterator_value_3 {
my @nums = 1 .. 10;
for (@nums) {
$_**= 2;
}
}
# You cannot use aliasing to modify constant values, however.
# Perl will produce an exception about modification of read-only values.
# {
# $_++ and say for qw( Huex Dewex Louid );
# }
# You may occasionally see the use of for with a single scalar variable:
# {
# for ($user_input) {
# s/\A\s+//; # trim leading whitespace
# s/\s+\z//; # trim trailing whitespace
#
# $_ = quotemeta; # escape non-word characters
# }
# }
# This idiom (Idioms) uses the iteration operator for its side effect of aliasing $_,
# though it's clearer to operate on the named variable itself.
# ================================
# Iteration and Scoping
# ================================
# The topic variable's iterator scoping has a subtle gotcha.
# Consider a function topic_mangler() which modifies $_ on purpose.
# If code iterating over a list called topic_mangler() without protecting $_,
# you'd have to spend some time debugging the effects:
# {
# for (@values) {
# topic_mangler();
# }
#
# sub topic_mangler {
# s/foo/bar/;
# }
# }
# The substitution in topic_mangler() will modify elements of @values in place.
# If you must use $_ rather than a named variable,
# use the topic aliasing behavior of for:
sub topic_mangler {
# was $_ = shift;
for (shift) {
s/foo/bar/;
s/baz/quux/;
return $_;
}
}
# Alternately, use a named iteration variable in the for loop.
# That's almost always the right advice.
# ================================
# The C-Style For Loop
# ================================
# All three subexpressions are optional.
sub demo_c_style_for_looping {
for (
# loop initialization subexpression
say 'Initializing', my $i = 0 ;
# conditional comparison subexpression
say "Iteration: $i" and $i < 10 ;
# iteration ending subexpression
say 'Incrementing ' . $i++
)
{
say "$i * $i = ", $i * $i;
}
}
# You must explicitly assign to an iteration variable in the looping construct,
# as this loop performs neither aliasing nor assignment to the topic variable.
# While any variable declared in the loop construct is scoped to the lexical block of the loop,
# Perl will not limit the lexical scope of a variable declared outside of the loop construct:
sub demo_c_style_for_looping_scope {
my $i = 'pig';
for ( $i = 0 ; $i <= 10 ; $i += 2 ) {
say "$i * $i = ", $i * $i;
}
isnt $i, 'pig', '$i overwritten with a number';
}
# Note the lack of a semicolon after the final subexpression
# as well as the use of the comma operator and low-precedence and;
# this syntax is surprisingly finicky.
# When possible, prefer the foreach-style loop to the for loop.
# ================================
# While and Until
# ================================
# Unlike the iteration foreach-style loop,
# the while loop's condition has no side effects.
# If @values has one or more elements,
# this code is also an infinite loop,
# because every iteration will evaluate @values in scalar context to a non-zero value
# and iteration will continue:
sub demo_infinite_while_looping {
# my @values = qw( one two three );
my @values = qw();
while (@values) {
say $values[0];
}
}
# To prevent such an infinite while loop,
# use a destructive update of the @values array by modifying the array within each iteration:
sub demo_destructive_update {
my @values = qw( 1 0 2 4 );
while (@values) {
my $value = shift @values;
say $value;
}
}
# NOTE: Modifying @values inside of the while condition check also works,
# but it has some subtleties related to the truthiness of each value.
# This loop will exit
# as soon as the assignment expression used as the conditional expression evaluates to a false value.
# If that's what you intend, add a comment to the code.
sub demo_subtleties {
my @values = qw( 1 0 2 4 );
while ( my $value = shift @values ) {
say $value;
}
}
# The canonical use of the while loop is to iterate over input from a filehandle:
# {
# while (<$fh>) {
# # remove newlines
# chomp;
# ...
# }
# }
# Perl interprets this while loop as if you had written:
# {
# while (defined($_ = <$fh>)) {
# # remove newlines
# chomp;
# ...
# }
# }
# NOTE: Without the implicit defined,
# any line read from the filehandle which evaluated to a false value in a scalar context—
# a blank line or a line which contained only the character 0—would end the loop.
# The readline (<>) operator returns an undefined value only when it has reached the end of the file.
# Use a do block to group several expressions into a single unit:
sub greet_people {
do {
say 'What is your name?';
my $name = <>;
chomp $name;
say "Hello, $name!" if $name;
} until (eof);
}
# A do block parses as a single expression which may contain several expressions.
# Unlike the while loop's block form,
# the do block with a postfix while or until will execute its body at least once.
# This construct is less common than the other loop forms, but very powerful.
# ================================
# Loops within Loops
# ================================
# Novices commonly exhaust filehandles accidentally while nesting foreach and while loops:
sub demo_buggy_filehandle {
use autodie 'open';
open my $fh, '<', '';
my @prefixes = qw(John Paul Ringo);
for my $prefix (@prefixes) {
# DO NOT USE; buggy code
while (<$fh>) {
say $prefix, $_;
}
}
}
# !!! Opening the filehandle outside of the for loop
# leaves the file position unchanged between each iteration of the for loop.
# On its second iteration,
# the while loop will have nothing to read (the (readline) will return a false value).
# You can solve this problem in many ways;
# re-open the file inside the for loop (wasteful but simple),
# slurp the entire file into memory (works best with small files),
# or (seek) the filehandle back to the beginning of the file for each iteration:
sub demo_seek {
open my $fh, '<', '';
my @prefixes = qw(John Paul Ringo);
for my $prefix (@prefixes) {
while (<$fh>) {
say $prefix, $_;
}
seek $fh, 0, 0;
}
}
# ================================
# Loop Control
# ================================
# The (next) statement restarts the loop at its next iteration.
# Use it when you've done everything you need to in the current iteration.
# To loop over lines in a file and skip everything that starts with the comment character #:
sub demo_next {
open my $fh, '<', '';
while (<$fh>) {
next if /\A#/;
...;
}
}
# The (last) statement ends the loop immediately.
# To finish processing a file once you've seen the ending token, write:
sub demo_last {
open my $fh, '<', '';
while (<$fh>) {
next if /\A#/;
last if /\A__END__/;
...;
}
}
# The (redo) statement restarts the current iteration without evaluating the conditional again.
# This can be useful in those few cases where you want to modify the line you've read in place,
# then start processing over from the beginning without clobbering it with another line.
# To implement a silly file parser that joins lines which end with a backslash:
sub demo_redo {
open my $fh, '<', '';
while ( my $line = <$fh> ) {
chomp $line;
# match backslash at the end of a line
if ( $line =~ s{\\$}{} ) { # BUG: There's a bug in syntax highlighting.
$line .= <$fh>;
redo;
}
...;
}
}
# Nested loops can be confusing, especially with loop control statements.
# If you cannot extract inner loops into named functions,
# use loop labels to clarify your intent:
sub demo_loop_label {
open my $fh, '<', '';
my @prefixes = qw(John Paul Ringo);
LINE:
while (<$fh>) {
chomp;
PREFIX:
for my $prefix (@prefixes) {
next LINE unless $prefix;
say "$prefix: $_";
# next PREFIX is implicit here
}
}
}
# ================================
# Continue
# ================================
# The continue construct behaves like the third subexpression of a for loop;
# Perl executes any continue block before subsequent iterations of a loop,
# whether due to normal loop repetition or premature re-iteration from next.
# You may use it with a while, until, when, or for loop.
# Examples of continue are rare,
# but it's useful any time you want to guarantee that something occurs with every iteration of the loop,
# regardless of how that iteration ends:
# {
# while ($i < 10 ) {
# next unless $i % 2;
# say $i;
# }
# continue {
# say 'Continuing...';
# $i++;
# }
# }
# Note that a continue block does not execute when control flow leaves a loop due to last or redo.
# ================================
# Switch Statements
# ================================
# Perl 5.10 introduced a new construct named given as a Perlish switch statement.
# It didn't quite work out; given is still experimental, if less buggy in newer releases.
# Avoid it unless you know exactly what you're doing.
# If you need a switch statement, use (for) to alias the topic variable ($_)
# and (when) to match it against simple expressions
# with smart match (Smart Matching) semantics.
# To write the Rock, Paper, Scissors game:
sub rock_paper_scissors {
my @options = ( \&rock, \&paper, \&scissors );
my $confused = "I don't understand your move.";
do {
say "Rock, Paper, Scissors! Pick one: ";
chomp( my $user = <STDIN> );
my $computer_match = $options[ rand @options ];
$computer_match->( lc($user) );
} until (eof);
sub rock {
print "I chose rock. ";
for (shift) {
when (/paper/) { say 'You win!' }
when (/rock/) { say 'We tie!' }
when (/scissors/) { say 'I win!' }
default { say $confused }
}
}
sub paper {
print "I chose paper. ";
for (shift) {
when (/paper/) { say 'We tie!' }
when (/rock/) { say 'I win!' }
when (/scissors/) { say 'You win!' }
default { say $confused }
}
}
sub scissors {
print "I chose scissors. ";
for (shift) {
when (/paper/) { say 'I win!' }
when (/rock/) { say 'You win!' }
when (/scissors/) { say 'We tie!' }
default { say $confused }
}
}
}
# ================================
# Tailcalls
# ================================
# A tailcall occurs when the last expression within a function is a call to another function.
# The outer function's return value becomes the inner function's return value:
sub log_and_greet_person {
my $name = shift;
log("Greeting $name");
return greet_person($name);
}
# Returning from greet_person() directly to the caller of log_and_greet_person()
# is more efficient than
# returning to log_and_greet_person() and then from log_and_greet_person().
# Returning directly from greet_person() to the caller of log_and_greet_person() is a tailcall optimization.
# Heavily recursive code (Recursion)—especially mutually recursive code—can consume a lot of memory.
# Tailcalls reduce the memory needed for internal bookkeeping of control flow
# and can make expensive algorithms cheaper.
# Unfortunately, Perl does not automatically perform this optimization,
# so you have to do it yourself when it's necessary.
# The builtin (goto) operator has a form
# which calls a function as if the current function were never called,
# essentially erasing the bookkeeping for the new function call.
# The ugly syntax confuses people who've heard "Never use (goto)", but it works:
sub log_and_greet_person_using_goto {
my ($name) = @_;
log("Greeting $name");
goto &greet_person;
}
# NOTE: This example has two important characteristics.
# First, (goto &function_name) or (goto &$function_reference) requires the use of the function sigil (&)
# so that the parser knows to perform a tailcall instead of jumping to a label.
# Second, this form of function call passes the contents of @_ implicitly to the called function.
# You may modify @_ to change the passed arguments if you desire.
# This technique is most useful when you want to hijack control flow
# to get out of the way of other functions inspecting caller
# (such as when you're implementing special logging or some sort of debugging feature),
# or when using an algorithm which requires a lot of recursion.
# Remember it if you need it, but feel free not to use it.
done_testing();