DEV Community

A simple explanation of functional pipe in JavaScript

Ben Lesh on June 27, 2019

Sometimes I'm asked why we don't have "dot-chaining" in RxJS anymore, or why RxJS made the switch to use pipe. There are a lot of reasons, but this...
Collapse
 
mrm8488 profile image
Manuel Romero

Real world use case: slugify a String:

const slugify = s => pipe(
toString,
toLower,
split(' '),
filter(removeDashes),
join('-')
);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
dmitriz profile image
Dmitri Zaitsev

Forgotten (s) at the end?

Collapse
 
mrm8488 profile image
Manuel Romero

Yes. It was like pseudo code

Collapse
 
matejsvajger profile image
Matej Svajger

Very cool article!

I gave an attempt at implementing your proposal for the slugify function if anyone cares to play around: codesandbox.io/s/js-functional-pip...

Collapse
 
leunardo profile image
Leonardo Alves • Edited

We can create another function to let pipe even more readable than pipeWith with the following function:

function pipeValue(args) {
    return {
        to: (...fns) => pipe(...fns)(args)
    }
}

const array = [1,2,3,4];
pipeValue(array).to(
    odds,
    double,
    log
);
Collapse
 
dmitriz profile image
Dmitri Zaitsev

Or use the curried pipeline where you can pass your arguments directly.

Collapse
 
jesse profile image
jesse • Edited

This API really resonates with me. Thanks for sharing.

Collapse
 
josgraha profile image
Joe Graham

Not a fan, this breaks functional composition

Collapse
 
ben profile image
Ben Halpern

Awesome first post Ben, welcome! Sorry I grabbed the premium username space.

Collapse
 
dmitriz profile image
Dmitri Zaitsev

It looks like the pipeline in general provides a really convenient and reusable syntax, also battle-proof from decades of use in Unix and other languages. But the pipeline proposal seems to get stale with multiple disagreements and can take years to complete.

The pipeWith function keeps the linear flow but mixes arguments with functions inside its arguments and requires to begin with array:

pipeWith([1, 2, 3, 4, 5], odds, double, log);
Enter fullscreen mode Exit fullscreen mode

Why not instead use a curried pipeline function with identical functionality:

pipeline(1, 2, 3, 4, 5)(odds, double, log)
Enter fullscreen mode Exit fullscreen mode

Among advantages: no need to wrap arguments in an array and typing the comma is quicker than "|>" that requires 2 Shift + 2 keys (4 in total vs 1 for the comma)?

I have been searching for cleanest and most reusable patterns to write and compose Continuation-Passing-Style functions and this pipeline curried function seems to do "the best job", but I'd be curious to put this claim to test and hear other thoughts.

Collapse
 
thiagorb profile image
Thiago Romão Barcala • Edited

From one side it is sad to have to wait so long for this improvement, but from the other side it is better to wait than to have a modification in the language syntax that doesn’t work well in the future.

Your efforts to find the perfect syntax are definitely valuable, but I think it requires a lot of analysis of the consequences to avoid superficial argumentation. Removing the need to wrap arguments in an array doesn’t necessarily makes things easier, because it is more likely that the argument being piped will be already coming as a variable from somewhere else. So we would rarely construct arrays to pipe, but pipe some existing array. And in this case you would need to spread the array to be able to use it in the pipe. Besides that, the pipeline operator can be used with anything, not only arrays. It is just a different way of writing nested function calls.

Also, the amount of keys pressed to type the code is not that important, because we spend more time reading code than actually typing, so maybe less parentheses is preferable instead of one extra character.

Collapse
 
puiutucutu profile image
puiu

Not data last.

Collapse
 
redbar0n profile image
Magne

Why is data last important? If the function composition grows (multi-line), it can be annoying to go to the end just to see what is passed in at the beginning. Chronological ordering is more readable. If it doesn't pose a clear disadvantage, I don't see why not use it.

Thread Thread
 
misterjones profile image
mister. jones

From what I understand of functional programming, currying typically uses a data-last approach.

It's nice, because it allows you to create functions from composed functions. So, yes, if you're piping a lot of functions into your pipeline, it could be annoying to have to find the end-of-the-line to check what data is actually being piped through. But, I'd argue that if you're piping that many functions through, you should consider making it into a named function anyway, so that readers don't have to parse through every step to understand what it's ultimately doing.

Manuel Romero already posted a slugify function (above) as an example, so I'll riff on that:

const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);

const toLowerCase = str => str.toLowerCase()
const split = separator => str => str.split(separator)
const join = separator => arr => arr.join(separator)

const slugify = pipe(
  String,
  toLowerCase,
  split(' '),
  join('-')
)

fooBarSlug = slugify('Foo Bar Fizz Buzz')
console.log(fooBarSlug)  // foo-bar-fizz-buzz
Enter fullscreen mode Exit fullscreen mode
Collapse
 
hopsken profile image
Hopsken

Great article Ben! When I moved to RxJS 6, I was always wondering why we need the pipe operator, this article really explains.

Collapse
 
fc250152 profile image
Nando

smart and beautiful! kudos, Ben!

a little typo maybe in "Functional programming FTW!" paragraph:
function double(array) {
return arr.map(x => x + x);
}
I think it should be:
return array.map ...
;)

have a good week-end!!

Collapse
 
crazytonyi profile image
Anthony

Really good article. Very insightful!

But! Just to be a nitpicky jerk (and because it's oddly amusing to me to notice this):

Your three initial Array prototype extensions are odds, double, and log. I don't know if there's a defined principle for this, but method naming should be consistent enough to allow for a certain amount of expectation to be met by the developer. My immediate assumption was that log() would return an array of the log of each current array member, consistent with double() (this is why I'm amused with myself, because that's such an unlikely method that has way less value). This speaks to the value of namespacing for the value of grouping methods by functionality. Maybe an Array.Math object would have any math related extensions while Array.Utils could have log(). Or maybe I'm wrong?

Obviously it's not central or relevant to your article, just noticed and thought it is its own interesting topic.

Collapse
 
lorefnon profile image
Lorefnon

Unfortunately typescript doesn't provide a good way to impose type constraints on functions with unlimited variadic arguments.

So pipeline function implementations like the above can be made type safe only by repeated overrides upto an arbitrarily determined maximum arity. Example.

I am curious why the RxJS team didn't opt for a fluent API (like this one) which does not suffer from a similar issue.

Collapse
 
asti profile image
Asti

They originally did use a fluent API. Then for RxJS5... bad decisions were made.

Collapse
 
sammyisa profile image
Sammy Israwi

Fantastic! I wondered about this but I don't think I've ever actually understood it.

I've seen the pipeline operator a lot, though I haven't followed the proposal status. I'm not in love with the syntax but the functionality will be very much a gift!

Collapse
 
kurtz1993 profile image
Luis Hernández

Very understandable article! Also, great to see the pipeline operator proposal at the end :D

Thanks for the explanation on this, Ben!

Collapse
 
normancarcamo profile image
Norman Enmanuel

I would like to implement this pipeline style programming but in real use cases, or better said, in more complex cases where we are used to face like asynchronous code, I have tried and I had to make use of some tricks to keep a nice composition, also debugging can be a headache (worse if you don't apply TDD).

Overall, nice post.

Collapse
 
hersman profile image
Hersh

I think meant for
function double(array) {
return arr.map(x => x + x);
}

to be
function double(array) {
return array.map(x => x + x);
}

Collapse
 
webreflection profile image
Andrea Giammarchi

there is a pipeline operator draft too and a pip.this utility for using Array methods (or any other)

Collapse
 
tobbe profile image
Tobbe Lundberg

If all you need/want is the pipeWith function, it can be written like this (I prefer to call it just pipe):

const pipe = (arg, ...fns) => fns.reduce((v, f) => f(v), arg);
Collapse
 
pianomanfrazier profile image
Ryan Frazier

Reminds me that computer languages are converging on lisp and haskell. I've been using elm a lot lately and loving it.

Collapse
 
oleksandr profile image
Oleksandr • Edited

Thank you for the nice article, Ben.
I would also read a pipe vs lift vs let detailed article. Dont you plan to write one?

Collapse
 
okeeffed profile image
Dennis O'Keeffe

Phenomenal. Always wanted some nice clarity on the why behind piping.

Collapse
 
jopie64 profile image
Johan

We really need that pipeline operator to move to the next stage. It's been sitting there on stage 1 for a long time. Some activity lately, so hopefully in the next tc39 meeting they will make progress