Shareable
DivergentInherit from it to get save-yourself-to-a-file — where the store file is .raku text that gets EVALed back.
- Version
0.0.1zef:tbrowder- Depends
Storable::Lite- License
- Artistic-2.0
- Its own test suite
- 2 files, green
- Checked
- 2026-09-15 against Raku++ 3.28.0 and Rakudo 2026.08
Install it #
$ rakupp install Shareablezef install Shareable writes the same store; either installer leaves the module usable by both engines.
What it is for #
Persisting a small object between runs without reaching for JSON or a database. Inherit from Shareable, call .save, and the object's .raku goes to a file; .from-store reads it back with EVAL.
Saving and loading #
use Shareable;
class Note is Shareable { has Str $.title; has Int $.n }
my $f = $*TMPDIR.add("shareable-{$*PID}.store");
LEAVE $f.unlink;
my $a = Note.new(title => 'hello', n => 3);
$a.to-file($f.Str);
say 'file contents : ', $f.slurp.trim;
say '';
my $b = $a.from-file($f.Str);
say 'round-tripped : ', $b.raku;
say ' same class : ', $b.^name;
say ' equal : ', $b.title eq $a.title && $b.n == $a.n;
say ' identical : ', $b === $a;
say '';
say 'from-file returns a NEW object and leaves self untouched — it is not';
say 'an in-place load.';file contents : Note.new(title => "hello", n => 3)
round-tripped : Note.new(title => "hello", n => 3)
same class : Note
equal : True
identical : False
from-file returns a NEW object and leaves self untouched — it is not
an in-place load.The store file #
use Shareable;
class Note is Shareable { has Str $.title }
say 'the module keeps ONE module-level store path, shared by every';
say 'instance and subclass, defaulting to $HOME/.Shareable-store-file.';
say '';
say ' .show-store reads it';
say ' .save writes self to it';
say ' .store is the same method under another name';
say ' .from-store reads it back';
say '';
say 'and .new(:store-file(...)) does NOTHING. The `multi new(:$store-file)`';
say 'in the class body has no `method` keyword, so it is a lexical SUB,';
say 'not a constructor candidate — .new falls through to Mu.new and the';
say 'named argument is dropped:';
say ' ', Note.new(store-file => '/tmp/mine').show-store.IO.basename;the module keeps ONE module-level store path, shared by every
instance and subclass, defaulting to $HOME/.Shareable-store-file.
.show-store reads it
.save writes self to it
.store is the same method under another name
.from-store reads it back
and .new(:store-file(...)) does NOTHING. The `multi new(:$store-file)`
in the class body has no `method` keyword, so it is a lexical SUB,
not a constructor candidate — .new falls through to Mu.new and the
named argument is dropped:
.Shareable-store-fileThe one thing to know #
set-store-file($path) deletes your existing store file before adopting the new one.
use Shareable;
class Note is Shareable { has Str $.title }
my $dir = $*TMPDIR.add("shareable2-{$*PID}");
LEAVE { $dir.add('a.store').unlink; $dir.add('b.store').unlink; $dir.rmdir }
$dir.mkdir;
my $a = $dir.add('a.store');
my $b = $dir.add('b.store');
my $n = Note.new(title => 'kept');
$n.to-file($a.Str);
say 'file A written : ', $a.e;
$n.set-store-file($b.Str);
say 'after set-store-file(B):';
say ' store is now : ', $n.show-store.IO.basename;
say ' file A : ', $a.e ?? 'still there' !! 'GONE';
say '';
say 'the source is method set-store-file($path) { unlink $storefile;';
say '$storefile = $path }. Nothing in the name suggests a deletion.';
say '';
say 'and the FIRST call in a fresh program unlinks whatever is at';
say '$HOME/.Shareable-store-file, because that is the default. "Point this';
say 'object at my own file" is spelled "delete the shared default store,';
say 'then point at my own file".';file A written : True
after set-store-file(B):
store is now : b.store
file A : still there
the source is method set-store-file($path) { unlink $storefile;
$storefile = $path }. Nothing in the name suggests a deletion.
and the FIRST call in a fresh program unlinks whatever is at
$HOME/.Shareable-store-file, because that is the default. "Point this
object at my own file" is spelled "delete the shared default store,
then point at my own file".A store file is executable code #
use Shareable;
class Note is Shareable { has Str $.title }
my $f = $*TMPDIR.add("shareable3-{$*PID}.store");
LEAVE $f.unlink;
$f.spurt('do { say " >>> code in the store file just ran <<<"; 42 }');
say 'loading a store file that contains an expression:';
my $r = Note.from-file($f.Str);
say ' result = ', $r.raku;
say '';
say 'Storable::Lite deserialises with EVAL under MONKEY-SEE-NO-EVAL.';
say 'Never point from-file or from-store at a file you did not write.';
say '';
say 'a missing file returns Bool::False after a warning — not Nil, not a';
say 'Failure — so `my $obj = $s.from-store;` silently yields False and';
say 'blows up somewhere else later:';
say ' from-file(missing) : ', Note.from-file('/no/such/store').raku;loading a store file that contains an expression:
>>> code in the store file just ran <<<
result = 42
Storable::Lite deserialises with EVAL under MONKEY-SEE-NO-EVAL.
Never point from-file or from-store at a file you did not write.
a missing file returns Bool::False after a warning — not Nil, not a
Failure — so `my $obj = $s.from-store;` silently yields False and
blows up somewhere else later:
from-file(missing) : Bool::FalseWhere the two engines differ #
Closure attributes come back as empty stubs on both — .raku renders a Sub as sub { ... }, which EVALs into a live Sub whose body is the yada stub. Calling it then diverges: Raku++ throws X::StubCode, Rakudo returns a Failure that is easy to swallow. Keep code out of anything you store.
use Shareable;
say 'the other divergence is scoping: `use Shareable` re-exports its';
say 'dependency`s exports under Raku++ and not under Rakudo.';
say '';
say ' after `use Shareable` alone:';
say ' FileStore, to-file and from-file as SUBS are visible on Raku++';
say ' and "Undeclared name" on Rakudo';
say '';
say 'so code written against Raku++ that calls to-file($path, $obj) as a';
say 'sub does not compile on Rakudo. Use the METHODS the class gives you,';
say 'which are the same on both:';
class Note is Shareable { has Str $.title }
my $f = $*TMPDIR.add("shareable4-{$*PID}.store");
LEAVE $f.unlink;
my $n = Note.new(title => 'portable');
$n.to-file($f.Str);
say ' .to-file / .from-file : ', Note.from-file($f.Str).title;the other divergence is scoping: `use Shareable` re-exports its
dependency`s exports under Raku++ and not under Rakudo.
after `use Shareable` alone:
FileStore, to-file and from-file as SUBS are visible on Raku++
and "Undeclared name" on Rakudo
so code written against Raku++ that calls to-file($path, $obj) as a
sub does not compile on Rakudo. Use the METHODS the class gives you,
which are the same on both:
.to-file / .from-file : portableOne last shape: Shareable itself declares no attributes, so Shareable.new.raku is just Shareable.new. All the state is yours.