Warpstar22
Warpstar22

Reputation: 613

Perl: Abstraction of function taking block of code as an argument

For my testing, I wanted to add a function that could time a block of code. The block of code would be like map where you give it in braces. I used this answer and it works well.

Timer.pm

sub recordTime(&@)
{
    # https://stackoverflow.com/a/6101734/11365539
    my $code = \&{shift @_};
    my %opts = @_;

    $opts{name}  //= 'Time';
    $opts{fmt}   //= 'default';
    $opts{hires} //= 0;

    my $timing_sub = $opts{hires} ? \&CORE::time : \&Time::HiRes::time;

    my $start = $timing_sub->();
    $code->();
    my $end = $timing_sub->();

    my $output;
    if($opts{log})
    {
        $output = IO::File->new($opts{log}, '>>')
                    or carp("Failed to open $opts{log}. Outputting to STDOUT...");
    }
    if(not defined($output)) #if $opts{log} is undefined or IO::File->new() failed
    {
        $output = *STDOUT;
    }

    my $time_diff = $end - $start;
    $output->print("$opts{name}: ");

    # formatting time and printing it

    {
        no warnings 'numeric'; # perl complains that *STDOUT is not numeric
        if($output != *STDOUT)
        {
            $output->close();
        }
    }
}

1;

This code seems to work as expected. I can use it like this:

Timer::recordTime {
    runTests();
} (name => 'Run all tests', log => $timing_log);

I don't want to write log => $timing_log everywhere, so I added this function in another module.

TesterInterface.pm

sub logTime(&@)
{
    my $code = \&{shift @_};
    my %opts = @_;

    Timer::recordTime {
        $code->();
    } (%opts, log => $timing_log);
}

So I would use it like this

TesterInterface::logTime {
    runTests();
} (name => "Run all tests");

This keeps throwing absurd errors such as Undefined subroutine &main::0 called. To me it seems like this is evaluating an implicit return of the last line of $code. I tested this by adding a say("hi") statement at the end of the code block which will return 1. I then saw Undefined subroutine &main::1 called. I tried to then escape it by changing the line to \ say("hi"), but that produces this error: Can't call method "logTime" on unblessed reference. I really have no idea how to fix this.

I did find that doing this will work, but I would prefer to avoid having to write sub. I also don't understand why this method works, but not the code block version.

TesterInterface::logTime(sub {
    runTests();
}, name => 'Run all tests');

I'm using Perl v5.22.1 in Windows

Upvotes: 1

Views: 87

Answers (1)

ikegami
ikegami

Reputation: 385764

You have not provided a demonstration of the problem, but it's apparent the call to Timer::recordTime occured was compiled before Timer::recordTime's declaration was compiled.

This causes

Timer::recordTime { $code->(); } ( %opts, log => $timing_log );

to be parsed as an indirect method call equivalent to the following:

do { $code->(); }->Timer::recordTime( %opts, log => $timing_log );

By the way,

my $code = \&{shift @_};

is a complicated way of doing

my $code = shift;

The former does accept a sub name in addition to a sub ref, but the prototype doesn't allow that.


By the way, your logTime adds a lot of unnecessary overhead. I'd use:

sub logTime(&@) {
   &Timer::recordTime( @_, log => $timing_log );
}

The & causes the prototype to be ignored.

Upvotes: 2

Related Questions