Debugging In List Context

Ovid on 2009-05-26T15:10:40

Today I faced a bug in this code:

$self->set_build_data([
        $self->build_primary_element,
        $self->build_identifiers,
        @build_event_in,
        @build_event_of,
        [
            offset => {},
            ($rs->offset ? $rs->offset->in_seconds : undef)
        ],
        [ position => {}, $rs->position ],
        [ title    => {}, $rs->title ],
        $self->build_synopses,
        $self->build_from('Taggings'),
        $self->build_resultset_links(qw/taggings changes/),
    ],
);

I saw that the build_from('Taggings') method wasn't returning the taggings. I put this code into the build_from method:

sub build_from {
    my ( $self, $type ) = @_;
    $DB::single = 'Taggings' eq $type;

The $DB::single = $some_true_value code sets a breakpoint. When you run the code through a debugger, hitting 'c' for "continue" will run the code up to that point and then halt there, waiting for you to issue additional commands (as a fun exercise, 'ack' for DB::single in your installed CPAN modules). That let me easily step into the code and find out that something prior to that method call had altered the state of the object and prevented my tags from being built. But let's look back at that code again. I didn't relish stepping into every one of those methods and finding out which was the offending code. As it turns out, it's the $self->build_context data which was getting altered. However, I can't just warn on those values because this fails:

use Data::Dumper;
$self->set_build_data([
        $self->build_primary_element,
        warn Dumper($self->build_context),
        $self->build_identifiers,
        warn Dumper($self->build_context),
        @build_event_in,
        warn Dumper($self->build_context),
        ...

It fails because warn returns a true value and thus destroys my list. However, there's an interesting feature of lists which many people are not aware. An empty lists collapses out of a list. The following list has two elements, not three:

my @two_element_list = (1, (), 2);

Now you can just have a subroutine return any empty list and safely make subroutine calls in the middle of a list. However, because of how lists work, you can't rely on line numbers via warn, so you need the __LINE__ directive to get the correct line number. That leads to the following code:

use Data::Dumper::Simple;
my $warn = sub { 
    my $line = shift;
    warn Dumper($line, $self->build_context);
    return (); 
};
$self->set_build_data([
        $self->build_primary_element,
        $warn->(__LINE__),
        $self->build_identifiers,
        $warn->(__LINE__), 
        ...

And near the top of my test output, I see the following:

$line = '35';
$self->build_context = {
  'expand_junction' => bless( [
    'promotions',
    'segments',
    'taggings'
  ], 'Perl6::Junction::Any' ),
  'uri_base' => bless( do{\(my $o = 'http://localhost/')}, 'URI::http' )
};
$line = '42';
$self->build_context = {
  'expand_junction' => bless( [
    'promotions',
    'segments'
  ], 'Perl6::Junction::Any' ),
  'uri_base' => bless( do{\(my $o = 'http://localhost/')}, 'URI::http' )
};
63 at /home/ovid/pips_dev/work/Pips3/branches/exploratory/lib/PIPs/API/Builder/SegmentEvent.pm line 60.
$self->build_context = {
  'expand_junction' => bless( [
    'promotions',
    'segments'
  ], 'Perl6::Junction::Any' ),
  'uri_base' => bless( do{\(my $o = 'http://localhost/')}, 'URI::http' )
};

And now I can instantly see that I lost the 'taggings' build context right after I called build_identifiers, thus telling me quickly where the bug in my code was.


you could also inline it

cowens on 2009-05-26T18:47:32

#!/usr/bin/perl

use strict;
use warnings;

my @a = (
                1,
                @{[map {()} warn "in here\n"]},
                2,
                3
);

print map { "$_\n" } @a;

Re:you could also inline it

cowens on 2009-05-26T18:52:07

or the slightly less stupid (I don't know why I threw it in an arrayref to start with):

#!/usr/bin/perl

use strict;
use warnings;

my @a = (
                1,
                (map {()} warn "in here\n"),
                2,
                3
);

print map { "$_\n" } @a;

Re:you could also inline it

Aristotle on 2009-05-30T18:20:37

An even shorter way to write that is this:

(map {;} warn "in here\n")

However, I prefer to take a page from Javascript, by defining the following amusing function:

sub void {}

I can then write the code in this much nicer way:

(void warn "in here\n")

This is far nicer than Ovid’s approach as well, IMO.

Re:you could also inline it

Ovid on 2009-06-01T07:52:26

Ah, that is a nicer solution, thanks.

return();

jarich on 2009-05-27T00:34:39

return() with no arguments (which is what you're doing) does the "right thing" in both scalar and list contexts. It returns either an empty list or undefined value.

Thus you could also have written:

return;

for the same effect. I'm sure you know this already, but from your commentary and style it did look like you were trying to explicitly return an empty list.

Re:return();

Ovid on 2009-05-27T06:47:19

Yes, I normally just use a bare return (and get annoyed when people use 'return 0' for false), but I honestly didn't think about that here because I originally had this:

my $warn = sub { warn Dumper($self->build_context); () };

That showed the annoying __LINE__ problem and I decided to be explicit about the return when I added the $line variable. Normally about the only time -- aside from this example -- I return an explicit empty list is when I do this:

return $condition ? $some_value : ();

Re:return();

Ovid on 2009-05-27T06:48:21

Oh, and what I didn't say, but should have, in my first reply was "thanks". The bare return is (IMHO) a cleaner solution.

Re:return();

kane on 2009-05-27T10:12:46

For less trickery with subs and call stacks and line numbers, remember your old friend 'do'!

$ perl -le'print join " ", (1, do { warn 2; () }, 3)'
2 at -e line 1.
1 3