perlmoo

Weakening captures using Sub::Quote


I'd like to weaken captured variables in the code generated by Sub::Quote. For example, here's the non-quoted alternative:

use 5.10.0;
use Scalar::Util qw[ weaken ];
{
  my $s = 'foo';
  my $x = sub { say $s };
  weaken( my $y = $x );

  my $bar = sub { &$y };
  &$bar;
  $x = undef;
  &$bar
}

and the output:

foo
Can't use an undefined value as a subroutine reference [...]

And here's my Sub::Quote attempt:

use 5.10.0;
use Sub::Quote;
use Scalar::Util qw[ weaken ];
{
  my $s = 'foo';
  my $x = sub { say $s };
  weaken( my $y = $x );

  my $bar = quote_sub( '&$y', { '$y' => \$y } );
  &$bar;
  $x = undef;
  &$bar;
}

and the output:

foo
foo

Obviously the captured $y isn't weakened. Is there a way of altering the generated code to weaken captured variables?

The documentation is sparse, and the Sub::Quote implementation is complex; I'm fairly convinced this isn't possible with the current code, but I'd love to be shown to be wrong.


Solution

  • my $bar = quote_sub( '&$y', { '$y' => \$y } );
    

    is roughly the same as

    my $bar = eval(q{ my $y = $y; sub { &$y } });
    

    (It does more, but those bits are irrelevant to this question). As you can see, that creates a new strong reference to the sub[1].

    As a workaround, you could add a layer of indirection:

    my $bar = eval(q{ my $y_ref = \$y; sub { &{ $$y_ref } } });
    

    This can be achieved by using:

    my $bar = quote_sub( '&{$$y_ref}', { '$y_ref' => \\$y } );
    

    There wouldn't be any problems if the $y created by Sub::Quote was an alias for your $y. This can be achieved using Data::Alias or an experimental feature introduced in 5.22.

    This can be demonstrated using the following:

    {
      package Sub::Quote;
    
      my $sub = sub {
        my ($from, $captures, $indent) = @_;
        join(
          '',
          "use feature qw( refaliasing );\n",
          "no warnings qw( experimental::refaliasing );\n",
          map {
            /^([\@\%\$])/
              or croak "capture key should start with \@, \% or \$: $_";
            (' ' x $indent).qq{\\my ${_} = \\${1}{${from}->{${\quotify $_}}};\n};
          } keys %$captures
        )
      };
    
      no warnings qw( redefine );
      *capture_unroll = $sub;
    }
    
    
    my $bar = quote_sub( '&$y', { '$y' => \$y } );
    

    You could talk to the module's maintainer about adding an option that would cause the use of aliasing.


    1. When you create a copy of a (strong or weak) reference, it's a strong reference.