tc39/proposal-function-pipe-flow

How much does this help with point-free programming?

Closed this issue · 15 comments

Soon (hopefully) we'll be receiving the pipeline operator, which is great! They're choosing to go the hack-style route, which is great for interoping with existing APIs, but it leaves a nasty taste in your mouth if you're a lover of point-free/tacit style programming.

The primary purpose of this proposal seems to be addressing the need of the tacit-programming side of our community - they were promised a pipeline operator, it was assumed to be tacit-style-friendly, and now they're not getting one, which is frustrating I'm sure. A new API, like Function.pipe() could be used to address the needs of this side of the community.

But, I'm left wondering how helpful this is really going to be. The only benefit of using Function.pipe() over the standard pipe operator is for tacit-programming, which depends heavily on the presence of curried functions. The standard library is not curried, and generally, our community-provided libraries are not curried either (unless they're somehow related to functional programming). So even if we provide a standard, tacit-programming-friendly pipe function, is it really going to help much? tacit-style-programmers are still going to have to hand-curry everything the standard and third-party libraries offer, or they're going to have to install a functional library that basically provides its own, substitute standard library (like Rambda). In either scenario, it's not that hard to hand-add yet another utility function (a one-line definition for pipe()), or to use a pipe function provided by Rambda.

Perhaps the answer is one of these?

  • Maybe the value of a built-in tacit-friendly pipe function is much more important than I'm realizing? If so, would someone be able to explain why a Function.pipe() is important to have, despite the non-curried nature of the standard library you have to deal with. To me, adding a Function.pipe() just seems like a small drop of improvement in a sea of missing tacit-friendly functionality.
  • Or, maybe this is a sign of things to come, and the language designers are wanting to better support tacit programming in JavaScript? I haven't heard of anything along these lines, nor do I know how reasonably possible this is.
  • Or, perhaps there's other uses for Function.pipe() besides helping our tacit-programming friends? If so, what? Are these other use-cases important enough to give enough value to Function.pipe() to introduce it into the language?
  • Or is there something else I'm missing?

One thing I definitely like about functions that we don't get with operators in JS (sadly) is currying/partial application. I often define my function compositions (either compose or, more often, pipe) progressively through several steps, using currying/PA, which can you easily do if you have a function but which is impossible with the |> operator.

I don't see currying/PA as inherently dependent on whether you're doing PF/tacit programming style or whether you're using manual arrow-function adapters at each step.

Perhaps this is optimistic considering we're talking about an addition to the standard-library rather than new syntax, but I'd rather hope there would be opportunities for improvements in debugging if there were a native pipe/compose/etc.

I don't see currying/PA as inherently dependent on whether you're doing PF/tacit programming style or whether you're using manual arrow-function adapters at each step.

@getify, could you expound on what you mean by this? If I interpret this correctly, you're saying that currying/PA is useful both for those who like tacit-style programming, and those who prefer explicit arrow-functions in their pipelines. But, I'm not sure I understand why. Isn't the primary point of using currying/PA to enable a point-free style? If you're not going after point-free programming, doesn't an arrow function accomplish anything currying/PA would accomplish? Or, what benefit does currying provide someone if they're not particularly drawn to the ideals that tacit-programming brings?

you're saying that currying/PA is useful both for those who like tacit-style programming, and those who prefer explicit arrow-functions in their pipelines

Yes that is exactly what I'm saying. I often see these topics (PF/tacit vs PA/currying) conflated or connected. They're not. They're orthogonal. You can like using one, or the other, or both at the same time.

In particular, note that I am specifically talking about PA/currying of the pipe(..) call itself, which I do frequently. That, again, is separate from whether you like to use PA/currying on individual steps of the composition to achieve PF/tacit style, or not.

@getify: I’m preparing my presentation to the plenary next week, and I’m writing a slide comparing its tradeoffs to the pipe operator.

I’d like to understand your “PA/currying of the `pipe(..) call itself” use case more; do you have any specific examples of this use case?

(I’d also love more elaboration on this statement: “I often define my function compositions (either compose or, more often, pipe) progressively through several steps, using currying/PA, which can you easily do if you have a function but which is impossible with the |> operator.” The pipe operator should be able to do anything that is possible with function calls, except it may be more verbose for unary function calls.)

“PA/currying of the `pipe(..) call itself” use case more; do you have any specific examples of this use case?

... love more elaboration on this statement...

Here's the kind of code I'm talking about:

function pipe(...fns) { .. }
const curriedPipe = curry(pipe,3);   // <-- can't do this with an operator

// step 1
var buildResponse = curriedPipe( collectData );

// later...

// step 2
buildResponse = buildResponse(
   (isAdmin) ? checkAdminAuth : checkUserAuth
);

// later...

// step 3
buildResponse = buildResponse( addHeaders );

// later...

var resp = buildResponse( data );

Does that help illustrate? It's progressively (spaced out in code, and even in time) building up a composition/pipe function one step at a time, eventually producing the fully composed function (buildResponse(..)) that's ready to use.

With the |> operator, all steps have to be known at once, because it's an eager operationr rather than a lazy definition of a function that can perform an operation later.

[Edit: renaming the below code from pipe to flow, sorry for confusion]

And as a side note, even if you remove currying-of-pipe from the equation, pipe(..) as a function instead of an operator is more convenient when you're progessively building up a list of functions to participate in a composition:

function flow(...fns) { .. }
var steps = [];

// step 1
steps.push( collectData );

// later...

// step 2
steps.push(
   (isAdmin) ? checkAdminAuth : checkUserAuth
);

// later...

// step 3
steps.push( addHeaders );

// later...

var buildResponse = flow( ...steps );  // <-- can't do nearly as easily with an operator
var resp = buildResponse( data );

[Edit: renaming the below discussion from pipe to flow, sorry for confusion]

For clarity... when I know in advance that there will be exactly N steps in a composition that I want to define progressively/lazily, I prefer the curry(..) approach. But it's even more common where conditionals may alter the number of steps in my composition, in which case I use the array-building instead of currying.

But in both cases, I prefer having my flow(..) be a function and not an operator.

It's just not very common for me to manually/explicitly write an eager multi-step piping expression where I'd use the |> operator line after line. I much more often use programming logic and conditionals to build up my compositions.

And for further clarification, to @theScottyJam's questions earlier in the thread... the above code snippets illustrate a pattern of currying (or not) of the composition itself, which is independent of whether, in the various steps of the composition, I use:

  1. already-unary functions, or
  2. inline arrow functions to create the unary functions, or
  3. higher-arity curried/PA'd functions, with partial invocation to create the unary functions

@getify: I’d like to clarify—when you define pipe in your snippets above, are you defining pipe as LTR function application (pipe(x, f0, f1, ...)) or LTR function composition (pipe(f0, f1, ...)(x))? You define pipe with (...fn) arguments but then curry pipe with 3 as its first argument.

Also, thank you for your examples. Basically, it’s about dynamically applying a sequence of callbacks, which indeed no binary operator can do (unless you have a language with binary operators as first-class functions, but, yeah).

Ah, got it. That indeed is a valid example where you can use currying and the pipe operator, without actually caring about point-free programming. I must say, it's a unique way to use pipe() that I haven't seen before either - it sort of feels like you're imperatively building a function from pieces, and bringing those pieces together with pipe() and currying.

Perhaps in more general terms, the use case presented here is a dynamically-generated pipe. When all of the functions in the pipeline are known in advance, then there's not much difference between using the pipeline syntax or a pipe function, but if you're dealing with dynamically-generated steps, you have no choice but to use a function form.

[Edit: renaming the below code from pipe to flow, sorry for confusion]

LTR or [RTL]... You define pipe with (...fn) arguments but then curry pipe with 3 as its first argument

Sorry for the lack of clarity. The 3 is manually indicating intended arity, telling curry how many N steps the composition will require, since it cannot determine the intended arity from the variadic flow(...fns) function signature.

I almost always do LTR style composition when I'm doing dynamic pipelines (with flow(..)), as RTL is a bit more awkward (unless you have a reverse currying going on).

I prefer RTL composition (aka compose(..)) when the number and content of all steps is known upfront and there's no need for a dynamic composition.

For posterity sake, I edited my above few comments to switch from pipe to flow, which is what I meant... sorry for confusion.

Due to its rejection for Stage 1 on 2022-07, I’m withdrawing this proposal. I really appreciate everyone here giving discussion, though. (In the far future, after the pipe operator gains users, pain points with the pipe operator may be enough motivation to revive this proposal.) Happy trails to all.