Skip Menu |

This queue is for tickets about the Memoize CPAN distribution.

Report information
The Basics
Id: 17265
Status: open
Priority: 0/
Queue: Memoize

People
Owner: Nobody in particular
Requestors: john [...] newchester.com
Cc:
AdminCc:

Bug Information
Severity: Important
Broken in:
  • 0.66
  • 1.00
  • 1.01
Fixed in: (no value)



Subject: Memoization of object methods
Memoizing an object method can produce undesirable results if an object is dereferenced, a new object is created with the same memory address, and then a memoized method is called on that object: the memoized result from when this same method was called on a completely different objet will be returned. The following test demonstrates, and the patch introduces a new option to Memoize for memoizing object methods.
Subject: test_patch.t
package Memoize::TEST::JohnObjectPatch; use strict; use warnings; use Test::More tests => 12; sub new { my($pkg, %args) = @_; return bless \%args, $pkg; } use Memoize; memoize('_test_method', SCALAR_CACHE => 'IN_OBJECT', LIST_CACHE => 'IN_OBJECT'); sub _test_method { my($object, $arg) = @_; $object->{invocation_count} ||= 0; $object->{invocation_count}++; return $arg * 2; } sub TEST_THIS_BAD_CLASS { # Make a simple object my $object = __PACKAGE__->new; # Call a memoized method { my $value = $object->_test_method(10); is($value, 20); is($object->{invocation_count}, 1); } # Call again with same arg. Invocation count should not change { my $value = $object->_test_method(10); is($value, 20); is($object->{invocation_count}, 1); } # Call again with different arg { my $value = $object->_test_method(11); is($value, 22); is($object->{invocation_count}, 2); } # Now make $object reference a completely different object # But still at the same refaddress # This simulates situations where an object is destroyed, but another # Object is constructed and given same reference address my $usurper = __PACKAGE__->new; %$object = %$usurper; # We should *not* get the memoized value this time { is($object->{invocation_count}, undef); my $value = $object->_test_method(10); is($value, 20); is($object->{invocation_count}, 1); } eval { my $value = _test_method(11); }; like($@, qr/Method .+_test_method was memoized with /); # Now check scalar context { my @value = $object->_test_method(10); is_deeply(\@value, [20]); # Since it's called in list context # Memoize will invoke the actual method again # To populate list cache is($object->{invocation_count}, 2); } } &TEST_THIS_BAD_CLASS; 1;
Subject: Memoize.pm.patch
--- /home/y/lib/perl5/5.8.6/Memoize.pm 2005-01-11 13:36:39.000000000 -0800 +++ /home/wardenj/Memoize.pm.patched 2005-12-07 22:24:35.000000000 -0800 @@ -28,6 +28,7 @@ use Exporter; use vars qw($DEBUG); use Config; # Dammit. +use Scalar::Util 'reftype'; @ISA = qw(Exporter); @EXPORT = qw(memoize); @EXPORT_OK = qw(unmemoize flush_cache); @@ -108,13 +109,18 @@ $options{"${context}_CACHE"} ||= ''; my $cache_opt = $options{"${context}_CACHE"}; + my @cache_opt_args; if (ref $cache_opt) { @cache_opt_args = @$cache_opt; $cache_opt = shift @cache_opt_args; } if ($cache_opt eq 'FAULT') { # no cache - $caches{$context} = undef; + $caches{$context} = undef; + } + # cache in the instance in which the method is invoked + elsif($cache_opt eq 'IN_OBJECT') { + $caches{$context} = 'IN_OBJECT'; } elsif ($cache_opt eq 'HASH') { # user-supplied hash my $cache = $cache_opt_args[0]; my $package = ref(tied %$cache); @@ -215,6 +221,7 @@ } } + # This is the function that manages the memo tables. sub _memoizer { my $orig = shift; # stringized version of ref to original func. @@ -224,6 +231,15 @@ my $argstr; my $context = (wantarray() ? LIST : SCALAR); + # If we are memoizing in the object itself, then don't include + # Object reference in argstring. + my $store_in_object = 1 if( + $context == SCALAR and $info->{S} eq 'IN_OBJECT' + ) or ( + $context == LIST and $info->{L} eq 'IN_OBJECT' + ); + my $object = shift() if $store_in_object; + if (defined $normalizer) { no strict; if ($context == SCALAR) { @@ -237,9 +253,22 @@ local $^W = 0; $argstr = join chr(28),@_; } + + # Put object back in @_, 'cause we still need to pass it to method + unshift(@_, $object) if $store_in_object; if ($context == SCALAR) { my $cache = $info->{S}; + if($cache eq 'IN_OBJECT') { + if( + !$object + or !(reftype($object) eq 'HASH' or ref($object) eq 'HASH') + ) { + confess "Method $info->{NAME} was memoized with SCALAR_CACHE => 'IN_OBJECT', which means that the method must always be called on an object"; + } else { + $cache = $object->{'_Memoize::SCALAR_CACHE'} ||= {} + } + } _crap_out($info->{NAME}, 'scalar') unless $cache; if (exists $cache->{$argstr}) { return $cache->{$argstr}; @@ -255,6 +284,16 @@ } } elsif ($context == LIST) { my $cache = $info->{L}; + if($cache eq 'IN_OBJECT') { + if( + !$object + or !(reftype($object) eq 'HASH' or ref($object) eq 'HASH') + ) { + confess "Method $info->{NAME} was memoized with LIST_CACHE => 'IN_OBJECT', which means that the method must always be called on an object"; + } else { + $cache = $object->{'_Memoize::LIST_CACHE'} ||= {} + } + } _crap_out($info->{NAME}, 'list') unless $cache; if (exists $cache->{$argstr}) { my $val = $cache->{$argstr};
This bug is 6 years old, and I'm having similar problems with reference data. It will report an array that is 3 levels deep, instead of two levels deep. Can we try out this patch and implement some t/ cases?