[Lexical $_
was removed in v5.24]
Perl 5.10 introduced the given-when
feature, a fancier version of the C switch
feature. However, it was poorly designed and tested and depended on two other dubious features, the lexical $_
and smart-matching. Parts of this feature are salvageable, but you should avoid the literal given
(and probably the lexical $_
and the smart matching, but I’ll skip those for this Item).
First, remember what the given-when
purports to do. It takes a value and uses it within its block:
use v5.10; given ($s) { ...; }
The given
assigns the value in its variable to a lexical version of $_
then goes through the code in its block:
do { my $_ = $s; ...; }
The $_
is important only for use in the short-cut smart matches where the when
assume a first argument of $_
. The feature allows you to implicitly use not only an operand but also an operator:
use v5.10; given ($s) { when( @array ) { ... } when( %hash ) { ... } }
Without that, you would repeatedly type the smart match operator and it’s left operand:
use v5.10; given ($s) { when( $_ ~~ @array ) { ... } when( $_ ~~ %hash ) { ... } }
This makes given
a topicalizer. You can think of $_
as the topic, just like foreach
. Statements in the block operate on the topic as their default argument. However, it was implemented poorly (and differently) than foreach
.
Consider this example, where inside the given
you match against any character (except a newline) in $_
. The match is in scalar context with the /g
flag, so it makes a match (if it can), and remembers where it made that match so it can start there the next time. Each scalar variable remembers its own last-matched position in the string:
use v5.10; my $s = "abc"; try_given($s) for 1 .. 3; try_do($s) for 1 .. 3; sub try_given { my $s = shift; state $n = 0; given ($s) { /./g; printf "%d. given: pos=%d\n", ++$n, pos; } } sub try_do { my $s = shift; state $n = 0; do { my $_ = $s; /./g; printf "%d. given: pos=%d\n", ++$n, pos; }; }
If the $_
in the given
was implemented correctly, each time you called try_given
, you’d get a fresh variable. Whatever you did to the previous version of $_
wouldn’t matter because those effects disappear at the end of its scope (this was fixed in v5.16). That’s not what you see in the output though:
1. given: pos=1 2. given: pos=2 3. given: pos=3 1. given: pos=1 2. given: pos=1 3. given: pos=1
The “lexical” $_
in the given
isn’t really lexical. It’s more like a state
variable (but not really) in that parts of it persist across calls (compare this to Make exclusive flip-flop operators). Each use of given
has its own version of $_
. If you make another given
:
use v5.10; my $s = "abc"; try_given($s); try_given($s); try_given2($s); try_given($s); try_given2($s); try_given($s); sub try_given { my $s = shift; state $n = 0; given ($s) { /./g; printf "%d. given: pos=%d\n", ++$n, pos; } } sub try_given2 { my $s = shift; state $n = 0; given ($s) { /./g; printf "%d. given2: pos=%d\n", ++$n, pos; } }
The output shows that each use of given
has its own side effects:
1. given: pos=1 2. given: pos=2 1. given2: pos=1 3. given: pos=3 2. given2: pos=2 4. given: pos=0
There’s another problem though. Since $_
is lexical inside given
, it masks the value of all other uses of $_
in its lexical scope:
#!/usr/bin/perl use v5.10; use List::MoreUtils qw(any); { my $_ = 'abc'; my $any = any { say "my \$_ is $_"; $_ % 3 } 0 .. 10; } do { local $_ = 'abc'; my $any = any { say "local: \$_ is $_"; $_ % 3 } 0 .. 10; }
The output shows that the package version of $_
which any
, along with many other CPAN modules, expect just isn’t there. Instead of being the value of one of the elements of the input list, it’s the value you assigned to the lexical version. In the version with do
, $_
is localized (Item 43. Understand the difference between my and local). Inside the do
, the any
gets the values it expects:
my $_ is abc my $_ is abc my $_ is abc my $_ is abc my $_ is abc my $_ is abc my $_ is abc my $_ is abc my $_ is abc my $_ is abc my $_ is abc local: $_ is 0 local: $_ is 1
To be fair, this lexical $_
isn’t really a bug. The feature does exactly what it’s supposed to do. All by itself, it does what it promises and just like any other lexical variable should do. It’s just that it’s against the grain of all of the history of Perl leading up to it where we expect $_
to be a global variable that always lives in main::
and has a dynamic scope. It’s a bug only in that it is a really bad idea.
Because of these bugs, you shouldn’t use given
, ever. But, it turns out that you can avoid it without losing any functionality. If you substitute given
with foreach
or for
, you still get all the magic (literally) without the shortcomings:
use v5.10; my $s = "abc"; try_foreach($s) for 1 .. 3; sub try_foreach { my $s = shift; state $n = 0; foreach ($s) { /./g; printf "%d. foreach: pos=%d\n", ++$n, pos; } }
Now there is no carryover:
1. foreach: pos=1 2. foreach: pos=1 3. foreach: pos=1
The when
still works in the foreach
, too:
use v5.10; my $s = "abc"; foreach ( $s ) { when( /a/ ) { say 'Matched an a'; continue } say "Continuing..."; when( /b/ ) { say 'Matched a b', continue } when( [ qw(xyz abc) ] ) { say 'Matched in the array' } default { say 'Matched nothing' } }
You can see that the when
works the same, the continue
works the same, you can have interstitial code,
and the smart matching works (to the extent that it works):
Matched an a Continuing... Matched in the array
There’s a slight catch, however. You can only use when
if you don’t supply your own variable name. You have to use $_
. This bit of code won’t even compile:
foreach my $item ( @array ) { when( ... ) { ... } }
Things to remember
- Don’t use
given
because its version of$_
is broken. - Substitute
for
forgiven
.
Instead of replacing it with something like that and writing a lengthy article about it, why don’t you put this effort into fixing given/when.
Effort isn’t fungible. I have neither the desire nor the skill to fix given-when. I can, however, explain what’s wrong with it and offer alternatives or workarounds for it.
Thanks for the great article brian. There’s more than one way to do it but some ways are better than others. It is nice to find such clear and concise explanations of tricky topics.
Also, clear evaluations like this of problems with features provide the developement community valueable feedback and guidance on what went wrong and how to make things better. Questions about this came up this morning on perlmonks.org and I couldn’t find documentation for the ‘when’ statement when used with foreach. It was nice to find the question had been dealt with so well.
I would expected the penultimate loop to say also “Matched a b” after “Matched an a”. It does not say this because of comma before continue instead of semicolon. Was it a bug or intentional?