NAME
Acme::Parataxis::Generator - Stackful, Lazily-Pulled Coroutine Iterators
SYNOPSIS
use v5.40;
use Acme::Parataxis::Generator;
my $fib = Acme::Parataxis::Generator->new( sub ($y) {
my ( $a, $b ) = ( 0, 1 );
while ( $a < 100 ) {
$y->($a);
( $a, $b ) = ( $b, $a + $b );
}
} );
my $total = 0;
while ( defined( my $n = $fib->next ) ) {
$total += $n;
}
say "Sum of Fibonacci numbers < 100: $total"; # 232
DESCRIPTION
A generator executes its producer body on an isolated, private coroutine fiber and produces values on demand. The body is plain synchronous Perl: it can loop, return, call functions, and recurse deeply. Calling $y->($value) from any depth suspends the producer immediately, preserving lexical variables, loop states, and call frames intact. The next invocation of ->next resumes execution right where it paused.
Key properties:
Lazy evaluation: Nothing inside the body executes until the first call to
next(). An infinite sequence costs nothing until pulled, and consumes only as much memory and CPU as needed for the values requested.Full coroutine ergonomics: Unlike shallow iterators, yielding is not restricted to the top-level block; helper subs can accept
$yand yield on behalf of the generator.Safe lifecycle teardown: Abandoning an unexhausted generator cleanly drains the fiber: the yield site raises an internal drain sentinel, unwinding lexical scopes and executing object destructors without leaking fibers.
Context independence: Generators do not enter the scheduler run queue. They can be instantiated and pulled from top-level mainline scripts, within
asyncblocks, inside spawned fibers, or inside nurseries.
CONSTRUCTOR
new( sub ($y) { ... } )
my $gen = Acme::Parataxis::Generator->new( sub ($y) {
$y->($_) for 1 .. 3;
} );
Constructs and returns a new generator.
The constructor takes exactly one argument: a code reference representing the producer body. That code reference receives a single parameter: the yield closure (conventionally named $y).
Calling $y->($value) suspends the body and delivers $value to the caller's next(). $y requires exactly one argument; calling $y->() or passing multiple arguments croaks.
METHODS
next( )
my $val = $gen->next;
Resumes the generator body and returns the next yielded value.
Once the body finishes (either by falling off the end or returning),
next()returnsundefand continues returningundefon subsequent calls.If the body explicitly yields
undef(e.g.$y->(undef)),next()returnsundef. Use "is_done( )" to differentiate a yieldedundeffrom sequence exhaustion.If the body throws an exception, that exception is rethrown directly on the caller's stack at the
next()call site, and the generator is marked permanently finished. Exception objects cross the resumption boundary intact.
is_done( )
while ( !$gen->is_done ) {
my $v = $gen->next;
process($v) if defined $v;
}
Returns true if the generator has finished execution (whether it returned normally or threw an exception), and false if it is still suspended awaiting the next pull.
fiber( )
Returns the underlying private Acme::Parataxis fiber object. Intended for debugging and ID inspection.
Recipes
Safely consuming sequences that contain undef
The idiom while ( defined(my $v = $gen->next) ) halts prematurely if the sequence legitimately yields undef. Test is_done() instead:
sub drain ($gen) {
my @items;
while (1) {
my $val = $gen->next;
last if $gen->is_done;
push @items, $val;
}
return @items;
}
my $gen = Acme::Parataxis::Generator->new( sub ($y) {
$y->('alpha');
$y->(undef);
$y->('omega');
} );
my @values = drain($gen); # [ 'alpha', undef, 'omega' ]
Composing lazy generator pipelines
my $cubes = Acme::Parataxis::Generator->new( sub ($y) {
my $ints = Acme::Parataxis::Generator->new( sub ($z) {
my $i = 1;
while (1) { $z->($i++) }
} );
while ( defined( my $n = $ints->next ) ) {
$y->( $n ** 3 );
}
} );
say $cubes->next for 1 .. 4; # 1, 8, 27, 64
Exception propagation preserving error objects
use Acme::Parataxis::Error;
my $gen = Acme::Parataxis::Generator->new( sub ($y) {
$y->(1);
Acme::Parataxis::Error::Timeout->throw( seconds => 5 );
} );
say $gen->next; # 1
eval { $gen->next };
if ( my $err = $@ ) {
say "Caught typed error: " . $err->kind; # 'timeout'
}
Behavior Notes
Body return values are discarded
A generator's output must be produced via
$y->(...). If a body terminates viareturn $val, that final return value is ignored, and the nextnext()returnsundef. Always yield the final value before returning.Do not park inside a generator body
Generators run outside the cooperative scheduler queue. Calling cooperative parking primitives (
await_sleep,await_read, or channelget) inside a generator injects scheduler control tokens (such as'WAITING') into the sequence as yielded values. Keep the generator body synchronous; perform async I/O on the consumer side.Abandoned generators drain on destruction
If a generator goes out of scope while suspended at a yield point, its
DESTROYhandler resumes the private fiber with an internal drain sentinel. This causes the yield call to throw internally, allowing enclosing scopes, destructors, andfinallyblocks in the generator to execute cleanups.Reserved fiber 0 on Windows
On Windows, longjmp-based unwinding across the very first fiber in a process (ID 0) can bypass anchored exception traps. To guarantee stability, the first generator allocated at the mainline permanently reserves fiber ID 0, ensuring generators always execute on fiber ID 1 or higher. Inside a scheduled
run()block, the root fiber already holds ID 0, so no extra reservation is created.
SEE ALSO
Acme::Parataxis, Acme::Parataxis::Channel, Acme::Parataxis::Stream
AUTHOR
Sanko Robinson https://github.com/sanko
LICENSE
Copyright (C) Sanko Robinson.
This library is free software; you can redistribute it and/or modify it under the terms found in the Artistic License 2.