xandercrews has asked for the wisdom of the Perl Monks concerning the following question:

#!/usr/bin/perl -- use strict; # use warnings; sub factoryOne { my ($var) = (@_); print "factory: '$var'\n"; return sub { eval { print "eval: '$var'\n" }; } } sub factoryTwo { my ($var) = (@_); print "factory: '$var'\n"; return sub { my $estr = "print \"eval: '\$var'\n\""; eval $estr; } } sub factoryThree { my ($var) = (@_); print "factory: '$var'\n"; return sub { $var; my $estr = "print \"eval: '\$var'\n\""; eval $estr; } } my $f = factoryOne('lulz'); $f->(); my $g = factoryTwo('frunz'); $g->(); my $h = factoryThree('lulz'); $h->();
yields:
factory: 'lulz' eval: 'lulz' factory: 'frunz' eval: '' factory: 'lulz' eval: 'lulz'

the first factory works as expected, the second two don't, really.

two questions:
1) why isn't $var in scope inside the eval inside the closure inside factoryTwo?
2) why does referencing $var in the closure remedy 1) in factoryThree?

Replies are listed 'Best First'.
Re: i don't understand this scope behavior w/ closures and eval
by ikegami (Patriarch) on Feb 11, 2010 at 22:51 UTC

    As an optimisation, closures only capture the variables they need. Perl has no idea what variable you will need for an eval EXPR (since EXPR can evaluate to anything), so those can't be taken into consideration.

    If Perl made a mistake (as it does in the second function), it will let you know with the warning "Variable "$var" is not available".

    The workaround is to make Perl notice you need the variable (like you did in the third function). Adding $var if 0; is a cheap and simple way of doing that without introducing warnings.

      Nitpick: I don't think of that as just an optimization, it's necessary for garbage collection so we don't have memory leaks and destructors are executed early.

Re: i don't understand this scope behavior w/ closures and eval
by jethro (Monsignor) on Feb 11, 2010 at 22:23 UTC

    A closure is only created if there is a variable in use in the subroutine

    FactoryTwo simply is no closure, because you never reference any variable in there. Because of the \ before $var the variable is simply a text string without any meaning to perl. If you remove the \ before it you will see that eval "frunz" is printed again (although it won't be the closure you want, as the string with $var in it will be fixed with the value at the time FactoryTwo was called)

    In the third example you reference a variable, so perl creates a closure.

Re: i don't understand this scope behavior w/ closures and eval
by crashtest (Curate) on Feb 11, 2010 at 22:33 UTC

    Why turn warnings off? Turning them on (and autoflushing STDOUT and STDERR, so the output is in order) prints:

    Useless use of private variable in void context at myscript.pl line 27 +. factory: 'lulz' eval: 'lulz' factory: 'frunz' Variable "$var" is not available at (eval 2) line 1. Use of uninitialized value $var in concatenation (.) or string at (eva +l 2) line 1. eval: '' factory: 'lulz' eval: 'lulz'

    In the first factory, you're using $var in an eval{} block... which is quite different from a string eval(). This isn't an interesting case, and works as you expected anyway.

    The difference between your second and third examples is the supposedly useless (as perl tells you) statement $var. The compressed answer is that when you generate the closure in these latter cases, perl has no idea that you're making use of $var in your block, and so it sees no reason to include it in the closure.

    After all, all perl sees is a string and some function call (to eval()). But when you explicitly make use of $var in your (third) factory, perl includes it in the closure - not because it knows you're going to need it, but because you included it. When the string eval is then evaluated, it finds $var in the third factory, but can't resolve it in the second factory.

    To make sure it is clear what I am referencing from a closure, I always like to explicitly write out which variables I need for it... I'd write something like:

    return sub{ my ($code) = ($var); my $estr = "print \"eval: '\$code'\n\""; eval $estr; }