Use for() instead of given()

[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 for given.

5 thoughts on “Use for() instead of given()”

  1. 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.

    1. 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.

  2. 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.

    1. 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.

  3. 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?

Comments are closed.