Perl Part I · Module 8 · Conditional Statements
Short-Circuit Evaluation and Defaults
Chapter 3.8 established what && and || do as operators, including that they stop evaluating as soon as the answer is certain. This chapter is about the two things that behaviour is actually for. The first is a guard: because the right-hand side may never run, you can put a cheap existence check on the left and something that would be wrong or expensive on the right, and the wrong thing simply never happens. The second is a default: because these operators return an operand rather than a boolean, a chain of them picks the first usable value out of several sources. Both are one-liners, both are everywhere in working Perl, and the guard prevents a failure whose symptom is a correct-looking answer sitting on top of corrupted data.
Foundation16 min readPerlPart IControl FlowConditionals
Part I · Module 8 · Chapter 8.5 · Short-Circuit Evaluation and Defaults
1. The Right-Hand Side May Never Run
Chapter 3.8 §3 introduced this. The proof is a side effect rather than a return value, so it is worth counting calls.
use strict;
use warnings;
my $calls = 0;
sub probe { $calls++; return 1 }
my $r1 = 0 && probe();
my $r2 = 1 || probe();
printf "after 0 && probe() and 1 || probe() : probe called %d time(s)\n", $calls;
my $r3 = 1 && probe();
my $r4 = 0 || probe();
printf "after 1 && probe() and 0 || probe() : probe called %d time(s)\n", $calls;after 0 && probe() and 1 || probe() : probe called 0 time(s)
after 1 && probe() and 0 || probe() : probe called 2 time(s)Zero calls across the first two lines. 0 && ... already knows the answer is false, and 1 || ... already knows it is true, so neither looked at its right-hand side at all.
The next two lines each had to look, and the counter went to 2.
| Expression | Right side runs? |
|---|---|
FALSE && anything | no — the answer is already false |
TRUE && X | yes |
TRUE || anything | no — the answer is already true |
FALSE || X | yes |
This is a guarantee, not an optimisation. Perl's documentation specifies it, so you can rely on the right-hand side being skipped — which is what makes everything else in this chapter possible.
2. The Guard
Put the cheap check on the left and the thing that must not happen prematurely on the right.
use strict;
use warnings;
my %r = ( alu => { slack => -0.12 } );
printf "before : %d key(s) [%s]\n", scalar(keys %r), join(" ", sort keys %r);
for my $b (qw(alu fifo regf)) {
my $has = exists $r{$b} && exists $r{$b}{slack};
printf " %-5s %s\n", $b, ($has ? "has slack" : "no slack");
}
printf "after : %d key(s) [%s]\n", scalar(keys %r), join(" ", sort keys %r);before : 1 key(s) [alu]
alu has slack
fifo no slack
regf no slack
after : 1 key(s) [alu]Three blocks probed, correct answers for all three, and the hash still has one key. Now the same program with the guard removed.
use strict;
use warnings;
my %r = ( alu => { slack => -0.12 } );
printf "before : %d key(s) [%s]\n", scalar(keys %r), join(" ", sort keys %r);
for my $b (qw(alu fifo regf)) {
my $has = exists $r{$b}{slack}; # NO GUARD
printf " %-5s %s\n", $b, ($has ? "has slack" : "no slack");
}
printf "after : %d key(s) [%s]\n", scalar(keys %r), join(" ", sort keys %r);before : 1 key(s) [alu]
alu has slack
fifo no slack
regf no slack
after : 3 key(s) [alu fifo regf]3. Guarding Something Expensive
The same shape protects work rather than data.
use strict;
use warnings;
my $opens = 0;
sub slow_read {
my ($path) = @_;
$opens++;
return "contents of $path";
}
my $path;
my $data1 = defined($path) && slow_read($path);
printf "undefined path : slow_read called %d time(s)\n", $opens;
$path = "report.rpt";
my $data2 = defined($path) && slow_read($path);
printf "defined path : slow_read called %d time(s), got [%s]\n", $opens, $data2;undefined path : slow_read called 0 time(s)
defined path : slow_read called 1 time(s), got [contents of report.rpt]With $path undefined, slow_read was never called — so it never had to handle an undefined argument, and no warning was produced for passing one. With $path set, it ran once.
Note what $data1 holds in the first case: && returns an operand, as Chapter 3.8 §4 established, so it holds the false left-hand side rather than a boolean. For a guard that is fine, because you are about to test it. If you need the value itself, use an if and assign inside it — relying on && to both guard and deliver a value is a readability trade that usually loses.
4. The Default
Because these operators return an operand, a chain of them selects a value.
use strict;
use warnings;
my %conf = ( retries => 0, timeout => undef );
printf "retries with || : %s\n", ($conf{retries} || 3);
printf "retries with // : %s\n", ($conf{retries} // 3);
printf "timeout with // : %s\n", ($conf{timeout} // 45);
printf "absent with // : %s\n", ($conf{jobs} // 8);retries with || : 3
retries with // : 0
timeout with // : 45
absent with // : 8|| got retries wrong. Someone configured zero retries deliberately, and || replaced it with 3 — because 0 is false, which is Chapter 8.1 §4's rule doing damage.
// asks whether the value is defined, not whether it is true, so a real 0 survives. It still supplies the default for a value that is undef and for a key that is absent entirely.
| Operator | Replaces | Use for |
|---|---|---|
|| | anything false — 0, "", "0", undef | a genuine "any falsy value means unset" |
// | only undef | almost every default you will write |
// requires Perl 5.10, which is from 2007 — old enough that this is not a portability concern on any machine you will meet.
Filling defaults in place
use strict;
use warnings;
my %conf = ( retries => 0 );
$conf{retries} //= 3;
$conf{timeout} //= 45;
printf "retries=%s timeout=%s keys=%d [%s]\n",
$conf{retries}, $conf{timeout}, scalar(keys %conf), join(" ", sort keys %conf);retries=0 timeout=45 keys=2 [retries timeout]//= does both jobs in one line: it left the deliberate 0 alone and created the missing key. That is the standard way to apply a defaults table at the top of a script, and it is why Chapter 7.7 §2 used $h{$_} //= [] for @all; to make sure every key appears in a report.
Had that been ||=, retries would now be 3.
5. A Chain of Sources
use strict;
use warnings;
my %cli = ();
my %env = ( timeout => 90 );
my %def = ( timeout => 45, retries => 3 );
for my $k (qw(timeout retries jobs)) {
my $v = $cli{$k} // $env{$k} // $def{$k} // "(none)";
printf "%-8s -> %s\n", $k, $v;
}timeout -> 90
retries -> 3
jobs -> (none)One expression, a precedence order, and a final fallback. timeout came from the environment because the command line did not set it; retries fell through to the defaults; jobs exists nowhere and landed on the literal.
This is the idiomatic way to express configuration precedence, and the reason it reads well is that the chain is written in priority order — command line beats environment beats defaults. Each // is "and if that was not set, try".
6. Why This Matters in Engineering Work
The guard protects your own data from your own check. Probing a set of block names for an optional field, without a guard, adds every name you probed. A script that then reports how many blocks it found reports the number it invented. The three answers were right; the summary was not.
The default is how every tool script handles configuration. A table of defaults applied with //=, overridden by the environment, overridden by the command line — three lines, in priority order, and a deliberately configured 0 survives all of them.
And || for a default is a real bug, not a style preference. Zero retries, zero violations, zero slack and an empty string are all legitimate configured values in this trade. || silently replaces every one of them. The habit is to reach for // first and only use || when you have decided that any falsy value should count as unset.
7. Common Mistakes
exists $h{a}{b} with no guard. It creates $h{a}. Guard outward-in with &&.
Trusting a top-level key count after probing optional fields. The probes may have added keys — measured, 1 became 3.
|| for a default. It replaces a legitimate 0 or "". Use //.
||= to fill a defaults table. Same problem; use //=.
Relying on && to deliver a value as well as guard. It returns an operand, so the false case gives you the left side. Use an if.
Expecting a // chain to respect an explicit undef. It cannot distinguish absent from undefined. Use exists.
$x ? $x : "default". Evaluates $x twice and mishandles 0. Use $x // "default".
8. Exercise
Exercise 1 — count the calls. Build a probe subroutine that increments a counter. Run 0 && probe(), 1 || probe(), 1 && probe() and 0 || probe(), printing the counter after the first two and after the last two. Confirm you get 0 then 2.
Exercise 2 — the guard pair. Write §2's two programs. Confirm the three middle answers are identical, then compare the final key counts. State in one sentence what the unguarded version did and why nothing warned.
Exercise 3 — the downstream damage. After running the unguarded version, print scalar(keys %r) and loop over sort keys %r printing each block. Describe what a report built from that would claim.
Exercise 4 — guarding a call. Write a subroutine that increments a counter and returns a string. Guard it with defined($path) && for an undefined and then a defined $path, printing the counter each time. Then print what the guarded expression returned in the undefined case and explain it.
Exercise 5 — || against //. Build a hash with retries => 0 and timeout => undef. Print both operators against retries, and // against timeout and an absent key. Say which row is the bug and who it hurts.
Exercise 6 — in place. Apply //= for retries and timeout to a hash containing only retries => 0. Print both values, the key count and the key names. Then redo it with ||= and report what changed.
Exercise 7 — the chain. Build three hashes for command line, environment and defaults as in §5, and resolve timeout, retries and a key that exists nowhere. Then move timeout into the command-line hash and confirm the resolved value changes.
Exercise 8 — absent against undefined. Build one hash with a key absent and one with the key present but undef. Resolve both through a // chain and confirm they behave the same. Then rewrite with the exists ladder and show they now differ.
9. Summary
&&and||are guaranteed to skip the right-hand side when the left already decides the answer — measured by call count, 0 then 2.- The guard puts a cheap check on the left:
exists $h{a} && exists $h{a}{b}, outward-in, at any depth. - Without the guard, the probe creates what it asked about. Measured: identical three-line output, empty STDERR, exit 0 in both cases, and 1 top-level key against 3.
- Anything that counts or iterates the top level is then wrong, which is how a correct-looking summary reports blocks that were never in the data.
- The same shape guards expensive or invalid work —
defined($path) && slow_read($path)called the reader zero times for an undefined path. &&returns an operand, not a boolean, so do not rely on it to deliver a value and guard at once.//is the default operator;||is not.||replaces a deliberate0,""or"0". Measured:0 || 3gave 3,0 // 3gave 0.//=applies a default in place — it leaves a real0alone and creates a missing key.- A
//chain expresses configuration precedence in priority order, with a literal fallback at the end. - A
//chain cannot distinguish absent from explicitlyundef. Use anexistsladder when that distinction is real.
Chapter 8.6 closes the module with the decision that all five of these forms are usually assembled to make: a verdict. It measures what happens when that verdict has only two possible answers and the input is unusable.
Where this fits
Part of the Perl curriculum.
