given/when and lexical $_ ...

... or happy debugging!

TL;DR Upd: Use for instead of given, as Aristotle suggests.

Here are some code snippets to show what's wrong with given/when.

use 5.010;
use strict;
use List::MoreUtils qw/any/;

given ('test') {
    when ('test') {
        say "any" if any {$_ == 2} (2, 3);
    }   
}

The above code prints nothing. Ok, let's change any to grep to see if it works:

given ('test') {
    when ('test') {
        say "grep" if grep {$_ == 2} (2, 3);
    }   
}

It prints "grep", just as planned.

Ok, to be perfectly honest all we always use warnings, and the first piece of code actually has to be as the following:

use warnings;
given ('test') {
    when ('test') {
        say "any" if any {$_ == 2} (2, 3);
    }
}

We were lucky and got a warning!
Argument "test" isn't numeric in numeric eq (==) at ...

But wait, what?

given ('test') {
    when ('test') {
        any {say $_} (2, 3);
    }   
}

It prints "test". Huh.

Gonna read some perldoc. It says:
given(EXPR) will assign the value of EXPR to $_ within the lexical scope of the block, so it's similar to
do { my $_ = EXPR; ... }

Ok, now we undestand what's the difference between any and grep here: implementation of any makes use of anonymous functions. Actually we have just a closure capturing that poor lexical $_.
The last one to make it obvious:

my $sub;
given ('test') {
    when ('test') {
        $sub = sub {say;};
    }
}
$sub->();

Well, you get the idea.

4 Comments

Weird, compare this to for and 'it works'.

see:


use v5.14;
use List::MoreUtils qw/any/;

my $f=1;
say 'bare';
say(( any { $_ == 14} (10..15)) ? "any() works" : "any() broken");
say((grep { $_ == 14} (10..15)) ? "grep() works" : "grep() broken");

for ($f) {
say "\nfor";
say(( any { $_ == 14} (10..15)) ? "any() works" : "any() broken");
say((grep { $_ == 14} (10..15)) ? "grep() works" : "grep() broken");

when( 1 ) {
say(( any { $_ == 14} (10..15)) ? "any() works" : "any() broken");
say((grep { $_ == 14} (10..15)) ? "grep() works" : "grep() broken");
say 'done';
}
}
given ($f) {
say "\ngiven";
say(( any { $_ == 14} (10..15)) ? "any() works" : "any() broken");
say((grep { $_ == 14} (10..15)) ? "grep() works" : "grep() broken");

when( 1 ) {
say(( any { $_ == 14} (10..15)) ? "any() works" : "any() broken");
say((grep { $_ == 14} (10..15)) ? "grep() works" : "grep() broken");
}
}

If done right, given would have been the same as for, except for only accepting one value and imposing scalar context on it. The given we have now looks as if it’s that, but isn’t, and is broken for many edge cases. Just avoid given and use for instead.

Here's a work-around. A bit ugly but ...

when(1) { say $_ if any {our $_; $_ == 2} (2,3)}

The Anonymous function can specify that it's not a closure over the implicit lexical $_ of given but rather is using global $_ by declaring it our $_;.

Maybe we need to propose doc fixes to the impacted List::* modules' POD to give this workaround.

[ The alternative I can think of is for the any() etc functions to be made magic like grep. Maybe a B::* using utility subr could munge the optree to switch lexical $_ refs to global in the block arg. Would want it to only do that once for any given block. Would be easier with Perl6 AST edits! ]

Leave a comment

About komarov

user-pic I blog about Perl.