NAME
Pipe::Declare - compile time, lexically scoped |> pipelines
VERSION
Version 0.01
SYNOPSIS
use Pipe::Declare;
# block form: assemble a value via left-to-right steps
my $total = pipe {
[1, 2, 3, 4, 5]
|> grep { $_ % 2 } # keep odd -> [1,3,5]
|> map { $_ * $_ } # square them -> [1,9,25]
|> sum_aref # bareword sub -> 35
};
# infix form: use |> as a real binary operator anywhere in the
# lexical scope (perl 5.38+)
my $title = $user |> fetch_profile |> profile_title;
my $tot = sum_of(values %h) |> normalise |> round_2dp;
# statement form: drive side effects
pipe { $request
|> validate
|> tap { warn "ok: $_->{id}" }
|> dispatch };
# all the step shapes (block form)
pipe { $h
|> ->{users} # hash deref
|> ->[0] # array deref
|> ->name # method call (nullary)
|> uc # bareword nullary
|> wrap(_, '[', ']') # bareword + _ placeholder
|> \&trim # \&name
|> sub { "$_[0]!" } # inline anonymous sub
|> $formatter # any expr yielding a coderef
};
DESCRIPTION
Pipe::Declare installs an F#/Elixir style |> pipeline operator in two complementary spellings:
A block keyword
pipe { HEAD |step |> step ... }> that works on perl 5.14 and later and supports every step shape (bareword,->method,->{key},->[idx],\&name,sub {...},$cv,map/grep/tapblocks, and the_placeholder).A real global infix operator
|>usable anywhere in the lexical scope, on perl 5.38 and later (which providesPL_infix_plugin). The infix form supports bareword calls, method calls, coderefs and chained|>; the block-style steps (map/grep/tap,_placeholder) are block-form only.
Both forms are parsed at compile time and lowered to an ordinary Perl optree. The common case (every step takes the topic exactly once) lowers to a fully nested call expression with zero per-step overhead - benchmarked at parity with hand-written nested calls (see "PERFORMANCE").
It is a real lexical pragma: both spellings are recognised only inside the lexical scope of use Pipe::Declare, and are disabled again by no Pipe::Declare. Outside such a scope pipe resolves to the core CORE::pipe built-in unchanged, and |> is a parse error (perl sees | as bitwise-or and > as the start of something else).
SEMANTICS
A pipe { HEAD | step |> step ... }> block is one expression made of two parts: a head term that supplies the initial value, and a sequence of steps each separated by |>.
my $topic = HEAD; # evaluated exactly once
$topic = step_1($topic);
$topic = step_2($topic);
...
step_N($topic) # final step: no assignment, so its
# calling context flows through
The hidden $topic is a fresh pad lexical, invisible to user code.
The head expression is evaluated exactly once, no matter how many steps follow.
Intermediate steps run in scalar context (the topic is one scalar slot). The final step inherits the calling context of the whole
pipe { ... }expression - scalar, list, or void.Steps run strictly left to right; there is no reordering.
Exceptions propagate out of the pipe; there is no implicit
eval.
HEAD FORMS
$var, $Pkg::var scalar variable
NUMBER, "STR", 'STR' literals
[ LIST ] anonymous arrayref literal
name, name(args),
Foo::Bar::name(args) bareword call (qualified or not)
( ANY EXPRESSION ) parens delimit a full expression
For anything else, wrap in parens: pipe { ( $obj->factory->build ) |> ... }.
STEP FORMS
|> name bareword nullary -> name($topic)
|> name(a, b) bareword with args -> name($topic, a, b)
|> name(a, _, b) _ placeholder -> name(a, $topic, b)
|> name(_, _, _) multiple _ -> name($topic, $topic, $topic)
|> Foo::Bar::name qualified -> Foo::Bar::name($topic)
|> \&name ref form -> name($topic)
|> sub { BLOCK } inline coderef -> (sub { BLOCK })->($topic)
|> $cv scalar coderef -> $cv->($topic)
|> ->method method nullary -> $topic->method
|> ->method(args) method with args -> $topic->method(args)
|> ->{KEY} hash deref -> $topic->{KEY}
|> ->[IDX] array deref -> $topic->[IDX]
|> map { BLOCK } block map -> [ map { BLOCK } @{$topic} ]
|> grep { BLOCK } block grep -> [ grep { BLOCK } @{$topic} ]
|> tap { BLOCK } side-effect tap -> do { my $t = $topic; for ($t) { BLOCK } $t }
The _ placeholder in a bareword-call arglist is replaced by the topic at every occurrence, suppressing the default first-argument threading. Inside map / grep / tap blocks, $_ is the iteration element (for map/grep) or the topic (for tap), exactly as the underlying Perl operators behave. The three block steps compile to perl's own map, grep and foreach, not to a sub call, so return inside one of them returns from the enclosing sub, as it would in a plain map block. A tap block sees a private copy of the topic in $_: assigning to $_ changes what flows to the next step, nothing else.
INFIX FORM (perl 5.38+)
On perl 5.38 and later, |> is also registered as a real left-associative infix operator usable anywhere in the lexical scope of use Pipe::Declare, with no surrounding pipe { } required:
use Pipe::Declare;
my $tidy = $name |> trim |> lc;
my $sz = $bytes + 1024 |> humanize; # ($bytes + 1024) |> humanize
my $name = $user |> fetch_profile |> profile_name(_, 'short');
Precedence
|> binds at INFIX_PREC_LOGICAL_OR - the same precedence as ||: tighter than =, ,, or, and, but looser than +, - and everything arithmetic. The common expectations all hold:
my $r = $x |> f; # my $r = ($x |> f)
$x + 1 |> f; # ($x + 1) |> f
$x |> f or warn 'nope'; # ($x |> f) or warn 'nope'
$x |> f |> g |> h; # ((($x |> f) |> g) |> h)
Splice rules
The infix form mirrors the block form's step-shape recognition:
Bareword / sub call: LHS becomes the first positional arg.
$x |> f # f($x) $x |> f(a, b) # f($x, a, b)Method call: LHS becomes the second positional - inserted after the invocant, before any explicit args.
$x |> $obj->m # $obj->m($x) $x |> $obj->m(a, b) # $obj->m($x, a, b)Coderef expression (
\&name,sub { ... }, scalar holding a coderef, anything else): RHS is called with LHS as the sole arg.$x |> \&f # f->($x) $x |> sub { ... } # (sub { ... })->($x) $x |> $cv # $cv->($x)
The block-only step forms (map/grep/tap blocks, _ placeholder, ->method as a standalone step, ->{key} / ->[idx] deref steps) are not available in the infix form: write them in a pipe { ... } block instead. Perl built-ins (ref, length, defined, ...) don't parse as OP_ENTERSUB and so fall into the coderef path, which fails at runtime - wrap them in an anon sub if you need them in the infix form.
SUMMARY TABLE
scrutinee / head evaluated exactly once
intermediate-step context scalar
final-step context inherits the pipe expression's context
match order strict left-to-right
exceptions propagate; no implicit eval
value of the whole pipe value of the final step
COMPARISON WITH Func::Util::pipeline
Func::Util already ships a runtime, value-level pipeline():
my $result = pipeline($data, \&fetch, \&transform, \&process);
Pipe::Declare is the compile-time, syntactic form of the same idea. They coexist; pick whichever reads more naturally for the call site.
Func::Util::pipeline Pipe::Declare
when threading happens runtime compile time
step shape coderef only any of the step forms above
extra args per step via wrapper closures inline: |> f(a, b, _)
optree per pipeline one entersub + array walk one call per step
reads as data-style code-style (left-to-right)
method / deref step via wrapper closures natively (->method, ->{k}, ->[i])
LIMITATIONS
The topic is always a single scalar. Pipelines that need list-context threading must encode it as an arrayref (which is the natural input/output shape for the
map/grepblock steps).The block-step set is closed:
map,grep,tap. Adding more (e.g.reduce,sort) is a v1.1 option.The
_placeholder is recognised only when it stands alone in an arglist position (followed by,or)). Complex expressions involving the topic should be written using a coderef step.The infix form (
|>outside apipe { }block) requires perl 5.38 or later forPL_infix_plugin. The block form works on 5.14+.In the infix form, perl built-ins (
ref,length, ...) don't parse asOP_ENTERSUBand fall into the coderef path, failing at runtime. Wrap them insub { ref $_[0] }or use the block form.
PERFORMANCE
The whole pipe lowers to one nested expression:
step_N( ... step_2( step_1( HEAD ) ) ... )
No hidden temp, no statement sequence: the optree is what you would write by hand. A step that uses _ more than once gets a hidden scalar assigned in its first _ and read in the others, which is how you would write name($t = step_1(HEAD), $t) yourself, and keeps the documented "intermediate steps are scalar context" semantics.
Measured against hand-written nested calls (xt/bench.pl; perl 5.42 on Apple Silicon):
3-step bareword chain within 1% of hand-nested
5-step bareword chain within 1% of hand-nested
call-with-args + _ within 2% of hand-nested
name(_, _) step within 5% of a hand-written temp
->method chain (4 steps) within 2% of hand-nested
map | grep (100 elems) ~15% slower (the head copies the array)
tap between two calls ~11% slower (a do-block around a foreach)
A run of consecutive map / grep steps is fused: only the last step of the run builds an arrayref, the earlier ones feed their list straight into the next, exactly as [ grep { B2 } map { B1 } @$topic ] would. The remaining map/grep cost against the hand-written chain is the head: [@big] copies the array into a fresh arrayref where the hand-written form reads @big in place.
tap lowers to a foreach over a private copy of the topic, so a tap step costs one loop iteration, with no closure built and no sub called.
SEE ALSO
Switch::Declare - compile-time switch/case by the same author, sharing the keyword-plugin and $^H lexical pragma scaffolding.
Func::Util - the runtime, value-level cousin of this module (pipeline, compose, tap, etc.).
AUTHOR
LNATION <email@lnation.org>
BUGS
Please report any bugs or feature requests to bug-pipe-declare at rt.cpan.org, or through the web interface at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Pipe-Declare. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
SUPPORT
You can find documentation for this module with the perldoc command.
perldoc Pipe::Declare
You can also look for information at:
RT: CPAN's request tracker (report bugs here)
Search CPAN
ACKNOWLEDGEMENTS
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)