NAME
Class::Simple::Readonly::Cached - cache messages to an object
VERSION
Version 0.13
SYNOPSIS
A caching decorator for Class::Simple-based (and arbitrary) objects.
It is up to the caller to maintain the cache if the object comes out of sync with the cache, for example by changing its state.
use Class::Simple::Readonly::Cached;
my $obj = Class::Simple->new();
$obj->val('foo');
my $cached = Class::Simple::Readonly::Cached->new(
object => $obj,
cache => {},
);
my $val = $cached->val(); # calls the real object
my $val2 = $cached->val(); # served from cache
$val = $cached->val(a => 'b'); # args form part of the cache key
Note that when the object goes out of scope (DESTROY is called), the cache is cleared automatically.
DESCRIPTION
Wraps any Perl object in a transparent caching layer. Every method call is intercepted via AUTOLOAD; on the first call (a miss) the result is stored in the cache and returned. Subsequent identical calls (same method name, same argument list) are hits and are served directly from the cache without touching the inner object.
Two cache backends are supported: a plain hash reference (fast, in-process, no expiry) and any CHI-compatible object (persistent, shared, with expiry).
SUBROUTINES/METHODS
new
Construct a caching proxy around any Perl object.
Arguments
-
cache(mandatory)Either a plain hash reference (
{}) or a CHI-compatible object that implementsget(),set(), andpurge(). -
object(optional)The object to wrap. Defaults to a bare Class::Simple instance. Must be a reference; a plain scalar argument causes a
carpand anundefreturn. Wrapping an already-wrappedClass::Simple::Readonly::Cachedobject returns the existing wrapper with a warning. -
quiet(optional, boolean)Suppress the double-wrap warning when non-zero.
Returns
A Class::Simple::Readonly::Cached object, or undef on invalid
object. Croaks on invalid cache.
EXAMPLE
use CHI;
use Class::Simple::Readonly::Cached;
# --- Hash-ref cache (in-process, no expiry) ---
my $obj = My::Expensive->new();
my $cached = Class::Simple::Readonly::Cached->new(
object => $obj,
cache => {},
);
my $result = $cached->compute(); # calls the real object
my $result2 = $cached->compute(); # from cache -- object not called
# --- CHI cache (persistent, file-based) ---
use File::Temp qw(tempdir);
my $chi = CHI->new(driver => 'File', root_dir => tempdir(CLEANUP => 1));
my $cached2 = Class::Simple::Readonly::Cached->new(
object => $obj,
cache => $chi,
);
# --- Clone an existing wrapper ---
my $clone = $cached->new(); # shares the same inner object and cache
API SPECIFICATION
# Input
{
cache => { type => ['hashref', 'object'], required => 1 },
object => { type => 'ref', optional => 1 },
quiet => { type => 'bool', optional => 1 },
}
# Output
{ type => 'object', class => 'Class::Simple::Readonly::Cached',
optional => 1 }
MESSAGES
Message Meaning Resolution
------- ------- ----------
Cache must be ref to HASH or object cache is not a hashref or blessed Pass \%hash or a CHI object.
object
Cache object must implement get(), set(), and purge() blessed cache lacks required API Use a CHI-compatible object.
$object must be a reference, not a scalar object is a plain string Pass a blessed reference.
warning: $object is already a cached object wrapping an already-wrapped object Reuse the returned wrapper.
$object is already cached at LINE of FILE double-wrap detected Reuse the existing wrapper;
set quiet => 1 to silence.
PSEUDOCODE
1. If class is undef: carp and return undef (::new() misuse)
2. If class is blessed: merge params into a clone and return
3. Validate cache: croak if not a hashref or CHI-compatible object
4. Validate object: carp+return if scalar; return existing
wrapper if already __PACKAGE__
5. Create inner object: Class::Simple->new(non-wrapper params)
unless object was supplied
6. Check double-wrap registry: if object in %cached, carp and return
existing wrapper (unless quiet)
7. Bless and register: bless $params, $class; set _class = $class;
call _build_cache_accessors to install
_get/_set coderefs and _cache_is_hash;
store in %cached with caller file and line
8. Return $self
object
Return the inner (wrapped) object.
Returns
The blessed reference that was passed as object to new().
EXAMPLE
# Bypass the cache to mutate state directly.
$cached->object()->reset();
API SPECIFICATION
# Input none
# Output { type => 'object' }
MESSAGES
(none)
state
Return a snapshot of cache hit and miss counts per cache key. Primarily useful for performance profiling and white-box tests.
Returns
A hash reference:
-
hitsHash reference mapping each cache key to the number of times the result was served from cache.
undefuntil the first hit. -
missesHash reference mapping each cache key to the number of times the inner object was actually invoked.
undefuntil the first miss.
EXAMPLE
my $s = $cached->state();
my $hits = do { my $n=0; $n += $_ for values %{$s->{hits} // {}}; $n };
my $misses = do { my $n=0; $n += $_ for values %{$s->{misses} // {}}; $n };
printf "Hit rate: %.0f%%\n", 100 * $hits / ($hits + $misses) if $hits + $misses;
API SPECIFICATION
# Input None
# Output { type => 'hashref',
# keys => { hits => 'hashref|undef',
# misses => 'hashref|undef' } }
MESSAGES
(none)
can
Report whether the inner object (or this class) can respond to a
given method. Overrides UNIVERSAL::can to account for the
decorator pattern.
Returns
A code reference if the method exists, undef otherwise.
EXAMPLE
my $code = $cached->can('compute');
$code->($cached) if $code;
API SPECIFICATION
# Input { self => { type => 'object|string' },
# method => { type => 'string' } }
# Output { type => 'coderef|undef' }
MESSAGES
(none)
isa
Test class membership, delegating to the inner object's class
hierarchy when needed. Overrides UNIVERSAL::isa to support the
transparent decorator pattern.
Returns
True if the wrapper or its inner object is-a $class.
EXAMPLE
$cached->isa('My::Domain::Object'); # true if inner object is
API SPECIFICATION
# Input { self => { type => 'object|string' },
# class => { type => 'string' } }
# Output { type => 'bool' }
MESSAGES
(none)
AUTOLOAD
Not called directly. Intercepts every method call not explicitly defined in this package, looks up the result in the cache, and on a miss proxies the call to the inner object and stores the result.
Cache lookup and storage use the pre-built _get/_set coderefs
installed by _build_cache_accessors at construction time, so the
backend-type decision (HASH vs CHI) is made once -- never on each
dispatch.
Three stored-value forms are mutually exclusive and exhaustive:
-
ARRAY ref
The wrapped method previously returned a list. Served as
@arrayin list context, or$array[-1]in scalar context. -
$UNDEF_SENTINELThe wrapped method returned
undefor an empty list. Stored as the sentinel string so a cache miss (undefined value) can be distinguished from a cachedundef. -
Any other defined scalar
The wrapped method returned a plain scalar in scalar context. Served as-is in scalar context. If the caller subsequently asks for the same key in list context, the scalar cannot be adapted -- the call is treated as a miss and the method is re-invoked so the array form gets independently cached.
Handles DESTROY specially: removes the wrapper from the double-wrap
registry and clears cache entries whose keys begin with $self-{_class}>
(Invariant I3 guarantees this is always set), then returns without
calling the inner object's DESTROY.
LIMITATIONS
-
Not safe for mutable objects
The cache is never invalidated automatically. If the inner object's state changes after caching, the wrapper will return stale data. The caller must either reset the cache manually or avoid using this module with objects that mutate.
-
Argument serialisation is naive
Cache keys are built by joining defined arguments with
::. Two different argument lists can therefore produce the same key if an argument itself contains::(e.g.foo('a::b', 'c')vsfoo('a', 'b::c')). Callers that pass arguments containing::should use a CHI backend with a custom key serialiser. -
Undefined arguments are collapsed
Undefined values in the argument list are silently dropped from the cache key, so
foo(undef)andfoo()share a cache entry. -
Scalar-then-list context mismatch is a miss
If a method is first called in scalar context and then in list context with identical arguments, the second call is a cache miss and re-invokes the inner object. Both results are then independently cached.
-
can('new')returns a code reference, not a booleanFor strict correctness
canreturns\&newfor the'new'method rather than the boolean1. The code reference is callable but callers who compare it with==to1will see a mismatch. -
Does not work with Memoize
Memoizeintercepts at the symbol-table level and conflicts with theAUTOLOADdispatch used here.
AUTHOR
Nigel Horne, <njh at nigelhorne.com>
BUGS
Please report any bugs or feature requests to https://github.com/nigelhorne/Class-Simple-Readonly-Cached/issues.
SEE ALSO
-
Values are shared between
Class::Simple::Readonly::Cachedobjects, since they are read-only.
SUPPORT
This module is provided as-is without any warranty.
You can find documentation for this module with the perldoc command.
perldoc Class::Simple::Readonly::Cached
-
MetaCPAN
-
Source Repository
-
CPAN Testers
http://matrix.cpantesters.org/?dist=Class-Simple-Readonly-Cached
FORMAL SPECIFICATION
new
new : (C x P) -> (W | undef)
C = class name string
P = { cache : (HashRef | CacheObj), object? : Ref, quiet? : Bool, ... }
W = blessed P in C
valid_cache(c) :=
ref(c) = 'HASH'
OR ( blessed(c) AND c.can('get') AND c.can('set') AND c.can('purge') )
Precondition:
valid_cache(P.cache)
Post-construction invariants (hold for all W returned by new()):
W._class = C
W._cache_is_hash = (ref(P.cache) = 'HASH')
W._get = λk. (W._cache_is_hash ? W.cache[k] : W.cache.get(k))
W._set = λ(k,v). (W._cache_is_hash ? W.cache[k]:=v
: W.cache.set(k,v,'never'))
Double-wrap invariant:
forall o in Dom(cached): new(C, {object: o, ...}) = cached[o].object
Clone (object invocation):
forall w : W: w.new(P') = bless( merge(w, P'), ref(w) )
Corollary: if cache in Dom(P'), rebuild _get/_set/_cache_is_hash for P'.cache
object
object : W -> Ref
forall w : W: object(w) = w.object
state
state : W -> HashRef
forall w : W: state(w) = { hits => w._hits, misses => w._misses }
can
can : (W|Str x Str) -> (CodeRef | undef)
forall w : W, m : Str:
can(w, 'new') = \&new
can(w, m) = w.object.can(m) OR SUPER::can(w, m)
isa
isa : (W x Str) -> Bool
forall w : W, c : Str:
isa(w, c) = 1 if c in { ref(w), 'Class::Simple::Readonly::Cached' }
| 1 if SUPER::isa(w, c)
| w.object.isa(c) if ref(w)
| 0 otherwise
autoload
autoload : (W x M x A*) -> R
M = method name string
A* = argument tuple (possibly empty)
R = scalar | list | undef
Cache key:
k(w, m, a) := w._class ++ '::' ++ m ++ '::' ++ defined_args(a)
(w._class = ref(w), pre-computed once in new() to avoid ref() per dispatch)
Caching law:
get(cache(w), k(w,m,a)) = v, v != undef
=> autoload(w, m, a) = v (cache hit)
get(cache(w), k(w,m,a)) = undef
=> v = w.object.m(a)
set(cache(w), k(w,m,a), v)
autoload(w, m, a) = v (cache miss)
LICENSE AND COPYRIGHT
Author Nigel Horne: njh@nigelhorne.com
Copyright (C) 2019-2026 Nigel Horne
Usage is subject to the GPL2 licence terms. If you use it, please let me know.