DEV Community

Cover image for Future Javascript: Javascript Pipeline Operators
Johnny Simpson
Johnny Simpson

Posted on • Originally published at fjolt.com

Future Javascript: Javascript Pipeline Operators

Pipeline operators are an upcoming feature to Javascript which gives us another way to pass values through a series of transformations. It gives more context to what developers were trying to achieve when they wrote their code, and allow us to do some cool things to boot. Here, we'll take a quick look at pipeline operators, how they work, and how you can use them today.

Javascript Pipeline Operators: Support

Currently, no browser or server side ECMAScript implementation (like Node.JS) support pipeline operators. You can, however, get them to work using Babel 7.15. You can learn more about installing Babel here, but suffice to say this will allow you to add pipeline operators into your code.

Javascript Pipeline Operators: How they work

Pipeline operators are simply another way to manipulate values in Javascript. The pipeline operator is |>. Suppose we have 3 mathematical functions which add numbers to an input value:

// Adds 1 to a number
let addOne = function(x) {
    return x + 1;
}

// Multiplies a number by 2
let multiplyByTwo = function(x) {
    return x * 2;
}

// Divides a number by 6
let divideBySix = function(x) {
    return x / 6;
}
Enter fullscreen mode Exit fullscreen mode

If we wanted to apply all of these functions to a number we have, we might do something like this today:

let number = 6;
let calculate = addOne(multiplyByTwo(divideBySix(number)));

console.log(calculate); // Returns 3.
Enter fullscreen mode Exit fullscreen mode

Although this works, when using multiple functions this can become quite messy - and often can take many lines. As such, we can simplify the above with a pipeline operator like so:

let number = 6;
let calculate = number |> divideBySix(%) |> multiplyByTwo(%) |> addOne(%);

console.log(calculate); // Returns 3.
Enter fullscreen mode Exit fullscreen mode

As you can see, this simplifies processing of numbers and values so that we can clearly see what is happening. Let's break down what we have done:

First, we use number, and pass it with a pipe operator to divideBySix(). We use % to represent the value from before the pipe operator, in this case, 6 which is in our number variable.
Then we pass the number from divideBySix() to multiplyByTwo(). Again, we use % to represent the outcome of the previous operation, i.e. the value of the divideBySix() function.
Finally, we do it again and addOne() to our value. The outcome is the same, so we still get 3 at the end.

Simplifying Object Mapping with Pipeline Operators

Obviously the above example is a very simple application, but we can also use pipeline operators to do cooler things, like map arrays in a more coherent fashion. For instance, the below takes an object of URL queries, and merges them into a text string which can be added to the end of a URL:

let URLParams = {
    'x' : '10245',
    'linkId': 'eojff-efekv-ef0kw',
    'author' : 'john-smith',
    'featured' : false
}

let getURLQuery = Object.keys(URLParams).map((key) => `${key}=${URLParams[key]}`) |> %.join('&') |> `?${%}`;

// Returns ?x=10245&linkId=eojff-efekv-ef0kw&author=john-smith&featured=false
console.log(getURLQuery);
Enter fullscreen mode Exit fullscreen mode

Conclusion on Javascript Pipeline Operators

Since pipe operators are not widely supported, you can only use this feature with Babel installed. With that said, pipeline operators add a lot of context to your code, and simplify operations so you can expand upon them later. As such, it may be worth giving Babel a try to get this into your code base. If you want to read the full pipeline operator specification, click here.

Discussion (22)

Collapse
lukeshiru profile image
LUKESHIRU • Edited on

This is one of the ES features I want the most. I hope it gets implemented at some point next year.

It saddens me a little that the winning proposal is the Hack approach instead of the F#, mainly because I see more value for the pipe operator in unary functions, and those will be more verbosed just because the most common functions in JS aren't unary :/

Collapse
csaltos profile image
Carlos Saltos • Edited on

If you don’t want to wait you can use them right now with Elm, check them out at elm-lang.org/docs/syntax#operators … and includes a lot of other cool things, specially a very helpful and fast compiler

Collapse
shuckster profile image
Conan

"If you don't want to wait to use a pipeline operator, just convert your entire codebase to a completely different language!"

Yeees... 😁

Just a gentle jab. Anyway, there are other ways of achieving pipelining without the new Hack-pipe operator that we can use today.

Thread Thread
csaltos profile image
Carlos Saltos

Ouch !! … (just a gentle ouch)

Collapse
lukeshiru profile image
LUKESHIRU

I love Elm, but I feel is super staggered at this point (no new versions in a long while), and the web is moving to "Transitional apps" (a mix of SPA and MPA) and Elm is still just doing SPA in the front-end. I Wish they release a new version soon that at least does SRR with hydration, or any kind of improvement that takes Elm in that direction 😊

Thread Thread
csaltos profile image
Carlos Saltos

Yes, Elm is really cool !!

You can use elm-pages for having Elm with hydration, it's really cool, you may give it a check at -> elm-radio.com/episode/elm-pages-v2
and also at -> elm-pages.com/docs/what-is-elm-pages

elm-pages is not the only cool tool, you can use a lot more like the elm time travel for debugging and Elm UI for a good design and interface along with Tailwind CSS Elm modules ... the Elm ecosystem is fascinating and powerful nowadays

Thread Thread
lukeshiru profile image
LUKESHIRU

I know, I keep myself up-to-date with it and use it on some personal projects of mine. Still elm-pages is not quite what I would like. If you take a look at what stuff like Svelte Kit or Qwik, which are taking this "transitional app" approach really seriously. It seems the other big player coming is Remix, and soon React server components. All have really nice mechanisms to only load the JS that's needed (not the entire app), and do a lot of SSR.

The approach that elm-pages does is and old technique at this point. I really wish Elm 0.20.0 is released with stuff like this in mind (0.19.1 was released 2 years ago already, and still no news).

Collapse
tunaxor profile image
Angel Daniel Munoz Gonzalez

Another gentle jab 😁 you could use the F# one directly as well
fable.io

Thread Thread
csaltos profile image
Carlos Saltos

Yes, Fable is great too !! πŸ‘πŸ˜Ž

Collapse
akashkava profile image
Akash Kava

Can anyone explain benefit of this? Except for different syntax, there is no performance benefit. Almost same number of characters are required to write same logic it isn’t even smaller syntax except you can write each one in new line.

This way there are million different syntax offered by various language, bringing all in JavaScript will be complex.

Collapse
peerreynders profile image
peerreynders • Edited on

Can anyone explain benefit of this?

In OO programming there is the concept of the fluent interface that allows you to chain method calls without needing intermediate assignments β€” this for example features prominently in the fluent builder patterm.

The disadvantage of this approach is that:

  • the method has to return an object (which in most cases is just simply this) and
  • the method the next "step" needs to perform has to be present on the returned object. So if you're dealing with with a third party object and you want to use your helper function then you are out of luck (unless you are OK which monkey patching the object first)

In value-oriented programming (most people call it functional programming though that definition is actually narrower) computation is advanced through the "transformation of values" rather than "flow of control".

So with pipelining you can use the "fluent chaining style" on any value including Primitve Values and objects without the required method/function.

Another way of writing

const calculate = addOne(multiplyByTwo(divideBySix(number)));
Enter fullscreen mode Exit fullscreen mode

is

const fns = [divideBySix, multiplyByTwo, addOne];
const calculate = fns.reduce((x, f) => f(x), number);
Enter fullscreen mode Exit fullscreen mode

The advantage with the second approach is that the function applications are now ordered left-to-right which many people find easier to read. The downside is the overhead of reducing an array of functions at runtime when the order and composition can fixed at design time ("Bake, don't fry"). Using the pipeline fixes the the reading order of the functions while not incurring any additional runtime overhead. Ideally the placeholder should be optional so that

const calculate = number |> divideBySix(%) |> multiplyByTwo(%) |> addOne(%);
Enter fullscreen mode Exit fullscreen mode

could be replaced with

const calculate = number |> divideBySix |> multiplyByTwo |> addOne;
Enter fullscreen mode Exit fullscreen mode

for single argument functions but I don't think that is even considered in the current proposal (ReScript has the advantage of resolving the placeholder at compile time; perhaps there are JS runtime performance concerns with an optional placeholder);

This way there are million different syntax offered by various language, bringing all in JavaScript will be complex.

Perhaps you are forgetting that JavaScript started out as "doing Scheme in the browser" β€” making it vaguely look like Java was a marketing stunt.

The later has better visibility and easy to debug.

Frankly that might simply be a result of your own familiarity bias. Most people who were taught imperative programming first, initially perceive functional programming as "less natural" and more difficult. But educator's experience has shown that is largely baby duck syndrome; in fact many students find it easier to start with functional programming first though there are always exceptions.

It should be no more difficult to follow the piped functions in the debugger or throw in a tee function.

const number = 6;
const fns = [divideBySix, multiplyByTwo, tee, addOne];
const calculate = fns.reduce((x, f) => f(x), number);

console.log(calculate);

function tee(x) {
  console.log(`tee: ${x}`);
  return x;
}

function addOne(x) {
  return x + 1;
}

function multiplyByTwo(x) {
  return x * 2;
}

function divideBySix(x) {
  return x / 6;
}
Enter fullscreen mode Exit fullscreen mode

Being imperatively minded also means thinking predominantly in "statements" as computation is about "place-oriented" data manipulation and "flow of control". Value-oriented programming relies heavily on "expressions" instead as computation is expressed as "transformations of values" and the pipe operator expands the expression toolkit.

The reason I am asking as I have written JavaScript engine for .NET,

Was F# ever considered as an option especially given that ML languages are considered to be good for writing compilers?

I understand that

  • hypothetically it should be easier to find C# contributors as F# programmers aren't as common
  • C# may currently have some performance advantages (e.g. asynchronous tasks)
  • C# seems like the safer choice

The issue is that there is already a precedent with Rhino. While it's still around, it seems to be languishing despite being based on something as "safe" and prevalent as Java. And Oracle has abandoned Nashorn in favour of supporting JS directly on GraalVM β€” removing Java as an intermediary.

Another approach would be to simply create a runtime that implements some smaller instruction set (like ES3) as generated by the Closure Compiler.

That said the pipeline operator proposal is only at Stage 2, with reservations as of August 2021 β€” so it may never happen and even in the best case wouldn't enter the spec until 2023 at the earliest.

I would imagine that Pattern Matching would cause a lot more work than the pipeline operator.

Collapse
akashkava profile image
Akash Kava

Thanks for detailed answer, I do find sequence of functions in correct order as important thing as nested function calls are evaluated from depth first order.

Collapse
bamartindev profile image
Brett Martin

Basically its just for readability. In the example above:

addOne(multiplyByTwo(divideBySix(number)))
Enter fullscreen mode Exit fullscreen mode

It takes a bit to parse what is happening, and you have to work inside out. Using the pipeline operator you can make it read a bit more step by step, and you can chain a lot of functions together without all of the parens.

Collapse
akashkava profile image
Akash Kava

Parsing benefit is very small compared to feature overhead. Also for better debugging chaining functions should be avoided, instead minizers can chain functions to reduce code size. And what about multiple parameters? I see no difference in terms of parsing benefit and syntax in below both styles.

var result = a 
    |> b(%) 
    |> c;
Enter fullscreen mode Exit fullscreen mode
var x = b(a);
      x = c(x);
var result = x;
Enter fullscreen mode Exit fullscreen mode

The later has better visibility and easy to debug. The reason I am asking as I have written JavaScript engine for .NET, and we are incorporating more features and I want to implement upcoming features as part of test drive.

I certainly want to know the real benefit, for example ?? has benefit over || as ?? will only check for null or undefined and || will first call valueOf for objects and check if string is empty or not, leading to slower execution. ?. is not only a shorter syntax but faster execution as it will generate smaller machine instructions to duplicate value on stack without causing any overhead of storing value in a variable and evaluating it.

GitHub logo yantrajs / yantra

JavaScript Engine for .NET Standard

YantraJS

Yantra (Machine in Sanskrit) is a Managed JavaScript Engine for .NET Standard written completely in C#.

NuGet

Name Package
YantraJS (With CSX Module Support) NuGet
YantraJS.Core (Compiler) NuGet
YantraJS.ExpressionCompiler (IL Compiler) NuGet
WebAtoms.YantraJS NuGet

Documentation

  1. Introduction
  2. Expression Compiler
  3. JavaScript Engine

Discussions

We recommend using Github Discussion on this repository for any question regarding this product.

Special Thanks

  1. We are thankful to authors of Jurassic (we have incorporated number parser, promise and some unit tests from Jurassic.) github.com/paulbartrum/jurassic
  2. We are thankful to authors of EsprimaDotNet, we initially built prototype over EsprimaDotNet, but we chose to build our parser/scanner from scratch to support token spans. github.com/sebastienros/esprima-do...
  3. We are thankful to author of ILPack (we have incorporated saving IL to Assembly from this library.) github.com/Lokad/ILPack
Thread Thread
bamartindev profile image
Brett Martin

For the first two points, if you are writing JS in a functional style then chaining functions is used often to compose functionality. In those instances, having a single input function is more common.

As far as the second example, if you want to avoid mutations / shadowing, the reassigning variables is something to avoid.

Again, these are personal choices on how to write the JS code. I guess its just part of the challenges of having multiple paradigms in a language. Here is the full proposal though, it will hopefully give more insight into the rationale: github.com/tc39/proposal-pipeline-...

Its still in Stage 2 and there appears to be outstanding questions around it since August.

Collapse
csaltos profile image
Carlos Saltos • Edited on

If you don’t want to wait you can use them right now with Elm, check them out at elm-lang.org/docs/syntax#operators … includes a lot of other cool things, specially a very helpful and fast compiler

Collapse
peerreynders profile image
peerreynders

ReScript also has a pipe operator ->. Interestingly they split the pipeline into two separate features β€” the pipe and the optional pipe placeholder.

Data-first and data-last: a comparison

Most of the functions in JS are n-ary (n > 1) and "Data-first". The "triangle pipe" has more utility in "Data-last" languages that support currying.

BuckleScript Pipe Operator API Review

In the case of Elixir the pipe operator targets the first argument of the function as currying isn't supported.

Collapse
csaltos profile image
Carlos Saltos

Yes, ReScript is also fascinating !! … thank God we have better alternatives to just JavaScript or TypeScript !!

Collapse
tarikoez profile image
TarikOez

Reminds me of Ocaml

Collapse
peerreynders profile image
peerreynders

TC39 Proposal readme:

that is, the F# pipe operator – has been rejected twice by TC39.

So currently the Hack pipes proposal is the only one in the running (stage 2; no OCaml-style reverse-application operator).

Aside: Archeological Semiotics: The Birth of the Pipeline Symbol, 1994

Douglas McIlroy conceived the notion of the "pipe" which Ken Thompson added to Version 3 Unix in 1973. The | character wasn’t associated with it until Version 4 Unix in 1978.

Collapse
hilleer profile image
Daniel Hillmann

This looks quite exciting and useful! Thanks for sharing.