Hey all — I want to put a proposal in front of the community for a pipe operator, and get a read on interest and feedback before it goes further into spec.
The problem: nested calls read inside-out
We’ve all written code like this:
resultOfFinalizeCall = service.finalize(
service.doTheThing(
service.cleanIt(
getData()
),
true,
"brad"
)
);
To understand what actually happens first, you have to start reading from the innermost parentheses and work your way out. The order the code executes in is the reverse of the order it’s written in. As chains like this grow, they get genuinely hard to follow — you’re constantly jumping your eyes back and forth to match up parens and figure out what’s feeding into what.
A pipe operator lets you write the same logic so it reads top-to-bottom, in the order it actually runs:
resultOfFinalizeCall = getData()
|> service.cleanIt( $$ )
|> service.doTheThing( $$, true, "brad" )
|> service.finalize( $$ );
Each step takes the result of the expression on the left and makes it available to the expression on the right, via a reserved placeholder variable: $$.
If you’ve used CommandBox, this should feel familiar — it already supports something similar with its own | piping syntax, e.g. #now | #dateFormat "full" | #ucase. This proposal brings that same readability win into the language itself.
How it would work
The core idea is simple: x |> f($$) evaluates x, stores it in a reserved variable called $$, then evaluates f($$) with that value available.
A few design decisions we landed on:
- Explicit placeholder, always. Some languages (F#, Elixir) pipe a value in as the implicit first argument to the next call. We’re deliberately not doing that. BoxLang would require you to write
$$explicitly wherever the piped value should land. This keeps things unambiguous — you can always see exactly where the value goes, and it works whether the call takes positional or named args. - No parens-less calls. The pipe operator isn’t an excuse to start dropping parentheses on function invocations (looking at you, Ruby). Every call still looks like a call.
$$can go anywhere in the argument list — not just first position, and it can even appear more than once in the same call.
Basic example
Old way:
ucase( dateFormat( now(), "full" ) )
Pipe way:
now() |> dateFormat( $$, "full" ) |> ucase( $$ )
Avoiding duplicate expensive calls
One nice side effect of an explicit placeholder: it also solves a completely different annoyance — needing the same expensive value in multiple argument slots.
Old way:
var expensiveData = getExpensiveData();
result = service.doTheThing( expensiveData, true, "brad", expensiveData )
Pipe way:
result = ( getExpensiveData() |> service.doTheThing( $$, true, "brad", $$ ) )
No more breaking your expression into a separate temp-variable assignment just to avoid calling getExpensiveData() twice.
Here’s another variation I hit a lot using ternary where you want to use the result of a function or method call in more than one branch of the ternary.
Old way:
var tmp = getName();
var finalVar = tmp == "brad" ? "Mr. #tmp#" : tmp;
Pipe way:
var finalVar = ( getName() |> $$ == "brad" ? "Mr. #$$#" : $$ )
Piping into a function returned by another function
Because $$ is explicit, it also plays nicely with higher-order functions:
function returnFunc( boolean add = true ){
if( add ){
return ( x ) => x + 2;
}
return ( x ) => x - 2;
}
added = 2 |> returnFunc( true )( $$ ) // 4
subtracted = 2 |> returnFunc( false )( $$ ) // 0
The first () invokes returnFunc to get the closure back; the second () invokes that closure with the piped value explicitly placed via $$.
A stretch idea: lazy evaluation
Something we haven’t fully committed to, but is worth floating: could $$ in certain argument positions be lazily evaluated — only computed if that argument actually ends up being used?
// $$ only evaluated if the ternary condition is false
getExpensiveData() |> condition ? ifTrue : ifFalse( $$, "etc", $$ )
This is more speculative and would need real design work, but it’s an interesting direction once the core semantics are settled. The idea of lazy variables exists in languages like Scala and could
also be a first-class BoxLang feature outside of the pipe oprator along the lines of
lazy nameEventually = ()=>service.getName();
println( nameEventually ) // Closure isn't run until we actually use the value here
println( nameEventually ) // Cached, closure doesn't run again
Open questions we’d love feedback on
- Is
$$the right token? It’s what we’ve gravitated toward in discussion, but we want to make sure it doesn’t collide with anything else in the language. Another idea would be to let the dev opt in to the variable name they wanted with something like|myVar>. SoproperName = ("brad" |name> "Mr. #name#") - What’s the scope of
$$? Does it live in local/default-assignment scope, and if so, should the RHS of the pipe be its own encapsulated scope so$$doesn’t leak past the expression it belongs to — similar to howcfcatchvariables are scoped only to their catch block?
Prior art we looked at
- JS Hack-pipe proposal (TC39, stage 2) — explicit placeholder token, closest to what we’re proposing
- HHVM Hack pipe operator
- F#/OCaml
|>, Elixir|>— implicit first-arg sugar (we’re deliberately not going this route) - Clojure
->/->>threading macros - R native pipe / magrittr
%>% - Ramda.js — great prior art for pipe/curry/placeholder ergonomics in general
- CommandBox’s existing
|REPL piping behavior
Would love to hear what the community thinks — does this solve a real pain point for you, does the $$ placeholder feel natural, and is there prior art we’re missing?