http://www.perlmonks.org?node_id=242974

Hook::LexWrap can do some deeply funky things. It has been particularly helpful to me when I've needed to add instrumentation to code during debugging and testing.

Consider the following class:

package Foo; sub new { bless {todo => 0, done => 0}, shift }; sub add { shift->{todo}++ }; sub remove { my $self = shift; $self->{todo}--; $self->{done}++; }; sub todo { shift->{todo} }; sub done { shift->{done} };

Nice and simple. Unless of course some bad classes break encapsulation:

package Bar; use base qw(Foo); # oops. $self->{done} not updated sub naughty_remove { shift->{todo}-- }; ... package Ni; use base qw(Foo); sub naughty_transfer { my ($to, $from) = @_; while ($from->todo) { $to->{todo}++; # oops. $from->{done} not updated $from->{todo}--; }; };

This kind of bug can be hard to locate in large classes. Wouldn't it be nice if we could ask perl to keep a lookout for where Foo objects are used incorrectly by a subroutine? Maybe something like this:

# Monitor the arguments of subroutines ... my $monitor = monitor_arg # ... in the following packages ... in_package => [ 'Foo', 'Bar', 'Ni' ], # ... where the argument isa Foo object matching => sub { ref($_[0]) && UNIVERSAL::isa($_[0], 'Foo') }, # ... and check that we didn't mess with the internals ... with => sub { my ($original, $current, $subroutine) = @_; my $removed = $original->todo - $current->todo; my $done = $current->done - $original->done; warn "removed item not added to done list in $subroutine\n" if $removed > 0 && $removed != $done; };

With the wonder of Hook::LexWrap we can :-)

use Carp; use Storable qw(dclone); use Hook::LexWrap; use Devel::Symdump; our $Monitoring; sub monitor_arg { my %param = @_; my ($matching, $in_package, $with) = map {$param{$_} or croak "need $_"} qw(matching in_package with); croak "called in void context" unless defined wantarray; my @wrappers; foreach my $subroutine (Devel::Symdump->functions(@$in_package)) { my ($original, @current); push @wrappers, wrap $subroutine, pre => sub { return if $Monitoring; local $Monitoring = 1; @current = grep { $matching->($_) } @_; $original = dclone(\@current); }, post => sub { return if $Monitoring; local $Monitoring = 1; foreach my $current (@current) { my $original = shift @$original; $with->($original, $current, $subroutine); } }; }; return \@wrappers; };

So running:

my $o = Bar->new; $o->add; $o->add; $o->naughty_remove; my $from = Foo->new(); $from->add; my $to = Ni->new; $to->naughty_transfer($from);

with the previously shown call to monitor_arg will give us

removed item not added to done list in Bar::naughty_remove removed item not added to done list in Ni::naughty_transfer

I'm sure you get the idea. What interesting things have you done with Hook::LexWrap?

Replies are listed 'Best First'.
Assertions?
by zby (Vicar) on Mar 14, 2003 at 13:38 UTC
    I've read somwhere they are just coming. Wouldn't it be simpler with assertions?
      Doing this by hand would be a complete PITA

      No. Doing this would be

      1. a complete waste of time
      2. counter-productive
      3. a case of the cure worse than the disease
      Every time you add a line of code, you have a non-zero (and often deceptively large) chance of introducing a bug. Let's say that you're a really good programmer and your error rate for development is 1 bug for every 20 lines of code*. If you add 100 assertions to find one bug (assuming that Foo is used in 100 places), then you just (potentially) added another five bugs, just as hard to track. (Possibly harder, because everyone assumes that bugfix code isn't buggy.)

      In addition, you now have a maintenance nightmare. Let's say that I'm your maintenance programmer. I go in and realize I need to now modify Foo somewhere. I have 24 hours to get this bug fixed. I don't know about your assertions thing. All I know is that I can fix the bug by hacking at your pretty object. Boom! Your entire assertions framework fails.

      Using Hook::LexWrap (and similar techniques) is vastly preferable to adding assertions by hand, for just these reasons.

      * Error rates gleaned from Code Complete on p. 610. Steve McConnell says that delivered code generally has 15-50 defects per 1000 lines. He also says that the Apps Division at MS has 10-20/1000 during in-house testing and 0.5/1000 in released code. That's a 30-1 ratio between development and release for a stringent regime. A lax regime will probably have a 10-1 ratio. Thus, a standard programmer during development will probably have 350-500 defects per 1000 lines of code. A really good one will be at 50/1000, or 1/20 lines of code.

      ------
      We are the carpenters and bricklayers of the Information Age.

      Don't go borrowing trouble. For programmers, this means Worry only about what you need to implement.

      Please remember that I'm crufty and crochety. All opinions are purely mine and all code is untested, unless otherwise specified.

        All true.

        One of the many things that excites me about perl6 is that it looks like you'll be able to implement proper class invarients and contracts ala Eiffel with the subroutine wrapping stuff. Nice.

        That's not to say I'm not looking forward to a sensible assertion mechanism in the next perl5. Although my first thought when I saw it was to use it for logging ;-)

      I don't think it wouldn't in this instance. We're tracking bug that's due to a naughty subclass breaking encapsulation (direct hash access rather than via methods).

      Adding assertions to the base class won't help. That's not where the bug is.

      Adding assertions to the subclasses is basically what we're doing - but at runtime not compile time.

      The "problem" with perl is that because we don't have proper encapsulation we would have to add the assertions by hand to anything that might fiddle with a Foo object in a naughty manner. Doing this by hand would be a complete PITA. Doing it programmatically using Hook::LexWrap is a breeze.

        I've got it - you are right. I had a thread about code analyzis - TIMTOWDTDI, obfu and analyzis of code it seems this could be applicable here too. The difference is this would be a static method while your's is a dynamic one.