NAME
Ekorn::Pipeline::Manual::HowTo - Ekorn::Pipeline how-to
DESCRIPTION
Recipes for everyday work with Ekorn::Pipeline, one task at a time. Each shows the shortest way to do the job; the reference says what every function and method does, in full.
Ekorn::Pipeline has three kinds of user, and the recipes are for all three:
Callers of a function that takes stages, who say what they want done with its values, beside the call.
Function authors, who give a function stages to take, in place of options.
Stage authors, who write stages for others to use. Notes marked For stage authors say what a recipe means for them, and Writing stages for others is theirs.
It needs Perl 5.36 or later.
Contents
The examples' data
Every example below starts from these tracks, and from tracks, a function that hands them out, taking stages from its caller. The examples follow on from each other, so @tracks and tracks are these wherever they appear.
use v5.36;
use Ekorn::Pipeline;
my @tracks = (
{artist => 'Abba', title => 'Dancing Queen', plays => 9100},
{artist => 'Abba', title => 'Knowing Me, Knowing You', plays => 4200},
{artist => 'Blur', title => 'Girls & Boys', plays => 6100},
{artist => 'Blur', title => 'Parklife', plays => 5800},
{artist => 'Blur', title => 'Country House', plays => 3900},
{artist => 'Cream', title => 'Sunshine of Your Love', plays => 7700},
);
# The tracks, through whatever stages the caller gives.
sub tracks (@stages) {
return Ekorn::Pipeline->new(@stages)->(@tracks);
}
Running values through stages
Run values through stages
new makes a pipeline from a list of stages, each a code reference. Call the pipeline with values, and it returns what comes out of the last stage:
my $titles = Ekorn::Pipeline->new(sub { $_->{title} });
my @titles = $titles->(@tracks);
say $titles[0]; # Dancing Queen
A pipeline can be called as often as you like, with different values each time. Each call is a run.
Change each value (map)
A stage that returns one value passes it on, in place of the value it was given:
my @shouted = tracks(sub { uc $_->{title} });
say "@shouted[0, 1]"; # DANCING QUEEN KNOWING ME, KNOWING YOU
Leave values out (filter)
A stage that returns an empty list drops the value, and the stages after it never see it:
my @hits = tracks(
sub { $_->{plays} >= 6000 ? $_ : () },
sub { $_->{title} },
);
say join ', ', @hits; # Dancing Queen, Girls & Boys, Sunshine of Your Love
A test on its own isn't a filter: sub { $_->{plays} >= 6000 } passes on what the test returns, true or false, in place of the track. Return the value, or nothing.
Turn one value into several (flatMap)
A stage that returns several values passes each of them on, in order, through the rest of the stages, before the next value starts:
my @words = tracks(
sub { $_->{artist} eq 'Abba' ? $_ : () },
sub { split ' ', $_->{title} },
sub { lc },
);
say "@words"; # dancing queen knowing me, knowing you
Name the value, or use $_
A stage is called with one value, both as its only argument and in $_. Use whichever reads better: $_ for a short stage, a signature when the name helps.
my @with_it = tracks(sub { $_->{title} });
my @with_name = tracks(sub ($track) { $track->{title} });
say "@with_it" eq "@with_name" ? 'the same' : 'different'; # the same
$_ is local to each call of a stage, so the caller's $_ is left as it was, and a stage can call code that uses $_ too.
For stage authors: a stage is always called with exactly one argument, so a signature of one, sub ($value), is safe. An end function, which Gather gives, is called with none, so its signature is sub ().
Pass on undef, 0 or an empty string
Only an empty list drops a value. undef, 0 and the empty string are values like any other, and go on:
my @plays = Ekorn::Pipeline->new(sub { $_ || undef })->(3, 0, '', 5);
say scalar @plays; # 4
Change values without changing the caller's
The values a pipeline is called with are copied, so assigning to $_ changes the copy, not the caller's array. But a reference is copied as a reference: a stage that changes a hash it's given changes the caller's hash. To leave the caller's data alone, return a new one:
my @doubled = tracks(sub { +{ %$_, plays => $_->{plays} * 2 } });
say "$doubled[0]{plays} $tracks[0]{plays}"; # 18200 9100
For stage authors: a stage others use should never change the value it's given, unless changing it is what the stage is for, and its documentation says so.
Count the results
Called in scalar context, a pipeline returns how many values came out:
my $hits = tracks(sub { $_->{plays} >= 6000 ? $_ : () });
say $hits; # 3
count_into counts values at any point in a pipeline, not only at its end.
Pulling values one at a time
Pull values from a source
iterator takes a source, a function returning the next value, or an empty list when there are no more. It returns a function handing out the results, one per call, and an empty list once they're done. The source is called only when a result is wanted:
my @queue = (3, 1, 2);
my $next = Ekorn::Pipeline->new(sub { $_ * 10 })
->iterator(sub { @queue ? shift @queue : () });
while (my($n) = $next->()) {
say $n; # 30, then 10, then 20
}
Read the results in list context
Read an iterator in list context, as my($n) = $next->(), so that a result of 0, '' or undef isn't taken for the end. In scalar context, while (my $n = $next->()) would stop at the first 0:
my @counts = (2, 0, 1);
my $next = Ekorn::Pipeline->new->iterator(sub { @counts ? shift @counts : () });
my @seen;
while (my($n) = $next->()) { push @seen, $n }
say "@seen"; # 2 0 1
Keep undef from ending a source
A source ends by returning an empty list, so a source that returns shift @queue // () ends at the first undef in the queue. Test whether there's anything left instead:
my @queue = ('a', undef, 'b');
my $next = Ekorn::Pipeline->new(sub { $_ // 'none' })
->iterator(sub { @queue ? shift @queue : () });
my @seen;
while (my($x) = $next->()) { push @seen, $x }
say "@seen"; # a none b
Read a file a line at a time
A filehandle is a source, read in scalar context, so that each call reads one line:
my $log = <<~'END';
INFO started
ERROR disk full
INFO retrying
ERROR disk still full
END
open my $fh, '<', \$log or die "Can't read the log: $!\n";
my $errors = Ekorn::Pipeline->new(sub { m( \A ERROR \s+ (.*) )x ? $1 : () })
->iterator(sub { scalar(<$fh>) // () });
while (my($message) = $errors->()) {
say $message; # disk full, then disk still full
}
Without scalar, <$fh> would read every line on its first call.
Fetch values a page at a time
A source can return several values from one call, such as a page of results from a service. Each is handed out before the source is called again:
my @pages = ([qw(a b c)], [qw(d e)]);
my $calls = 0;
my $next = Ekorn::Pipeline->new(sub { uc })
->iterator(sub { $calls++; @pages ? @{ shift @pages } : () });
my($first) = $next->();
say "$first after $calls call"; # A after 1 call
Know when an iterator is done
Once an iterator has handed out its last result, it returns an empty list on every call after that, and doesn't call its source again. Make a new iterator to start again; each is a run of its own.
The same goes once a stage has died: the call that asked for the result dies, and the iterator has ended, so catching the error and reading on gets nothing more. To skip a bad value, catch the error inside the stage, as Catch errors in a stage, but not a stop does.
Stopping early
Take the first few
take passes on the first few values, then stops the pipeline. With an iterator, the source isn't called again, so no more is read than is wanted:
use Ekorn::Pipeline::Stages::Flow; # take and drop
my $read = 0;
my $next = Ekorn::Pipeline->new(take(2))
->iterator(sub { $read < @tracks ? $tracks[$read++] : () });
my @two;
while (my($track) = $next->()) { push @two, $track->{title} }
say join ', ', @two; # Dancing Queen, Knowing Me, Knowing You
say "read $read of 6"; # read 2 of 6
Page through values
drop leaves out the first few values, and passes on the rest. With take, it gives a page:
my @page2 = tracks(drop(2), take(2), sub { $_->{title} });
say join ', ', @page2; # Girls & Boys, Parklife
Stages run in the order they're given, so a filter before drop and take pages through what the filter passes on.
Stop at the first match
Stop ends the pipeline from a stage. The values already out are kept, and any values given to Stop go on through the rest of the stages, as the stage's last:
use Ekorn::Pipeline::Stop;
my($blur) = tracks(sub { $_->{artist} eq 'Blur' ? Stop($_) : () });
say $blur->{title}; # Girls & Boys
Stop(), with nothing, ends the pipeline without passing anything on. To stop without importing Stop, call Ekorn::Pipeline::Stop->throw, which takes values in the same way.
Take values while a test holds
take_while passes on values while its block is true of them, and stops the pipeline at the first it's false of, which isn't passed on:
use Ekorn::Pipeline::Stages::Flow qw(:DEFAULT take_while drop_while);
my $mail = <<~'END';
From: abba@example.com
Subject: Waterloo
My, my, at Waterloo Napoleon did surrender
END
open my $fh, '<', \$mail or die "Can't read the mail: $!\n";
my $headers = Ekorn::Pipeline->new(take_while { m( \S )x }, sub { s( \n \z )()xr })
->iterator(sub { scalar(<$fh>) // () });
my @headers;
while (my($line) = $headers->()) { push @headers, $line }
say join ' | ', @headers; # From: abba@example.com | Subject: Waterloo
Skip values while a test holds
drop_while leaves out values while its block is true of them, and passes on the rest, from the first it's false of:
my @lines = ('# tracks', '# by plays', 'Dancing Queen', '# not a comment now');
my @body = Ekorn::Pipeline->new(drop_while { m( \A \# )x })->(@lines);
say join ' | ', @body; # Dancing Queen | # not a comment now
Catch errors in a stage, but not a stop
A stop is an exception, so a stage that catches errors itself, with eval or try, would catch a stop too. Throw it on again:
use experimental qw(try);
my @titles = tracks(sub ($track) {
try {
Stop() if $track->{artist} eq 'Blur';
return uc $track->{title};
}
catch ($e) {
die $e if $e isa 'Ekorn::Pipeline::Stop';
return 'unknown';
}
});
say join ', ', @titles; # DANCING QUEEN, KNOWING ME, KNOWING YOU
Test it with isa, so that a stop of a subclass is thrown on too. The same goes for the stops that canned stages throw, such as take's, when a stage that catches errors runs a pipeline of its own.
For stage authors: a stage that catches errors must always throw a stop on, or it swallows the stops of the stages it runs, and the pipeline carries on when it should have ended.
Gathering values
Hand out values in batches
batch passes values on in batches, each an array reference. The last, which may be shorter, goes on once the values run out:
use Ekorn::Pipeline::Stages::Batch; # batch and collect
my @batches = tracks(sub { $_->{title} }, batch(4));
say scalar @{ $batches[0] }; # 4
say "@{ $batches[1] }"; # Country House Sunshine of Your Love
With an iterator, batch holds no more than one batch, so it can write a large file to a database in batches of 500 without reading the whole file first.
Make one value from every value
collect holds every value, and once they run out, passes on an array reference of them, or what its block makes of them:
use List::Util qw(sum0);
my($total) = tracks(sub { $_->{plays} }, collect { sum0 @_ });
say $total; # 36800
my($all) = tracks(sub { $_->{artist} eq 'Cream' ? $_->{title} : () }, collect);
say "@$all"; # Sunshine of Your Love
collect gives a result even when there are no values: an empty array, or what its block makes of none. Unlike batch, it holds every value until the end, so the whole result is in memory at once.
Keep count, starting afresh on each run
A stage is a closure, so it can keep state, but a plain closure keeps it from one run to the next:
my $count = 0;
my $plain = Ekorn::Pipeline->new(sub { ++$count . ". $_" });
$plain->('a');
my($second_run) = $plain->('a');
say $second_run; # 2. a
Make the stage with gather. Its block is called at the start of each run, and returns the stage, so whatever the block declares is fresh on each:
use Ekorn::Pipeline::Gather;
sub numbered () {
return gather {
my $n = 0; # this run's
return sub ($value) { ++$n . ". $value" };
};
}
my $p = Ekorn::Pipeline->new(numbered());
$p->('a');
my($again) = $p->('a');
say $again; # 1. a
For stage authors: anything a stage keeps, even a count, goes in a gather block. A stage made once and given to several pipelines, or run by an iterator made twice, otherwise carries what it kept from one into the next.
Do something once the values run out
A gather block can return a second function, an end function, called once the values run out. What it returns goes on through the rest of the stages, as what a stage returns does:
sub titles_by_artist () {
return gather {
my %titles;
my $stage = sub ($track) {
push @{ $titles{ $track->{artist} } }, $track->{title};
return; # hold every track; pass nothing on yet
};
my $end = sub () {
return map { "$_: " . join(', ', @{ $titles{$_} }) } sort keys %titles;
};
return ($stage, $end);
};
}
say for tracks(titles_by_artist());
# Abba: Dancing Queen, Knowing Me, Knowing You
# Blur: Girls & Boys, Parklife, Country House
# Cream: Sunshine of Your Love
That's how batch and collect are made.
For stage authors: an end function is called even when no values went by, so it can give an empty result, such as [], or nothing at all. It's called with no arguments, and with $_ undefined. The ends are called in order, first stage first, and what one returns goes through the stages after it before the next end is called, so a collect after a batch gets the last batch too.
Tell values from an end apart
While the values going through the stages come from an end function, rather than from the source, Ekorn::Pipeline::ending() is true:
my @lines = tracks(
sub { $_->{plays} },
collect { sum0 @_ },
sub { Ekorn::Pipeline::ending() ? "total $_" : $_ },
);
say "@lines"; # total 36800
It's for code that tells its stages about the value they're given, such as where it came from, which a value an end makes doesn't have.
Watching values go by
Look at each value as it goes by
peek calls its block with each value, for its side effects, and passes the value on, whatever the block returns:
use Ekorn::Pipeline::Stages::Taps; # peek and count_into
my @log;
my @titles = tracks(peek { push @log, $_->{artist} }, sub { $_->{title} }, take(2));
say "@log"; # Abba Abba
A stage of your own, run for its side effects, has to return the value it was given, as peek does, or it drops it.
Count the values that went by
count_into counts values into a variable, and passes them on. It sets it to 0 at the start of each run, so after a run it holds how many values went by, even when that's none:
my($read, $kept);
my @hits = tracks(
count_into(\$read),
sub { $_->{plays} >= 6000 ? $_ : () },
count_into(\$kept),
);
say "$kept of $read"; # 3 of 6
Dump values while debugging
dump_to writes each value with Data::Dumper, its keys in order, to a filehandle, or to STDERR, and passes it on. It's for debugging, and writes values in full, so take it out before the code goes live:
use Ekorn::Pipeline::Stages::Taps qw(:DEFAULT dump_to);
open my $out, '>', \my $dumped or die "Can't write the dump: $!\n";
tracks(take(1), dump_to($out));
print $dumped;
# {
# 'artist' => 'Abba',
# 'plays' => 9100,
# 'title' => 'Dancing Queen'
# }
Writing a function that takes stages
Take stages from your caller
A function that hands out values takes stages as its last arguments, and runs what it would return through them. Its caller says what they want done with each value, beside the call:
sub tracks_by ($artist, @stages) {
my @by = grep { $_->{artist} eq $artist } @tracks;
return Ekorn::Pipeline->new(@stages)->(@by);
}
say join ', ', tracks_by('Blur', sub { $_->{title} }, take(2));
# Girls & Boys, Parklife
A caller who wants nothing done passes no stages, and a pipeline with no stages returns its values as they are, so there's nothing to special-case.
Make values only as they're wanted
If the function makes its values one at a time, from a file, a socket, a database or a computation, give iterator a source, and return the iterator. A caller's stage that stops then saves the work of making the rest:
# Square numbers, for as long as the caller wants them.
sub squares (@stages) {
my $n = 0;
return Ekorn::Pipeline->new(@stages)->iterator(sub { ++$n * $n });
}
my $next = squares(sub { $_ % 2 ? $_ : () }, take(3));
my @odd;
while (my($square) = $next->()) { push @odd, $square }
say "@odd"; # 1 9 25
The source never runs out, so without a stage that stops, the caller would loop for ever. That's fine for an iterator, as the caller reads it one value at a time; a function that returns a list should only be given a source that ends.
Replace an option with a stage
Every option that filters, reshapes or limits what a function returns is code its author writes, tests, documents and keeps. A stage does it already, in the caller's code:
# In place of tracks(min_plays => 6000, fields => ['title'], limit => 2):
my @top = tracks(sub { $_->{plays} >= 6000 ? $_->{title} : () }, take(2));
say join ', ', @top; # Dancing Queen, Girls & Boys
Keep the arguments that only the function can act on, such as which file to read, or a filter a database can apply with an index, and let stages do the rest.
Do some of the work yourself, then the caller's
A function can put stages of its own before its caller's, so that what reaches the caller is already in the shape it promises:
# Titles, through whatever stages the caller gives.
sub titles (@stages) {
return Ekorn::Pipeline->new(sub { $_->{title} }, @stages)->(@tracks);
}
say join ', ', titles(drop(4)); # Country House, Sunshine of Your Love
The function's own stages count in the numbering of a stage that fails, so its documentation should say what its caller's first stage is given.
Pass your caller's stages on
A function built on another that takes stages can pass its caller's stages on, after its own:
sub hits (@stages) {
return tracks(sub { $_->{plays} >= 6000 ? $_ : () }, @stages);
}
say join ', ', hits(sub { $_->{artist} }); # Abba, Blur, Cream
Report your callers' mistakes at their own lines
A stage that dies is reported at the line that ran its pipeline, and, for a function taking stages, that's a line inside the function. A module of such functions that puts Ekorn::Pipeline's packages in its @CARP_NOT has them reported at its caller's line instead, where the stage was written:
package My::Music {
use Ekorn::Pipeline;
# Report the errors of Ekorn::Pipeline at my callers' lines.
our @CARP_NOT = @Ekorn::Pipeline::Errors::INTERNAL;
sub tracks (@stages) {
return Ekorn::Pipeline->new(@stages)->(@tracks);
}
}
eval { My::Music::tracks(sub { die "Not today\n" }) };
print $@;
# Pipeline stage #1 failed at ... line ...: (this line, not one in My::Music)
# Not today
The same holds for a stage that isn't a code reference, and for a stage that dies when an iterator the function returned is read: the line is the caller's.
Name the stages you use often
Stages are code references, so a function can make one, and a set of them becomes a vocabulary, which grows in the callers' code, not the author's:
sub by ($artist) { sub { $_->{artist} eq $artist ? $_ : () } }
sub value_of ($name) { sub { $_->{$name} } }
say join ', ', tracks(by('Abba'), value_of('title'));
# Dancing Queen, Knowing Me, Knowing You
The same stages work with every function that takes them, whoever wrote it.
Give a class pipe and tap
A class that inherits from Ekorn::Pipeline::Pipes has two methods, which run the object through stages. pipe returns what comes out, and tap returns the object, so a chain of method calls can carry on after it:
package Playlist {
use parent 'Ekorn::Pipeline::Pipes';
sub new ($class, @tracks) { bless {tracks => [@tracks]}, $class }
sub tracks ($self) { @{ $self->{tracks} } }
sub size ($self) { scalar $self->tracks }
}
my $list = Playlist->new(@tracks[0 .. 2]);
my @artists = $list->pipe(sub { $_->tracks }, sub { $_->{artist} });
say "@artists"; # Abba Abba Blur
say $list->tap(sub { warn "checking\n" if $_->size > 10; $_ })->size; # 3
In scalar context, pipe returns the first value that comes out.
Writing stages for others
A stage for others to use is written as the canned stages are, so it works alongside them. Writing stages in the reference has the whole list.
Write a function that makes a stage
For a stage others will use, write a function that returns one, as take(10) does. It can take options, and check them once, when it's called, rather than on each value:
package My::Stages::Words {
use Ekorn::Pipeline::Errors qw(croakf);
sub longer_than ($length) {
croakf 'Expected a length of 0 or more, not %s', $length // 'undef'
unless defined $length && $length =~ m( \A \d+ \z )xa;
return sub ($word) { length $word > $length ? $word : () };
}
}
say join ', ', tracks(sub { $_->{title} }, My::Stages::Words::longer_than(14));
# Knowing Me, Knowing You, Sunshine of Your Love
A module of stages is a module like any other, which would export longer_than for its users to import; it's written inline here only so that the example runs as it stands. A function that makes a stage without options is called with empty parentheses, as numbered(), or without any once it's declared or imported, as collect is.
Report a mistake at the caller's line
croakf, from Ekorn::Pipeline::Errors, croaks with a message made by sprintf, at the line that called the function making the stage, so the user sees their own line, not one inside your module:
eval { My::Stages::Words::longer_than(-1) };
print $@; # Expected a length of 0 or more, not -1 at ... line ...
It reports the caller's line only when the function that croaks is in a package of its own, as a module of stages is: from main, called by main, Carp gives a full backtrace instead. A helper in the same package can call croakf for the function's caller too. One in another package needs that package in @CARP_NOT, as Errors in the reference says.
Pass a last value on when stopping
A stage that knows the next value is the last one wanted passes it on with the stop, as take does, so the pipeline ends without asking its source for one more:
sub up_to ($wanted) {
return sub ($value) { $value eq $wanted ? Stop($value) : $value };
}
my $next = Ekorn::Pipeline->new(value_of('title'), up_to('Parklife'))
->iterator(do { my $i = 0; sub { $i < @tracks ? $tracks[$i++] : () } });
my @up_to;
while (my($title) = $next->()) { push @up_to, $title }
say scalar @up_to; # 4
up_to compares the whole value with eq, so it's given titles here, not tracks. Mind what a stage is given, and say so in its documentation.
Use a pipeline's stages in another
A pipeline isn't a stage itself, but its stages can go wherever stages go, with stages, so stages made once can be used again, in another pipeline or in a call of any function that takes them:
my $clean = Ekorn::Pipeline->new(sub { s( \A \s+ | \s+ \z )()xgr }, sub { length ? $_ : () });
my @titles = Ekorn::Pipeline->new($clean->stages, take(2))->(' Parklife ', ' ', 'Tender', 'Waterloo');
say join '|', @titles; # Parklife|Tender
say join '|', tracks(value_of('title'), $clean->stages, take(3)); # Dancing Queen|Knowing Me, Knowing You|Girls & Boys
The stages run as if they'd been listed there: a stop in one ends the whole run, and a gather stage starts afresh with each run, so what it keeps isn't shared by two pipelines, or by its uses twice in one. A plain closure is the same code wherever it goes, so whatever it keeps is shared: keep state in gather, as Keep count, starting afresh on each run says.
A stage can call a pipeline instead, as sub { $clean->($_) }. Then each call is a run of its own, so its gather stages start afresh on each value, and a stop in it ends only that call: a take(2) inside limits what each call gives, not the whole run.
Test a stage
A pipeline called with values tests a stage, with no source to set up. Run it twice, to show it starts afresh, and with no values, to show what its end gives:
use Test2::V0;
my $p = Ekorn::Pipeline->new(numbered());
is [$p->(qw(a b))], ['1. a', '2. b'], 'numbers each value';
is [$p->('c')], ['1. c'], 'from 1 on each run';
is [$p->()], [], 'nothing, from nothing';
done_testing;
Test a stage that stops through an iterator too, counting the calls of its source, to show it reads no more than it should.
Publish a module of stages
Where a module of stages goes depends on what its stages need to know:
Stages for any values, whatever they are and wherever they come from, go under
Ekorn::Pipeline::Stages, as the canned stages do.Stages for one kind of value, or one source, go with the module that knows about it, under its own namespace: DBIx::Squirrel's stages for rows are
DBIx::Squirrel::Stages::Rows, and stages for a log format of your own might beMy::Log::Stages.
Export the stages a module is usually used for by default, and the rest on request, with an :all tag, as the canned stages do. Choose names that don't clash with what common modules export: Flow has drop, not skip, which Test2::V0 and Test::More both export.
Have the distribution require the version of Ekorn::Pipeline it was tested with, up to the next change of its middle number, which is where the way stages behave can change. In a cpanfile:
requires 'Ekorn::Pipeline', '>= v0.1.1, < v0.2.0';
Handling errors
Find the stage that failed
A stage that dies is named by its place in the pipeline, at the line of the code that ran it, with its message below, indented:
eval { tracks(sub { $_->{title} }, sub { die "No more Blur\n" if m( Boys )x; $_ }) };
print $@;
# Pipeline stage #2 failed at ... line ...:
# No more Blur
The line is the one that ran the pipeline: here, the line in tracks that calls it, as tracks is in main. A module of functions taking stages can have it be its caller's line, as Report your callers' mistakes at their own lines shows. With an iterator, the line is the one that asked for the result. An end function that dies is named by its stage's place, as the stage is.
Throw exception objects through
An exception object, such as one from a class of your own, is thrown on as it is, so a caller can catch it by its class:
package Not::Found { sub new ($class, %args) { bless {%args}, $class } }
eval { tracks(sub { die Not::Found->new(title => $_->{title}) }) };
say ref $@, ': ', $@->{title}; # Not::Found: Dancing Queen
See mistakes in making a pipeline at your own line
A mistake in making a pipeline or a canned stage, such as a stage that isn't a code reference, or take(-1), is reported at the line that made it, before any value goes through:
eval { Ekorn::Pipeline->new(sub { $_ }, 'title') };
print $@; # Expected stage #2 to be a code reference at ... line ...
eval { take(-1) };
print $@; # Expected a count of 0 or more at ... line ...
Diagnostics in the reference lists every message, and what it means.
SEE ALSO
Ekorn::Pipeline, Ekorn::Pipeline::Gather, Ekorn::Pipeline::Stop, Ekorn::Pipeline::Pipes
AUTHOR
Iain Campbell <cpanic@cpan.org>
LICENSE
Copyright (c) 2024-2026 by Iain Campbell.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.