lib/Class/Simple/Cached.pm

Structural Coverage (Approximate)

TER1 (Statement): 100.00%
TER2 (Branch): 96.67%
TER3 (LCSAJ): 100.0% (9/9)
Approximate LCSAJ segments: 61

LCSAJ Legend

โ— Covered โ€” this LCSAJ path was executed during testing.

โ— Not covered โ€” this LCSAJ path was never executed. These are the paths to focus on.

Multiple dots on a line indicate that multiple control-flow paths begin at that line. Hovering over any dot shows:

        start โ†’ end โ†’ jump
        

Uncovered paths show [NOT COVERED] in the tooltip.

Mutant Testing Legend

Survived (tests missed this) Killed (tests detected this) No mutation
    1: package Class::Simple::Cached;
    2: 
    3: use strict;
    4: use warnings;
    5: use autodie qw(:all);
    6: 
    7: use Carp ();
    8: use Class::Simple;
    9: use Params::Get 0.15;
   10: use Scalar::Util ();
   11: use Sub::Protected;
   12: 
   13: # Stored in the cache to distinguish "the object returned undef" from
   14: # "this key has never been cached".  Must never be a legitimate return value.
   15: use constant UNDEF_SENTINEL => __PACKAGE__ . '>UNDEF<';
   16: 
   17: # Evaluated once at compile time so DESTROY never pays a string comparison
   18: # on every object teardown just to decide whether ${^GLOBAL_PHASE} exists.
   19: use constant _GLOBAL_PHASE_AVAILABLE => defined($^V) && ($^V ge 'v5.14.0');
   20: 
   21: =head1 NAME
   22: 
   23: Class::Simple::Cached - cache getter results for any get/set object
   24: 
   25: =head1 VERSION
   26: 
   27: Version 0.06
   28: 
   29: =cut
   30: 
   31: our $VERSION = '0.06';
   32: 
   33: =encoding UTF-8
   34: 
   35: =head1 SYNOPSIS
   36: 
   37:     use CHI;
   38:     use Class::Simple::Cached;
   39: 
   40:     # Wrap an existing object with a cache layer
   41:     my $cache = CHI->new(driver => 'RawMemory', global => 1);
   42:     my $obj   = Class::Simple::Cached->new(
   43:         cache  => $cache,
   44:         object => My::Expensive::Object->new(),
   45:     );
   46: 
   47:     $obj->name('Alice');      # setter: delegates to wrapped object, updates cache
   48:     my $n = $obj->name();     # getter: returns cached 'Alice' without hitting object
   49: 
   50:     # Or use a plain hash ref as the cache backend
   51:     my %store;
   52:     my $simple = Class::Simple::Cached->new(cache => \%store);
   53:     $simple->colour('blue');
   54:     print $simple->colour();  # 'blue', served from %store
   55: 
   56: =head1 DESCRIPTION
   57: 
   58: A subclass of L<Class::Simple> that transparently caches the return values of
   59: getter calls, so that repeated reads of expensive-to-compute or
   60: expensive-to-transport values hit the cache instead of the wrapped object.
   61: 
   62: Cache coherency is I<not> automatic.  If the wrapped object's state changes
   63: through a path other than the cached wrapper, callers must invalidate the cache
   64: themselves.
   65: 
   66: =head1 SUBROUTINES/METHODS
   67: 
   68: =head2 new
   69: 
   70: Constructs a C<Class::Simple::Cached> instance that wraps C<object> behind
   71: C<cache>.
   72: 
   73: =head3 ARGUMENTS
   74: 
   75: =over 4
   76: 
   77: =item C<cache> (mandatory)
   78: 
   79: Either a blessed object implementing C<get($key)>, C<set($key, $val, $expires)>,
   80: and C<purge()> (e.g. any L<CHI> driver); or a plain hash reference used as an
   81: in-process store.
   82: 
   83: =item C<object> (optional)
   84: 
   85: The object whose methods will be proxied and cached.  Defaults to a fresh
   86: C<Class::Simple> instance.
   87: 
   88: =back
   89: 
   90: Calling C<< ->new() >> on an already-blessed instance returns a shallow clone
   91: (all stored fields are merged, including the existing cache handle).
   92: 
   93: =head3 RETURNS
   94: 
   95: A blessed C<Class::Simple::Cached> instance.
   96: 
   97: =head3 SIDE EFFECTS
   98: 
   99: None beyond allocating the new instance.
  100: 
  101: =head3 EXAMPLE
  102: 
  103:     # CHI-backed cache
  104:     use CHI;
  105:     my $obj = Class::Simple::Cached->new(
  106:         cache  => CHI->new(driver => 'RawMemory', global => 1),
  107:         object => My::Model->new(),
  108:     );
  109: 
  110:     # Hash-ref cache (useful for tests or short-lived objects)
  111:     my $obj = Class::Simple::Cached->new(cache => {});
  112: 
  113:     # Clone an existing wrapped object
  114:     my $clone = $obj->new();
  115: 
  116: =head3 API SPECIFICATION
  117: 
  118:     Input:
  119:       cache  => HashRef | CHICompatibleObject  # required
  120:       object => Object                          # optional
  121: 
  122:     Output:
  123:       Class::Simple::Cached instance
  124: 
  125: =head3 MESSAGES
  126: 
  127:     Message                                              Meaning                              Resolution
  128:     ---------------------------------------------------  -----------------------------------  ----------------------------------------
  129:     "use ->new() not ::new() to instantiate"             Called as Class::Simple::Cached::new  Use $obj->new() or ClassName->new()
  130:     "Usage: $class->new(cache => \$cache)"               No arguments supplied                 Pass at least cache => ...
  131:     "Cache must be ref to HASH or object"                cache is a plain scalar or wrong ref  Use a hashref or CHI-compatible object
  132:     "Cache object must implement get, set, purge"        Blessed cache lacks required methods  Use a fully CHI-compatible object
  133: 
  134: =head3 PSEUDOCODE
  135: 
  136:     new(class, args):
  137:       IF class undefined   → carp and return undef
  138:       IF class is blessed  → merge fields, return shallow clone
  139:       IF no args           → croak Usage message
  140:       PARSE args into hashref via Params::Get
  141:       IF params.object absent → params.object = Class::Simple->new()
  142:       IF params.cache is a blessed object:
  143:         VERIFY it can('get') AND can('set') AND can('purge')
  144:         IF not → croak capability message
  145:         RETURN bless params, class
  146:       IF params.cache is a HASH ref:
  147:         RETURN bless params, class
  148:       croak "Cache must be ref to HASH or object"
  149: 
  150: =cut
  151: 
  152: sub new
  153: {
โ—154 โ†’ 157 โ†’ 163  154: 	my $class = shift;
  155: 
  156: 	# Guard: always call as a method, not a bare function
  157: 	if(!defined($class)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

158: Carp::carp(__PACKAGE__, ' use ->new() not ::new() to instantiate'); 159: return; 160: } 161: 162: # When called on a blessed instance, return a shallow clone โ—163 โ†’ 163 โ†’ 192 163: if(Scalar::Util::blessed($class)) {

Mutants (Total: 1, Killed: 1, Survived: 0)

164: my $params = Params::Get::get_params(undef, \@_) || {}; 165: my %merged = (%{$class}, %{$params}); 166: # Always recalculate internal dispatch fields after the merge so that 167: # caller-supplied _is_hash_cache or _cache_prefix args cannot corrupt 168: # the cache-type flag or the key-prefix (internal field injection). 169: $merged{'_is_hash_cache'} = ref($merged{'cache'}) eq 'HASH'; 170: $merged{'_cache_prefix'} = ref($class) . ':'; 171:

Mutants (Total: 2, Killed: 2, Survived: 0)

172: # Validate the (potentially-replaced) cache object using the same rules 173: # as new() — the clone path must not produce an unusable object. 174: if(!$merged{'_is_hash_cache'}) { 175: if(Scalar::Util::blessed($merged{'cache'})) { 176: unless($merged{'cache'}->can('get')

Mutants (Total: 2, Killed: 2, Survived: 0)

177: && $merged{'cache'}->can('set') 178: && $merged{'cache'}->can('purge')) 179: { 180: Carp::croak("Cache object must implement 'get', 'set', and 'purge' methods"); 181: } 182: } else { 183: Carp::croak(ref($class) . ': Cache must be ref to HASH or object'); 184: } 185: } 186: 187: return bless \%merged, ref($class); 188: } 189: 190: # Require at least one argument so Params::Get's confess is never reached;

Mutants (Total: 1, Killed: 1, Survived: 0)

191: # we want croak (Test::Carp-compatible) not confess. โ—192 โ†’ 192 โ†’ 196 192: if(scalar(@_) == 0) {

Mutants (Total: 1, Killed: 1, Survived: 0)

193: Carp::croak('Usage: ', $class, '->new(cache => $cache)'); 194: } 195: โ—196 โ†’ 206 โ†’ 219 196: my $params = Params::Get::get_params('cache', @_) || {}; 197: 198: # Default the wrapped object to a bare Class::Simple instance

Mutants (Total: 2, Killed: 2, Survived: 0)

199: $params->{'object'} ||= Class::Simple->new(); 200: 201: # Precompute per-instance fields so AUTOLOAD (hot path) avoids ref() and 202: # string concatenation on every call. 203: $params->{'_is_hash_cache'} = ref($params->{'cache'}) eq 'HASH';

Mutants (Total: 1, Killed: 1, Survived: 0)

204: $params->{'_cache_prefix'} = "$class:";

Mutants (Total: 2, Killed: 2, Survived: 0)

205: 206: if(Scalar::Util::blessed($params->{'cache'})) { 207: # Verify the cache object speaks the required interface 208: unless($params->{'cache'}->can('get') 209: && $params->{'cache'}->can('set') 210: && $params->{'cache'}->can('purge')) 211: { 212: Carp::croak("Cache object must implement 'get', 'set', and 'purge' methods"); 213: } 214: return bless $params, $class; 215: } 216: 217: # Transitive reduction: _is_hash_cache already holds ref($cache) eq 'HASH' 218: # computed two statements earlier — no need to call ref() again. โ—219 โ†’ 219 โ†’ 223 219: if($params->{'_is_hash_cache'}) { 220: return bless $params, $class; 221: } 222: 223: Carp::croak("$class: Cache must be ref to HASH or object"); 224: } 225: 226: =head2 can 227: 228: Reports whether this wrapper (or its embedded object) can handle a method. 229: 230: =head3 ARGUMENTS 231: 232: =over 4 233: 234: =item C<$method> — the method name to probe. 235: 236: =back 237: 238: =head3 RETURNS 239: 240: True if the method is known; false otherwise. 241: 242: =head3 EXAMPLE 243: 244: $obj->can('name'); # true if the wrapped object has a name() method 245: 246: =head3 API SPECIFICATION 247: 248: Input: method_name : Str

Mutants (Total: 2, Killed: 2, Survived: 0)

249: Output: Bool 250: 251: =head3 MESSAGES 252: 253: None. 254: 255: =cut 256: 257: sub can 258: { 259: my ($self, $method) = @_; 260: 261: # Undefined method name is outside the documented API; return undef cleanly. 262: return unless defined $method; 263: 264: # When called as a class method there is no wrapped object to probe 265: return $self->SUPER::can($method) unless Scalar::Util::blessed($self); 266: 267: return ($method eq 'new') 268: || $self->{'object'}->can($method) 269: || $self->SUPER::can($method); 270: } 271: 272: =head2 isa 273: 274: Reports whether this wrapper or its embedded object is of a given class. 275: 276: =head3 ARGUMENTS 277: 278: =over 4 279: 280: =item C<$class> — the class name to test. 281: 282: =back 283: 284: =head3 RETURNS 285: 286: True if the wrapper or the wrapped object is-a C<$class>. 287: 288: =head3 EXAMPLE 289:

Mutants (Total: 2, Killed: 0, Survived: 2)
290: $obj->isa('My::Model'); # delegates to the wrapped object 291:

Mutants (Total: 2, Killed: 2, Survived: 0)

292: =head3 API SPECIFICATION 293: 294: Input: class_name : Str 295: Output: Bool

Mutants (Total: 2, Killed: 2, Survived: 0)

296: 297: =head3 MESSAGES 298: 299: None. 300: 301: =cut 302: 303: sub isa 304: { 305: my ($self, $class) = @_; 306: 307: # Undefined class name is outside the documented API; return undef cleanly. 308: return unless defined $class; 309: 310: # When called as a class method there is no wrapped object to interrogate 311: return $self->SUPER::isa($class) unless Scalar::Util::blessed($self); 312: 313: return 1 if $class eq ref($self) 314: || $class eq __PACKAGE__

Mutants (Total: 1, Killed: 1, Survived: 0)

315: || $self->SUPER::isa($class); 316: 317: return $self->{'object'}->isa($class);

Mutants (Total: 1, Killed: 1, Survived: 0)

318: } 319: 320: # DESTROY — purge this instance's entries from the cache on object teardown. 321: # 322: # Purpose: Prevent stale entries from leaking into a shared cache after 323: # the wrapper goes out of scope. 324: # Entry: Called automatically by Perl's garbage collector. 325: # Exit Status: None (void). 326: # Side Effects: Removes all cache keys prefixed with ref($self) for hash caches; 327: # calls purge() for CHI-style caches. 328: # See https://github.com/Perl/perl5/issues/14673 for why an 329: # explicit DESTROY is required even though AUTOLOAD could catch it. 330: sub DESTROY 331: { โ—332 โ†’ 336 โ†’ 344 332: my $self = shift; 333:

Mutants (Total: 2, Killed: 2, Survived: 0)

334: my $cache = $self->{'cache'} or return; 335: 336: if($self->{'_is_hash_cache'}) { 337: # Remove only this class's keys to avoid stomping on siblings sharing the hash 338: my $prefix = $self->{'_cache_prefix'}; 339: delete $cache->{$_} for grep { index($_, $prefix) == 0 } keys %{$cache}; 340: return;

Mutants (Total: 1, Killed: 1, Survived: 0)

341: } 342: 343: # Skip purge during global destruction to avoid order-of-destruction crashes 344: return if _GLOBAL_PHASE_AVAILABLE && ${^GLOBAL_PHASE} eq 'DESTRUCT'; 345: 346: $cache->purge(); 347: } 348: 349: # _cache_get / _cache_set — unified read/write; hides hash-ref vs. CHI dispatch 350: # so the rest of the code never needs to branch on ref($cache). 351: sub _cache_get :Protected 352: { 353: my ($self, $key) = @_; 354: my $cache = $self->{'cache'}; 355: return $self->{'_is_hash_cache'} ? $cache->{$key} : $cache->get($key); 356: } 357: 358: sub _cache_set :Protected 359: { โ—360 โ†’ 362 โ†’ 0 360: my ($self, $key, $val) = @_; 361: my $cache = $self->{'cache'}; 362: if($self->{'_is_hash_cache'}) { 363: $cache->{$key} = $val; 364: } else { 365: $cache->set($key, $val, 'never'); 366: } 367: } 368: 369: =head2 AUTOLOAD (getter/setter proxy) 370: 371: Intercepts every method call that is not explicitly defined, proxying it to 372: the wrapped object with a caching layer for zero-argument (getter) calls. 373: 374: Setter calls (one or more arguments) always pass through to the wrapped object 375: and update the cache with the new value. 376: 377: =head3 ARGUMENTS 378: 379: $method() — getter: returns cached value if present, else calls object 380: $method($scalar) — scalar setter: stores scalar, updates cache 381: $method(@list) — array setter: stores list, updates cache 382: 383: =head3 RETURNS 384: 385: The value returned by the wrapped object (or the cached copy thereof). 386: 387: =head3 SIDE EFFECTS 388: 389: =over 4 390: 391: =item * Getter: may write to the cache on first call. 392: 393: =item * Setter: writes to both the wrapped object and the cache. 394: 395: =item * DESTROY: clears cache entries for this instance (see above). 396: 397: =back 398: 399: =head3 EXAMPLE 400: 401: $obj->colour('red'); # setter — writes 'red' to object and cache 402: $obj->colour(); # getter — returns 'red' from cache 403: 404: =head3 API SPECIFICATION 405: 406: Input (getter): method_name : Str, args : () 407: Input (setter): method_name : Str, args : (Scalar | List) 408: Output: the stored/retrieved value or list 409: 410: =head3 MESSAGES 411: 412: Message Meaning Resolution 413: -------------------------------- ---------------------------------- ---------------------------------------- 414: "$method" (croak) Cached array's first element is Do not store the sentinel string 415: the UNDEF_SENTINEL string as a real value in the wrapped object 416: 417: =head3 PSEUDOCODE 418: 419: AUTOLOAD(method, args...): 420: key = ref(self) + ":" + method 421: 422: IF no args (getter mode): 423: val = cache_get(key) 424: IF cache hit: 425: IF val is a plain string (not a ref): 426: IF val is UNDEF_SENTINEL → return undef 427: RETURN val 428: IF val is an arrayref: 429: IF first element is a plain string AND equals UNDEF_SENTINEL → croak 430: RETURN dereferenced list 431: RETURN val (blessed object) 432: # Cache miss — ask the wrapped object 433: IF list context: 434: result_list = object->method() 435: IF empty → return () 436: cache_set(key, \result_list) 437: RETURN result_list 438: # Scalar context 439: result = object->method() 440: IF defined: 441: cache_set(key, result) 442: RETURN result 443: cache_set(key, UNDEF_SENTINEL) 444: RETURN undef 445: 446: ELSE (setter mode): 447: IF more than one arg (array setter): 448: val = object->method(\@args) # wrapped object stores arrayref 449: IF defined: 450: cache_set(key, val) 451: RETURN @val 452: cache_set(key, UNDEF_SENTINEL)

Mutants (Total: 2, Killed: 2, Survived: 0)

453: RETURN undef 454: ELSE (scalar setter): 455: val = object->method(args[0]) 456: cache_set(key, val // UNDEF_SENTINEL) 457: RETURN val 458:

Mutants (Total: 1, Killed: 1, Survived: 0)

459: =cut 460: 461: sub AUTOLOAD 462: { โ—463 โ†’ 474 โ†’ 516 463: our $AUTOLOAD;

Mutants (Total: 1, Killed: 1, Survived: 0)

464: # rindex+substr avoids regex engine startup on every method dispatch 465: my $param = substr($AUTOLOAD, rindex($AUTOLOAD, '::') + 2); 466:

Mutants (Total: 2, Killed: 2, Survived: 0)

467: my $self = shift; 468: # _cache_prefix is precomputed at new() time (= ref($self) . ":") to avoid 469: # calling ref() and doing string concat on every AUTOLOAD invocation. 470: my $key = $self->{'_cache_prefix'} . $param; 471: my $object = $self->{'object'}; 472:

Mutants (Total: 2, Killed: 2, Survived: 0)

473: # Getter path ───────────────────────────────────────────────────────────── 474: if(scalar(@_) == 0) { 475: my $rc = $self->_cache_get($key); 476:

Mutants (Total: 2, Killed: 2, Survived: 0)

477: # Truthiness check: falsy-but-defined values (0, '') are treated as cache 478: # misses so the object is re-invoked every call. This is a known 479: # limitation; see the LIMITATIONS section in the POD. 480: if($rc) {

Mutants (Total: 1, Killed: 1, Survived: 0)

481: # Premise 1: $rc is truthy (the if-guard guarantees this). 482: # Premise 2: ref($rc) returns '' for plain scalars, 'ARRAY' for 483: # arrayrefs, or a package name for blessed objects. 484: # The three branches below are mutually exclusive and exhaustive.

Mutants (Total: 2, Killed: 2, Survived: 0)

485: if(!ref($rc)) { 486: # Conclusion: plain scalar — only type that can equal the sentinel. 487: return if $rc eq UNDEF_SENTINEL; 488: return $rc; 489: } elsif(ref($rc) eq 'ARRAY') { 490: # Premise: !ref($rc) was false, so ref($rc) is truthy; no need

Mutants (Total: 2, Killed: 2, Survived: 0)

491: # to re-test ref() — the elsif is the deductive next step. 492: Carp::croak($param) 493: if !ref($rc->[0]) && $rc->[0] eq UNDEF_SENTINEL; 494: return @{$rc};

Mutants (Total: 4, Killed: 4, Survived: 0)

495: } 496: # Conclusion: ref($rc) is truthy and ≠ 'ARRAY' → blessed object. 497: # Overloaded eq was never invoked; return the object as-is. 498: return $rc;

Mutants (Total: 2, Killed: 2, Survived: 0)

499: } 500: 501: # Cache miss — call the real object 502: if(wantarray) { 503: my @result = $object->$param(); 504: return unless scalar(@result); # empty list: don't cache, return () 505: $self->_cache_set($key, \@result); 506: return @result;

Mutants (Total: 2, Killed: 2, Survived: 0)

507: } 508: 509: # Scalar / void context: cache the value (or the sentinel for undef) 510: my $val = $object->$param(); 511: $self->_cache_set($key, defined($val) ? $val : UNDEF_SENTINEL); 512: return $val; 513: } 514: 515: # Setter path — array ───────────────────────────────────────────────────── โ—516 โ†’ 516 โ†’ 526 516: if(scalar(@_) > 1) { 517: # Pass the list as an arrayref; the wrapped object stores it 518: my $val = $object->$param(\@_); 519: $self->_cache_set($key, defined($val) ? $val : UNDEF_SENTINEL); 520: return defined($val) ? @{$val} : (); 521: } 522: 523: # Setter path — scalar ──────────────────────────────────────────────────── 524: # CHI's set() returns the cache object, not the stored value; 525: # capture the value before the set call and return it directly. 526: my $val = $object->$param($_[0]); 527: $self->_cache_set($key, defined($val) ? $val : UNDEF_SENTINEL); 528: return $val; 529: } 530: 531: =head1 LIMITATIONS 532: 533: =over 4 534: 535: =item Setter arguments are not part of the cache key. 536: 537: Calls like C<< $obj->method($arg) >> use the key C<ClassName:method> regardless 538: of C<$arg>. Setters with different arguments therefore overwrite each other's 539: cache entries. For read-only caching of parameterised methods, see 540: L<Class::Simple::Readonly::Cached>. 541: 542: =item Falsy scalar return values are not cached. 543: 544: Methods that return C<0> or C<''> (false but defined) are treated as a cache 545: miss on every call: the underlying object is re-invoked each time. Only 546: C<undef> is cached (via the UNDEF_SENTINEL). If you need to cache C<0>, wrap 547: it in a container object or use a different caching strategy. 548: 549: =item Sentinel collision. 550: 551: If the wrapped object ever returns the literal string 552: C<< "Class::Simple::Cached\>UNDEF\<" >> as a scalar, or as the first element of a 553: list, the module will misinterpret it as a cached-undef marker. This string is 554: deliberately unusual, but callers working with arbitrary string data should be 555: aware of the constraint. 556: 557: =item Shared cache and multiple classes. 558: 559: When two C<Class::Simple::Cached> instances of B<different> classes share the 560: same cache object, their keys are namespaced by class name and do not collide. 561: However, C<purge()> on a CHI-style cache is global: destroying one instance will 562: purge I<all> entries from a shared CHI cache, including those of the other 563: instance. Use per-instance cache objects, or a hash-ref cache, to avoid this. 564: 565: =item Does not work with L<Memoize>. 566: 567: =item Overloaded C<eq> on cached values. 568: 569: If the wrapped object returns a blessed value that overloads the C<eq> operator, 570: the sentinel comparison in the getter uses a C<ref()> pre-check so that only 571: plain strings are compared against the sentinel. Array elements are subject to 572: the same pre-check. Callers wrapping objects that return sentinel-like strings 573: via overloading should be aware of this guard. 574: 575: =back 576: 577: =head1 AUTHOR 578: 579: Nigel Horne, C<< <njh at nigelhorne.com> >> 580: 581: =head1 BUGS 582: 583: Please report bugs and feature requests at 584: L<https://github.com/nigelhorne/Class-Simple-Cached/issues>. 585: 586: =head1 SEE ALSO 587: 588: L<Class::Simple>, L<CHI>, L<Class::Simple::Readonly::Cached> 589: 590: =over 4 591: 592: =item * L<Test Dashboard|https://nigelhorne.github.io/Class-Simple-Cached/coverage/> 593: 594: =back 595: 596: =head1 SUPPORT 597: 598: This module is provided as-is without any warranty. 599: 600: You can find documentation for this module with the perldoc command: 601: 602: perldoc Class::Simple::Cached 603: 604: =over 4 605: 606: =item * MetaCPAN: L<https://metacpan.org/release/Class-Simple-Cached> 607: 608: =item * Source: L<https://github.com/nigelhorne/Class-Simple-Cached> 609: 610: =item * CPANTS: L<http://cpants.cpanauthors.org/dist/Class-Simple-Cached> 611: 612: =item * Testers Matrix: L<http://matrix.cpantesters.org/?dist=Class-Simple-Cached> 613: 614: =back 615: 616: =head1 FORMAL SPECIFICATION 617: 618: =head2 new 619: 620: ───────────────────────────────────────────────────────────────── 621: [State] 622: cache : â„™(HashRef ∪ CHIObject) 623: object : Object 624: 625: [CHIObject] 626: can_get : Method 627: can_set : Method 628: can_purge : Method 629: 630: new ────────────────────────────────────────────────────────────── 631: Δ(cache, object) 632: cache? : HashRef ∪ CHIObject 633: object? : Object ∪ {∅} 634: ───────────────────────────────────────────────── 635: cache? ≠ ∅ 636: cache ′ = cache? 637: object ′ = (object? ≠ ∅ ⟹ object?) ∨ Class::Simple.new() 638: ───────────────────────────────────────────────── 639: 640: =head2 can 641: 642: can ────────────────────────────────────────────────── 643: Ξ(cache, object) 644: method? : MethodName 645: ───────────────────────────────────────────────── 646: result! = (method? = 'new') 647: ∨ object.can(method?) 648: ∨ SUPER::can(method?) 649: 650: =head2 isa 651: 652: isa ────────────────────────────────────────────────── 653: Ξ(cache, object) 654: class? : ClassName 655: ───────────────────────────────────────────────── 656: result! = (class? = ref(self)) 657: ∨ (class? = __PACKAGE__) 658: ∨ SUPER::isa(class?) 659: ∨ object.isa(class?) 660: 661: =head2 AUTOLOAD 662: 663: AUTOLOAD ───────────────────────────────────────────────────────── 664: Δ(cache) 665: method? : MethodName 666: args? : Seq(Any) 667: ───────────────────────────────────────────────── 668: key = ref(self) ⊕ ":" ⊕ method? 669: 670: Getter (args? = ∅): 671: (∃ v • cache_hit(key, v) ∧ v ≠ UNDEF_SENTINEL ⟹ result! = v) 672: ∨ (cache_hit(key, UNDEF_SENTINEL) ⟹ result! = undef) 673: ∨ (¬cache_hit(key, _) 674: ∧ result! = object.method?() 675: ∧ cache′ = cache ∪ {key ↦ encode(result!)}) 676: 677: Setter (args? ≠ ∅): 678: object′.method?(args?) = args? 679: cache′ = cache ∪ {key ↦ encode(object′.method?())} 680: result! = args? 681: 682: =head1 LICENCE AND COPYRIGHT 683: 684: Copyright (C) 2019-2026, Nigel Horne 685: 686: Usage is subject to the GPL2 licence terms. 687: If you use it, 688: please let me know. 689: 690: =cut 691: 692: 1;