← All modules

Distribution · IO

String::Stream

Divergent

A one-class in-memory sink you print into and read back — and, without inheriting anything, a drop-in $*OUT.

Version
0.8 github:jaffa4
Depends
none beyond the core
License
none stated
Its own test suite
1 file, green
Checked
2026-09-15 against Raku++ 3.28.0 and Rakudo 2026.08
Where it lives
raku.land · source

Install it #

$ rakupp install String::Stream

zef install String::Stream writes the same store; either installer leaves the module usable by both engines.

What it is for #

Somewhere between $text ~= … and opening a real file sits the thing that looks like a handle and keeps its bytes in memory. This distribution is 35 lines of exactly that: a class with print, say and get, over one Str buffer.

Using it #

File
use String::Stream;

my $s = String::Stream.new('');
$s.print('a', 'b');
$s.print('c');
$s.say(42);
$s.print('e');

say 'buffer     : ', $s.get.raku;
say 'read twice : ', $s.get.raku;
say '';
say '.print joins its arguments with NO separator; .say appends "\n".';
say '.get does not consume — read it twice and you get the history twice.';
say '';
my $seeded = String::Stream.new('seed:');
$seeded.print('x');
say 'new(Str)   : ', $seeded.get.raku;
Output
buffer     : "abc42\ne"
read twice : "abc42\ne"

.print joins its arguments with NO separator; .say appends "\n".
.get does not consume — read it twice and you get the history twice.

new(Str)   : "seed:x"

A fresh stream's buffer is Any, not '' — there is no attribute default, so .get.chars on an untouched stream blows up. Seed it with .new('').

File
use String::Stream;

my $fresh = String::Stream.new;
say 'a fresh .get : ', $fresh.get.raku, '   (an undefined Str, not "")';
say '';
say '.buffer is a read-only public accessor:';
say '  reading it : ', String::Stream.new('x').buffer.raku;
my $ok = try { $fresh.buffer = 'zap'; True };
say '  writing it : ', $ok ?? 'succeeded' !! 'refused';
say '';
say '.flush exists and its body is EMPTY. It does not clear, drain or';
say 'commit anything — the name implies otherwise:';
my $f = String::Stream.new('');
$f.print('still here');
$f.flush;
say '  after .flush : ', $f.get.raku;
Output
a fresh .get : Any   (an undefined Str, not "")

.buffer is a read-only public accessor:
  reading it : "x"
  writing it : refused

.flush exists and its body is EMPTY. It does not clear, drain or
commit anything — the name implies otherwise:
  after .flush : "still here"

The one thing to know #

It is not an IO::Handle and inherits nothing, yet it works as a drop-in $*OUT — so it is a two-line output capture.

File
use String::Stream;

my $cap = String::Stream.new('');
{
    my $*OUT = $cap;
    say 'captured line';
    print 'captured print';
}
say 'nothing in the class advertises this.';
say '';
say 'what the capture holds:';
say '  ', $cap.get.raku;
say '';
say 'core say/print only need .print and .say on the handle, and this';
say 'class happens to have both. No role, no inheritance, no IO::Handle.';
Output
nothing in the class advertises this.

what the capture holds:
  "captured line\ncaptured print"

core say/print only need .print and .say on the handle, and this
class happens to have both. No role, no inheritance, no IO::Handle.

That is also the whole risk: anything in the captured block that reaches for a real handle method — .flush with meaning, .encoding, .t, .close — finds either nothing or the no-op above.

Where the two engines differ #

Constructor arguments the class does not declare. Raku++ silently builds an object with an empty buffer; Rakudo raises X::Constructor::Positional.

File
use String::Stream;

say 'the two documented forms:';
say '  .new()      -> buffer ', String::Stream.new.get.raku;
say '  .new("seed") -> buffer ', String::Stream.new('seed').get.raku;
say '';
say 'anything else is where the engines part:';
say '  .new(42)        builds with an EMPTY buffer on Raku++,';
say '                  X::Constructor::Positional on Rakudo';
say '  .new("a", "b")  the same';
say '';
say 'so a typo`d constructor argument is lost without a sound on one';
say 'engine and caught on the other. Pass one Str, or none.';
say '';
say 'the safe idiom:';
sub sink(Str $seed = '') { String::Stream.new($seed) }
my $s = sink();
$s.say('line one');
$s.say('line two');
say '  ', $s.get.raku;
Output
the two documented forms:
  .new()      -> buffer Any
  .new("seed") -> buffer "seed"

anything else is where the engines part:
  .new(42)        builds with an EMPTY buffer on Raku++,
                  X::Constructor::Positional on Rakudo
  .new("a", "b")  the same

so a typo`d constructor argument is lost without a sound on one
engine and caught on the other. Pass one Str, or none.

the safe idiom:
  "line one\nline two\n"

Reduced with no module involved: a class that declares its own multi method new accepts stray positionals under Raku++ and refuses them under Rakudo — plain multi dispatch is fine on both, it is new specifically.

The other thing to plan around, identical on both engines: there is no reset. .get does not consume and .flush does nothing, so the only way to start over is a new object.