Babel F# pipeline operator

Introduction

Writing readable and declarative code in JavaScript, how nice it would be! 😆
I know, readable and declarative don’t go in the same sentence when talking about JavaScript, but should it be always like this? The community has been looking for ways to improve these problems with the language for years.

Since Babel 7.0 there have been a few proposals implemented that lean towards the readable and declarative JavaScript goal. I’m sure you have heard about the pipeline operator, but if you haven’t then it’s fine, this post will guide you through. This operator is syntactic sugar with the intention to make our code really sweet (pun intended). In this post I’m going to discuss one of the competing proposals, the F# variant.

Babel pipeline operator proposal

The community has been working on the pipeline operator proposal for quite sometime now. To be precise, they managed to release the minimal variant of this proposal in Babel 7.0.0-beta.

The minimal proposal covers only the basics around the pipeline operator, so features like partial function application and async/await don’t work.

They decided to play around with this proposal and work with few more variants on the same operator, which actually extend the minimal variant. Some proposed variants didn’t make it, like Hack or Split style variants, but we have two at the moment, which both are available in Babel 7.5.0 version. The people who are working on this proposal want to compare the trade-offs between the two competing proposals, which as I mentioned earlier, are based on the minimal variant.

One of the competing proposals is the Smart style variant, which was introduced in Babel 7.3.0, while the other is the F# style variant, which has been introduced in Babel 7.5.0. To use any of these three variants (minimal, smart, F#), you must use a Babel plugin. More on this below.

The operator

The pipeline operator (|>) takes two arguments. The second one, on the right side, is a function, while the first one, on the left side, is a value. The pipeline operator provides that value as an argument to the function on the right and returns the result.

If you think about it, it’s very similar to chaining functions, like the old-fashioned jQuery for example, however it’s not limited to intrinsic methods of an object and also it’s way more declarative.

// Chaining functions with jQuery
var banner = $('#banner-message');
banner
.css('background-color', '#eee')
.fadeOut(300)
.fadeIn(250)
.append($('<hr>'))
.append($('<footer>', { text: 'This is the footer' }));
view raw jquery.js hosted with ❤ by GitHub

This technique is commonly used to pipe multiple operations in one go. As discussed above, the operator passes a value from the left side to a function on the right side. For the next operation, it passes the result of the previously executed operation, which is a value, to the function on the right and so on and so forth. This continues, until there’s no operation left, i.e. no more pipelining.

const add = (x, y) => {
return x + y;
}
const mul = (x, y) => {
return x * y;
}
const subtract = (x, y) => {
return x - y;
}
const result = 5
|> add(10, ?)
|> mul(2, ?)
|> subtract (?, 3);
console.log("The value is:", result);
// prints: "The value is: 27"
view raw pipeline.js hosted with ❤ by GitHub

Such coding style is very common in FP languages like F#, OCaml, Elixir, etc. and also it can be combined with other powerful functional programming techniques like partial function application and function composition.

Why use it?

Generally speaking, in the functional world, this is a very useful operator. FP languages are notorious of their declarative programming style, which results in more readable code, well at least for people familiar with the language, as for developers who come from an OOP background the code might look cryptic. 😥

However, it’s proven when using these languages, the clutter is minimal and the code has a tendency to read naturally from left to right, like a proper sentence, which makes it easier for one’s brain to follow the code flow and finally make sense from the jargon.

The F# pipeline operator in JavaScript

This operator borrows its style from FP languages like F#, OCaml, Elm, etc. and makes it easier for developers to develop functional solutions in JavaScript. I must inform you that this proposal is still at Stage 1 and it’s been evaluated, so expect things to change in the future, as the proposal matures.

The variants

The F# and Smart variants are quite similar in how they operate, however the smart variant uses a # topic reference. For this variant, each step of the pipeline creates its own lexical scope, with the # topic reference being the result of the previous step. You don’t need to use arrow functions, you just provide the result via the topic reference.

In the F# pipeline, you must use arrow functions, a style which is more close to the JavaScript we all know and love. The partial function application is not supported out of the box if you choose the F# variant, so make sure to install and add it as a plugin in your .babelrc file if you wish to use this technique. You will need the @babel/plugin-proposal-pipeline-operator plugin for the pipeline operator to transpile.

npm install @babel/plugin-proposal-pipeline-operator --save-dev

And in the .babelrc set the plugin as follows for fsharp.

{
"plugins": [
["@babel/plugin-proposal-pipeline-operator", { "proposal": "fsharp" }]
]
}
view raw .babelrc hosted with ❤ by GitHub

Note: For other variants, set the proposal property either to minimal or smart.

The partial application proposal supports the ? reference, which is similar to the # topic reference in the smart variant, but you can pass it only in function calls.

npm install @babel/plugin-proposal-partial-application --save-dev

Then add it to your babel plugins in .babelrc as follows.

{
"plugins": [
"@babel/plugin-proposal-partial-application",
["@babel/plugin-proposal-pipeline-operator", { "proposal": "fsharp" }]
]
}
view raw .babelrc hosted with ❤ by GitHub

Examples

Let’s see an example with the F# variant. First, you have to download and install the babel plugin as shown above. I am going to use partial function application, so make sure to install and add as plugin the partial application proposal plugin as well. See the gist above about the plugins.

const keyboard = { name: 'Keyboard', inStock: true, unitPrice: 125.5, quantity: 2 };
const laptop = { name: 'Laptop', inStock: true, unitPrice: 920.99, quantity: 1 };
const cables = { name: 'HDMI Cables', inStock: true, unitPrice: 12.99, quantity: 4 };
const headphones = { name: 'Headphones', inStock: false, unitPrice: 40, quantity: 1 };
const order = [keyboard, laptop, cables, headphones];
const coupon = 0.2;
const inStock = (product) => product.inStock;
const productPrice = (product) => product.unitPrice * product.quantity;
const applyCoupon = (customerCoupon) => (total) => total - (total * customerCoupon);
const sum = (total, price) => total + price;
const result = order
|> i => i.filter(inStock)
|> i => i.map(productPrice)
|> i => i.reduce(sum, 0)
|> applyCoupon(coupon) // partial function application
|> t => t.toFixed(2);
console.log(`Order total is $${result} by applying a 20% discount`);
// prints: Order total is $979.16 by applying a 20% discount
view raw order.js hosted with ❤ by GitHub

In this example I have a list of order items, which makes an order. I want to calculate the final price for the items in stock and apply a discount of 20%, as the client has a coupon. The operations I mentioned earlier are all performed in sequence thanks to the pipeline operator. Notice the applyCoupon function returns a function which receives a total, but I don’t provide the total explicitly in the parameters. This is called partial function application, the value is passed implicitly by the pipeline operator.

Finally, let’s see how this works with async/await.

const fetch = require('node-fetch');
const url = 'https://restcountries.eu/rest/v2/alpha/gb';
async function getCountry() {
return url
|> fetch
|> await
|> r => r.json()
|> await
|> c => {
return {
name: c.name,
capital: c.capital,
population: c.population,
flag: c.flag
}
};
}
(async () => {
const response = await getCountry();
console.log(response);
})();
view raw async.js hosted with ❤ by GitHub

In this example I want to fetch a country from a remote API and transform the response to an object that I want to show in console. Because I am running these examples in NodeJS and not in browser, I’ve installed a package called node-fetch, in order to use the fetch API.

The pipeline and the await operator work very nice together as we can see. Notice the await operator must go after the promise/asynchronous code, which in this example are the fetch and json methods, which both return a promise. So, always remember to put your await after the promise when pipelining. Think of await as a function, which takes one Promise parameter, unwraps it and returns the underlying value.

You can find additional motivating examples at the TC39 Pipeline Operator proposal page and explore other interesting use cases. You can use the pipeline operator in several cases in your code, like in a validation scenario, where you want to validate specific attributes of an object, or you can use it to create mixins in a much more concise way, or to create object decorators as well as many other use cases that are presented in the official proposal.

Why nesting function calls is bad?

This is the traditional way, it’s the “pipelining before it was cool”. With this technique we nest multiple function calls, making something that resembles a Christmas tree.

const append = (text) => (value) => value + text;
const removeTheLetter = (letter) => (sentence) =>
sentence
.split('')
.filter(c => c !== letter)
.join('');
const makeLetterUpperCase = (letter) => (sentence) =>
sentence.split('')
.map(c => c === letter ? c.toUpperCase() : c)
.join('');
const removeWordsWithLessThanFourLetters = (sentence) =>
sentence
.split(' ')
.filter(word => word.length > 4)
.join(' ');
const phrase = 'Welcome';
const result = removeWordsWithLessThanFourLetters(
makeLetterUpperCase('e')(
makeLetterUpperCase('a')(
makeLetterUpperCase('o')(
removeTheLetter('i')(
append(' pipelining')(
append(' I hope you like')(
append(' Functional Programming')(
append(' to')(phrase)
)
)
)
)
)
)
)
);
console.log(result);
// prints: WElcOmE FunctOnAl PrOgrAmmng ppElnng
view raw pipeline.js hosted with ❤ by GitHub

Please admit it. It’s ugly. That’s the reason the pipe function was invented, which does the same as above, a bit more elegantly.

Why choose this over the pipe() method?

Libraries like Ramda and RxJS provide a pipe() function which you can use to pipe operations, very much like the pipeline operator. Techniques such these have been around for few years, as no pipeline operator existed before in JavaScript. But now, with its arrival, it manages to make nested function calls look clumsy, wordy and obsolete. I have prepared an example, one with RxJS and one with Ramda and finally, the native approach with the pipeline operator. Let’s make it functional all the way.

With this example, I have to first define my functions.

const { of } = require('rxjs');
const { map } = require('rxjs/operators');
const append = (text) => map(x => x + text);
const removeTheLetter = (letter) => map(x =>
x.split('')
.filter(c => c !== letter)
.join(''));
const makeLetterUpperCase = (letter) => map(x =>
x.split('')
.map(c => c === letter ? c.toUpperCase() : c)
.join(''));
const removeWordsWithLessThanFourLetters = map(x =>
x.split(' ')
.filter(word => word.length > 4)
.join(' '));
view raw rxjs.js hosted with ❤ by GitHub

Now, here’s the pipe function in action.

const phrase = 'Welcome';
const observable = of(phrase);
observable.pipe(
append(' to'),
append(' Functional Programming'),
append(' I hope you like'),
append(' pipelining!'),
removeTheLetter('i'),
makeLetterUpperCase('o'),
makeLetterUpperCase('a'),
makeLetterUpperCase('e'),
removeWordsWithLessThanFourLetters // partial function application
).subscribe(x => console.log(x));
// prints: WElcOmE FunctOnAl PrOgrAmmng ppElnng!
view raw rxjs.js hosted with ❤ by GitHub

I have to nest the functions in the pipe function, list them in order, so they execute in sequence and finally subscribe to the observable, which is returned by the pipe function, in order to read the raw value.

The same example now in Ramda. I must admit I like Ramda more when working with Functional stuff in JS, mostly for its simplicity. I’ll setup my functions first in a similar manner.

const R = require('ramda');
const append = (text) => (value) => value + text;
const removeTheLetter = (letter) => (sentence) =>
sentence
.split('')
.filter(c => c !== letter)
.join('');
const makeLetterUpperCase = (letter) => (sentence) =>
sentence.split('')
.map(c => c === letter ? c.toUpperCase() : c)
.join('');
const removeWordsWithLessThanFourLetters = (sentence) =>
sentence
.split(' ')
.filter(word => word.length > 4)
.join(' ');
view raw ramda.js hosted with ❤ by GitHub

Now, I call the pipe method of Ramda and I provide the phrase as a parameter. In the same fashion, all functions in my pipe will run sequentially.

const phrase = 'Welcome';
const result = R.pipe(
append(' to'),
append(' Functional Programming'),
append(' I hope you like'),
append(' pipelining!'),
removeTheLetter('i'),
makeLetterUpperCase('o'),
makeLetterUpperCase('a'),
makeLetterUpperCase('e'),
removeWordsWithLessThanFourLetters // partial function application
)(phrase);
console.log(result);
// prints: WElcOmE FunctOnAl PrOgrAmmng ppElnng!
view raw ramda.js hosted with ❤ by GitHub

Finally, it’s time for the pipeline operator. As with previous examples, I will set my functions upfront. I’ve made them a bit more functional compared to previous examples, plus they use the pipeline operator for internal operations.

const splitCharacters = (str) => str.split('');
const splitWords = (str) => str.split(' ');
const combineAllCharacters = (str) => str.join('');
const filter = (fn) => (array) => array.filter(fn);
const map = (fn) => (array) => array.map(fn);
const append = (text) => (value) => value + text;
const removeTheLetter = (letter) => (sentence) =>
sentence
|> splitCharacters
|> filter(c => c !== letter)
|> combineAllCharacters;
const makeLetterUpperCase = (letter) => (sentence) =>
sentence
|> splitCharacters
|> map(c => c === letter ? c.toUpperCase() : c)
|> combineAllCharacters;
const removeWordsWithLessThanFourLetters = (sentence) =>
sentence
|> splitWords
|> filter(word => word.length > 4 )
|> combineAllCharacters;
view raw pipeline.js hosted with ❤ by GitHub

Now that my functions are ready to use, let’s get the result.

const phrase = 'Welcome';
const result = phrase
|> append(' to')
|> append(' Functional Programming')
|> append(' I hope you like')
|> append(' pipelining!')
|> removeTheLetter('i')
|> makeLetterUpperCase('o')
|> makeLetterUpperCase('a')
|> makeLetterUpperCase('e')
|> removeWordsWithLessThanFourLetters // partial function application
console.log(result);
// prints: WElcOmE FunctOnAl PrOgrAmmng ppElnng!
view raw pipeline.js hosted with ❤ by GitHub

I like the latter most, I don’t have to learn a new library and its intricacies nor I need to install it and import it, I am just using native stuff and Babel will transpile it accordingly for me.

Notice also how different this looks from the example which nests multiple function calls. The order is completely opposite. The pipelining example reads naturally, while the nesting example is awkward and hard to understand.


Another advantage is that you don’t have to use third party libraries like RxJS or at least some of their functions like pipe(), which only adds to your bundle size. You’ll have to worry for less bandwidth to download for your consumers and less libraries to support in-project, for your developers.

It’s awesome how the language has progressed, from being so fragmented and badly implemented, to a better version of itself through tools like Babel and Typescript. Less and less utility libraries are needed currently to develop a web application, with developers finally putting more trust in native code.


If you liked this blog, please like and share! For more, follow me on Twitter.