Proposal: A Pipe Operator (|>) for BoxLang

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

  1. 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>. So properName = ("brad" |name> "Mr. #name#")
  2. 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 how cfcatch variables 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?

I like this idea very much.

Some thoughts:

Is $ the right token?
Personally I like the ECMAScript proposal’s % as the token. $$ feels very old-school to me.

What’s the scope of $
I think it has to be its own encapsulated scope - or local if that’s how we handle it with the compilation. Either way, it should never leak out of the “pipe context” it is in.

could [the token] in certain argument positions be lazily evaluated
Yes, please, though I’m not sure how you do that since the pipe has to pass it through anyway

The idea of lazy variables exists in languages like Scala and could
also be a first-class BoxLang feature
Also, yes, please. That said, I would not make lazy its own scope ( as seems to be the case from your example ). I would make it an operator on the scope ( or or the implicit ). So:

lazy var nameEventually

would be the same as

lazy nameEventually

There is something about |> that feels icky to me, IMHO. That seems to be a convention of other languages, and I can live with that, but just a straight pipe operator I would support. I would decrease characters strokes and most people would intuit it easily:

now() | dateFormat( $$, "full" ) | ::ucase

reads very clean to me.

1 Like

Thanks for the feedback!

Personally I like the ECMAScript proposal’s % as the token

I’m very open to ideas. The issue I see with % is it is an existing operator (modulo) and not a valid variable name. It would introduce more parsing ambiguity and expression changes than something that doubles as a valid variable name already and isn’t an known operator.

I’m not sure how you do that since the pipe has to pass it through anyway

I have my own ideas about how we’d implement lazy variables. It would be a wrapper object which housed a functional wrapper which was invoked on access. Every place in the runtime that “uses” a value would need to check and unwrap it like we do for DynamicObject. But I don’t necessarily want to get too far off in the weeds of lazy stuff in the context of the pipe operator.

I would not make lazy its own scope ( as seems to be the case from your example ).

Sorry, if that was unclear. the lazy token in that code sample was an assignment modifier like final and wouldn’t affect the scope. That idea came from Scala, which uses a similar assignment modifier.

Of course, we also have some precedent growing for something like

nameEventually = lazy{ ()=>service.getName() };

using our new convention of thing{...}.

There is something about |> that feels icky to me, IMHO

Totally open to ideas here. The thing I liked best about |> was that it’s not used anywhere else in the language so lexing it is very easy. We do use single pipe chars in our catch statement

try {
} catch ( foo | bar | baz e ) {
}

Workable given the specific context, but there is some usage already for that.

The other reason I liked the |>, albeit a small one, is that if we attempted to do something like my proposal to embed the variable into the operator, the two characters provide nice “bookends” for that. Here that was again in case it got buried in the post above:

properName = ("brad" |name> "Mr. #name#")

where |name> is the pipe operator with an explicit placeholder variable name of name instead of $$ embedded in it. I’m not sold on that, it was just a crazy idea.

now() | dateFormat( $$, "full" ) | ::ucase

Nitpick-- ::ucase returns a reference to invokable function, but doesn’t invoke it. It would have to be ::ucase( $$ ) or, more simply, ucase( $$ ). Some languages that use an explicit placeholder actually enforce at the compilation level that the place holder is used at least once on the RHS of the pipe.

Just adding a thought that came up in the original Slack thread along the lines of existing functionality of the language which already offers something similar to consider.


The very nature of building a pipeline of data does remind me of what we already have in streams

["value"]
  .stream()
  .map( ::ucase )
  .map( ::reverse )
  .map( v->v & " brad" )
  .toArray()[1]  // EULAV brad

or runasync,

runasync( ()->"value" )
  .then( ::ucase )
  .then( ::reverse )
  .then( v->v & " brad" )
  .get()  // EULAV brad

or even an optional/attempt

attempt( "value" )
  .map( ::ucase )
  .map( ::reverse )
  .map( v->v & " brad" )
  .get()  // EULAV brad

:point_up: Each of those existing constructs in our language also give you a “pipe” line to pass a value along, but just using a function semantic at each step.
An argument could be made that these existing constructs make the addition of a pipe operator unnecessary.

Each of those example work and you can run them here

And to see them all side-by-side, the “old nested” way to do that same example would be

reverse( ucase( "value" ) ) & " brad"  // EULAV brad

and the proposed pipe operator version would be:

"value" 
  |> ucase( $$) 
  |> reverse( $$ )
  |> $$ & " brad"  // EULAV brad

I’d like to see this for the 2 use cases:

  1. un-backwards’d method calls - c(b(a()))a() |> b($$) |> c($$)
  2. effectively add non-member methods to types we don’t own or that it doesn’t make sense to globally extend, for example in the string example above
"value"
  |> ucase($$)
  |> reverse($$)
  |> someNonStringMemberMethodThatNowChains($$)

It’s true that this can be achieved in userspace, with less syntactic niceness, as wrapAsPipeable(v).pipe(v => ...).get() for some user-defined version of wrapAsPipeable. But one could imagine a similar argument against array literal syntax: arrayNew and arrayAppend are sufficient to construct and populate an array.