File Coverage

File:blib/lib/Class/Simple/Cached.pm
Coverage:95.2%

linestmtbrancondsubtimecode
1package Class::Simple::Cached;
2
3
13
13
13
792678
9
120
use strict;
4
13
13
13
18
7
195
use warnings;
5
13
13
13
1225
37607
20
use autodie qw(:all);
6
7
13
13
13
53952
12
99
use Carp ();
8
13
13
13
2055
20039
150
use Class::Simple;
9
13
13
13
1314
28877
184
use Params::Get 0.15;
10
13
13
13
21
9
72
use Scalar::Util ();
11
13
13
13
2050
172881
26
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
13
13
13
980
9
304
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
13
13
13
19
4
4067
use constant _GLOBAL_PHASE_AVAILABLE => defined($^V) && ($^V ge 'v5.14.0');
20
21 - 29
=head1 NAME

Class::Simple::Cached - cache getter results for any get/set object

=head1 VERSION

Version 0.06

=cut
30
31our $VERSION = '0.06';
32
33=encoding UTF-8
34
35 - 150
=head1 SYNOPSIS

    use CHI;
    use Class::Simple::Cached;

    # Wrap an existing object with a cache layer
    my $cache = CHI->new(driver => 'RawMemory', global => 1);
    my $obj   = Class::Simple::Cached->new(
        cache  => $cache,
        object => My::Expensive::Object->new(),
    );

    $obj->name('Alice');      # setter: delegates to wrapped object, updates cache
    my $n = $obj->name();     # getter: returns cached 'Alice' without hitting object

    # Or use a plain hash ref as the cache backend
    my %store;
    my $simple = Class::Simple::Cached->new(cache => \%store);
    $simple->colour('blue');
    print $simple->colour();  # 'blue', served from %store

=head1 DESCRIPTION

A subclass of L<Class::Simple> that transparently caches the return values of
getter calls, so that repeated reads of expensive-to-compute or
expensive-to-transport values hit the cache instead of the wrapped object.

Cache coherency is I<not> automatic.  If the wrapped object's state changes
through a path other than the cached wrapper, callers must invalidate the cache
themselves.

=head1 SUBROUTINES/METHODS

=head2 new

Constructs a C<Class::Simple::Cached> instance that wraps C<object> behind
C<cache>.

=head3 ARGUMENTS

=over 4

=item C<cache> (mandatory)

Either a blessed object implementing C<get($key)>, C<set($key, $val, $expires)>,
and C<purge()> (e.g. any L<CHI> driver); or a plain hash reference used as an
in-process store.

=item C<object> (optional)

The object whose methods will be proxied and cached.  Defaults to a fresh
C<Class::Simple> instance.

=back

Calling C<< ->new() >> on an already-blessed instance returns a shallow clone
(all stored fields are merged, including the existing cache handle).

=head3 RETURNS

A blessed C<Class::Simple::Cached> instance.

=head3 SIDE EFFECTS

None beyond allocating the new instance.

=head3 EXAMPLE

    # CHI-backed cache
    use CHI;
    my $obj = Class::Simple::Cached->new(
        cache  => CHI->new(driver => 'RawMemory', global => 1),
        object => My::Model->new(),
    );

    # Hash-ref cache (useful for tests or short-lived objects)
    my $obj = Class::Simple::Cached->new(cache => {});

    # Clone an existing wrapped object
    my $clone = $obj->new();

=head3 API SPECIFICATION

    Input:
      cache  => HashRef | CHICompatibleObject  # required
      object => Object                          # optional

    Output:
      Class::Simple::Cached instance

=head3 MESSAGES

    Message                                              Meaning                              Resolution
    ---------------------------------------------------  -----------------------------------  ----------------------------------------
    "use ->new() not ::new() to instantiate"             Called as Class::Simple::Cached::new  Use $obj->new() or ClassName->new()
    "Usage: $class->new(cache => \$cache)"               No arguments supplied                 Pass at least cache => ...
    "Cache must be ref to HASH or object"                cache is a plain scalar or wrong ref  Use a hashref or CHI-compatible object
    "Cache object must implement get, set, purge"        Blessed cache lacks required methods  Use a fully CHI-compatible object

=head3 PSEUDOCODE

    new(class, args):
      IF class undefined   â†’ carp and return undef
      IF class is blessed  â†’ merge fields, return shallow clone
      IF no args           â†’ croak Usage message
      PARSE args into hashref via Params::Get
      IF params.object absent → params.object = Class::Simple->new()
      IF params.cache is a blessed object:
        VERIFY it can('get') AND can('set') AND can('purge')
        IF not → croak capability message
        RETURN bless params, class
      IF params.cache is a HASH ref:
        RETURN bless params, class
      croak "Cache must be ref to HASH or object"

=cut
151
152sub new
153{
154
218
933195
        my $class = shift;
155
156        # Guard: always call as a method, not a bare function
157
218
205
        if(!defined($class)) {
158
3
8
                Carp::carp(__PACKAGE__, ' use ->new() not ::new() to instantiate');
159
3
293
                return;
160        }
161
162        # When called on a blessed instance, return a shallow clone
163
215
209
        if(Scalar::Util::blessed($class)) {
164
16
14
                my $params = Params::Get::get_params(undef, \@_) || {};
165
16
16
16
135
14
21
                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
16
22
                $merged{'_is_hash_cache'} = ref($merged{'cache'}) eq 'HASH';
170
16
16
                $merged{'_cache_prefix'}  = ref($class) . ':';
171
172                # Validate the (potentially-replaced) cache object using the same rules
173                # as new() — the clone path must not produce an unusable object.
174
16
21
                if(!$merged{'_is_hash_cache'}) {
175
3
4
                        if(Scalar::Util::blessed($merged{'cache'})) {
176
1
4
                                unless($merged{'cache'}->can('get')
177                                        && $merged{'cache'}->can('set')
178                                        && $merged{'cache'}->can('purge'))
179                                {
180
1
3
                                        Carp::croak("Cache object must implement 'get', 'set', and 'purge' methods");
181                                }
182                        } else {
183
2
5
                                Carp::croak(ref($class) . ': Cache must be ref to HASH or object');
184                        }
185                }
186
187
13
24
                return bless \%merged, ref($class);
188        }
189
190        # Require at least one argument so Params::Get's confess is never reached;
191        # we want croak (Test::Carp-compatible) not confess.
192
199
142
        if(scalar(@_) == 0) {
193
5
23
                Carp::croak('Usage: ', $class, '->new(cache => $cache)');
194        }
195
196
194
203
        my $params = Params::Get::get_params('cache', @_) || {};
197
198        # Default the wrapped object to a bare Class::Simple instance
199
194
1951
        $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
194
4129
        $params->{'_is_hash_cache'} = ref($params->{'cache'}) eq 'HASH';
204
194
135
        $params->{'_cache_prefix'}  = "$class:";
205
206
194
193
        if(Scalar::Util::blessed($params->{'cache'})) {
207                # Verify the cache object speaks the required interface
208
36
147
                unless($params->{'cache'}->can('get')
209                        && $params->{'cache'}->can('set')
210                        && $params->{'cache'}->can('purge'))
211                {
212
14
41
                        Carp::croak("Cache object must implement 'get', 'set', and 'purge' methods");
213                }
214
22
29
                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
158
110
        if($params->{'_is_hash_cache'}) {
220
130
117
                return bless $params, $class;
221        }
222
223
28
81
        Carp::croak("$class: Cache must be ref to HASH or object");
224}
225
226 - 255
=head2 can

Reports whether this wrapper (or its embedded object) can handle a method.

=head3 ARGUMENTS

=over 4

=item C<$method> — the method name to probe.

=back

=head3 RETURNS

True if the method is known; false otherwise.

=head3 EXAMPLE

    $obj->can('name');   # true if the wrapped object has a name() method

=head3 API SPECIFICATION

    Input:  method_name : Str
    Output: Bool

=head3 MESSAGES

    None.

=cut
256
257sub can
258{
259
47
1878
        my ($self, $method) = @_;
260
261        # Undefined method name is outside the documented API; return undef cleanly.
262
47
46
        return unless defined $method;
263
264        # When called as a class method there is no wrapped object to probe
265
46
100
        return $self->SUPER::can($method) unless Scalar::Util::blessed($self);
266
267        return ($method eq 'new')
268
16
57
                || $self->{'object'}->can($method)
269                || $self->SUPER::can($method);
270}
271
272 - 301
=head2 isa

Reports whether this wrapper or its embedded object is of a given class.

=head3 ARGUMENTS

=over 4

=item C<$class> — the class name to test.

=back

=head3 RETURNS

True if the wrapper or the wrapped object is-a C<$class>.

=head3 EXAMPLE

    $obj->isa('My::Model');   # delegates to the wrapped object

=head3 API SPECIFICATION

    Input:  class_name : Str
    Output: Bool

=head3 MESSAGES

    None.

=cut
302
303sub isa
304{
305
39
2100
        my ($self, $class) = @_;
306
307        # Undefined class name is outside the documented API; return undef cleanly.
308
39
31
        return unless defined $class;
309
310        # When called as a class method there is no wrapped object to interrogate
311
38
52
        return $self->SUPER::isa($class) unless Scalar::Util::blessed($self);
312
313
35
88
        return 1 if $class eq ref($self)
314                || $class eq __PACKAGE__
315                || $self->SUPER::isa($class);
316
317
11
25
        return $self->{'object'}->isa($class);
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.
330sub DESTROY
331{
332
168
56133
        my $self = shift;
333
334
168
156
        my $cache = $self->{'cache'} or return;
335
336
165
141
        if($self->{'_is_hash_cache'}) {
337                # Remove only this class's keys to avoid stomping on siblings sharing the hash
338
143
83
                my $prefix = $self->{'_cache_prefix'};
339
143
605
143
76
449
166
                delete $cache->{$_} for grep { index($_, $prefix) == 0 } keys %{$cache};
340
143
287
                return;
341        }
342
343        # Skip purge during global destruction to avoid order-of-destruction crashes
344
22
27
        return if _GLOBAL_PHASE_AVAILABLE && ${^GLOBAL_PHASE} eq 'DESTRUCT';
345
346
22
26
        $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).
351sub _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
13
13
13
27
8
113
}
357
358sub _cache_set :Protected
359{
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
13
13
13
1047
10
89
}
368
369 - 459
=head2 AUTOLOAD (getter/setter proxy)

Intercepts every method call that is not explicitly defined, proxying it to
the wrapped object with a caching layer for zero-argument (getter) calls.

Setter calls (one or more arguments) always pass through to the wrapped object
and update the cache with the new value.

=head3 ARGUMENTS

    $method()        â€” getter: returns cached value if present, else calls object
    $method($scalar) — scalar setter: stores scalar, updates cache
    $method(@list)   â€” array setter: stores list, updates cache

=head3 RETURNS

The value returned by the wrapped object (or the cached copy thereof).

=head3 SIDE EFFECTS

=over 4

=item * Getter: may write to the cache on first call.

=item * Setter: writes to both the wrapped object and the cache.

=item * DESTROY: clears cache entries for this instance (see above).

=back

=head3 EXAMPLE

    $obj->colour('red');    # setter — writes 'red' to object and cache
    $obj->colour();         # getter — returns 'red' from cache

=head3 API SPECIFICATION

    Input (getter):  method_name : Str,  args : ()
    Input (setter):  method_name : Str,  args : (Scalar | List)
    Output:          the stored/retrieved value or list

=head3 MESSAGES

    Message                           Meaning                             Resolution
    --------------------------------  ----------------------------------  ----------------------------------------
    "$method" (croak)                 Cached array's first element is     Do not store the sentinel string
                                      the UNDEF_SENTINEL string           as a real value in the wrapped object

=head3 PSEUDOCODE

    AUTOLOAD(method, args...):
      key = ref(self) + ":" + method

      IF no args (getter mode):
        val = cache_get(key)
        IF cache hit:
          IF val is a plain string (not a ref):
            IF val is UNDEF_SENTINEL → return undef
            RETURN val
          IF val is an arrayref:
            IF first element is a plain string AND equals UNDEF_SENTINEL → croak
            RETURN dereferenced list
          RETURN val (blessed object)
        # Cache miss — ask the wrapped object
        IF list context:
          result_list = object->method()
          IF empty      â†’ return ()
          cache_set(key, \result_list)
          RETURN result_list
        # Scalar context
        result = object->method()
        IF defined:
          cache_set(key, result)
          RETURN result
        cache_set(key, UNDEF_SENTINEL)
        RETURN undef

      ELSE (setter mode):
        IF more than one arg (array setter):
          val = object->method(\@args)     # wrapped object stores arrayref
          IF defined:
            cache_set(key, val)
            RETURN @val
          cache_set(key, UNDEF_SENTINEL)
          RETURN undef
        ELSE (scalar setter):
          val = object->method(args[0])
          cache_set(key, val // UNDEF_SENTINEL)
          RETURN val

=cut
460
461sub AUTOLOAD
462{
463
735
19921
        our $AUTOLOAD;
464        # rindex+substr avoids regex engine startup on every method dispatch
465
735
448
        my $param = substr($AUTOLOAD, rindex($AUTOLOAD, '::') + 2);
466
467
735
351
        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
735
390
        my $key  = $self->{'_cache_prefix'} . $param;
471
735
322
        my $object = $self->{'object'};
472
473        # Getter path ─────────────────────────────────────────────────────────────
474
735
467
        if(scalar(@_) == 0) {
475
155
162
                my $rc = $self->_cache_get($key);
476
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
154
723
                if($rc) {
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.
485
103
90
                        if(!ref($rc)) {
486                                # Conclusion: plain scalar — only type that can equal the sentinel.
487
80
67
                                return if $rc eq UNDEF_SENTINEL;
488
68
100
                                return $rc;
489                        } elsif(ref($rc) eq 'ARRAY') {
490                                # Premise: !ref($rc) was false, so ref($rc) is truthy; no need
491                                # to re-test ref() — the elsif is the deductive next step.
492
18
51
                                Carp::croak($param)
493                                        if !ref($rc->[0]) && $rc->[0] eq UNDEF_SENTINEL;
494
14
14
9
20
                                return @{$rc};
495                        }
496                        # Conclusion: ref($rc) is truthy and ≠ 'ARRAY' → blessed object.
497                        # Overloaded eq was never invoked; return the object as-is.
498
5
5
                        return $rc;
499                }
500
501                # Cache miss — call the real object
502
51
45
                if(wantarray) {
503
19
21
                        my @result = $object->$param();
504
19
78
                        return unless scalar(@result);  # empty list: don't cache, return ()
505
10
15
                        $self->_cache_set($key, \@result);
506
10
206
                        return @result;
507                }
508
509                # Scalar / void context: cache the value (or the sentinel for undef)
510
32
39
                my $val = $object->$param();
511
31
201
                $self->_cache_set($key, defined($val) ? $val : UNDEF_SENTINEL);
512
30
274
                return $val;
513        }
514
515        # Setter path — array ─────────────────────────────────────────────────────
516
580
303
        if(scalar(@_) > 1) {
517                # Pass the list as an arrayref; the wrapped object stores it
518
12
25
                my $val = $object->$param(\@_);
519
12
171
                $self->_cache_set($key, defined($val) ? $val : UNDEF_SENTINEL);
520
12
10
149
13
                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
568
751
        my $val = $object->$param($_[0]);
527
567
1919
        $self->_cache_set($key, defined($val) ? $val : UNDEF_SENTINEL);
528
566
533
        return $val;
529}
530
531 - 690
=head1 LIMITATIONS

=over 4

=item Setter arguments are not part of the cache key.

Calls like C<< $obj->method($arg) >> use the key C<ClassName:method> regardless
of C<$arg>.  Setters with different arguments therefore overwrite each other's
cache entries.  For read-only caching of parameterised methods, see
L<Class::Simple::Readonly::Cached>.

=item Falsy scalar return values are not cached.

Methods that return C<0> or C<''> (false but defined) are treated as a cache
miss on every call: the underlying object is re-invoked each time.  Only
C<undef> is cached (via the UNDEF_SENTINEL).  If you need to cache C<0>, wrap
it in a container object or use a different caching strategy.

=item Sentinel collision.

If the wrapped object ever returns the literal string
C<< "Class::Simple::Cached\>UNDEF\<" >> as a scalar, or as the first element of a
list, the module will misinterpret it as a cached-undef marker.  This string is
deliberately unusual, but callers working with arbitrary string data should be
aware of the constraint.

=item Shared cache and multiple classes.

When two C<Class::Simple::Cached> instances of B<different> classes share the
same cache object, their keys are namespaced by class name and do not collide.
However, C<purge()> on a CHI-style cache is global: destroying one instance will
purge I<all> entries from a shared CHI cache, including those of the other
instance.  Use per-instance cache objects, or a hash-ref cache, to avoid this.

=item Does not work with L<Memoize>.

=item Overloaded C<eq> on cached values.

If the wrapped object returns a blessed value that overloads the C<eq> operator,
the sentinel comparison in the getter uses a C<ref()> pre-check so that only
plain strings are compared against the sentinel.  Array elements are subject to
the same pre-check.  Callers wrapping objects that return sentinel-like strings
via overloading should be aware of this guard.

=back

=head1 AUTHOR

Nigel Horne, C<< <njh at nigelhorne.com> >>

=head1 BUGS

Please report bugs and feature requests at
L<https://github.com/nigelhorne/Class-Simple-Cached/issues>.

=head1 SEE ALSO

L<Class::Simple>, L<CHI>, L<Class::Simple::Readonly::Cached>

=over 4

=item * L<Test Dashboard|https://nigelhorne.github.io/Class-Simple-Cached/coverage/>

=back

=head1 SUPPORT

This module is provided as-is without any warranty.

You can find documentation for this module with the perldoc command:

    perldoc Class::Simple::Cached

=over 4

=item * MetaCPAN: L<https://metacpan.org/release/Class-Simple-Cached>

=item * Source: L<https://github.com/nigelhorne/Class-Simple-Cached>

=item * CPANTS: L<http://cpants.cpanauthors.org/dist/Class-Simple-Cached>

=item * Testers Matrix: L<http://matrix.cpantesters.org/?dist=Class-Simple-Cached>

=back

=head1 FORMAL SPECIFICATION

=head2 new

    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    [State]
      cache  : â„™(HashRef ∪ CHIObject)
      object : Object

    [CHIObject]
      can_get   : Method
      can_set   : Method
      can_purge : Method

    new ──────────────────────────────────────────────────────────────
    Î”(cache, object)
    cache? : HashRef ∪ CHIObject
    object? : Object ∪ {∅}
    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    cache? ≠ ∅
    cache ′ = cache?
    object ′ = (object? ≠ ∅ ⟹ object?) ∨ Class::Simple.new()
    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

=head2 can

    can ──────────────────────────────────────────────────
    Îž(cache, object)
    method? : MethodName
    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    result! = (method? = 'new')
            âˆ¨ object.can(method?)
            âˆ¨ SUPER::can(method?)

=head2 isa

    isa ──────────────────────────────────────────────────
    Îž(cache, object)
    class? : ClassName
    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    result! = (class? = ref(self))
            âˆ¨ (class? = __PACKAGE__)
            âˆ¨ SUPER::isa(class?)
            âˆ¨ object.isa(class?)

=head2 AUTOLOAD

    AUTOLOAD ─────────────────────────────────────────────────────────
    Î”(cache)
    method? : MethodName
    args?   : Seq(Any)
    â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€
    key = ref(self) ⊕ ":" ⊕ method?

    Getter (args? = ∅):
      (∃ v • cache_hit(key, v) ∧ v ≠ UNDEF_SENTINEL ⟹ result! = v)
      âˆ¨ (cache_hit(key, UNDEF_SENTINEL)              âŸ¹ result! = undef)
      âˆ¨ (¬cache_hit(key, _)
          âˆ§ result! = object.method?()
          âˆ§ cache′ = cache ∪ {key ↦ encode(result!)})

    Setter (args? ≠ ∅):
      object′.method?(args?) = args?
      cache′ = cache ∪ {key ↦ encode(object′.method?())}
      result! = args?

=head1 LICENCE AND COPYRIGHT

Copyright (C) 2019-2026, Nigel Horne

Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.

=cut
691
6921;