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/tap blocks, and the _ placeholder).

  • A real global infix operator |> usable anywhere in the lexical scope, on perl 5.38 and later (which provides PL_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 / grep block 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 a pipe { } block) requires perl 5.38 or later for PL_infix_plugin. The block form works on 5.14+.

  • In the infix form, perl built-ins (ref, length, ...) don't parse as OP_ENTERSUB and fall into the coderef path, failing at runtime. Wrap them in sub { 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:

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)