Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shorthand for pipe function definition? #131

Closed
chrants opened this issue Aug 28, 2018 · 18 comments
Closed

Shorthand for pipe function definition? #131

chrants opened this issue Aug 28, 2018 · 18 comments

Comments

@chrants
Copy link

chrants commented Aug 28, 2018

I believe the pipeline operator will bring only a small short-hand syntax for something that can be implemented with a simple function definition.

WORKING EXAMPLE MADE ON THE SPOT:

const pipe = (obj, ...funcs) => funcs.reduce((o, f) => f(o), obj);

const double = (n) => n * 2;
const increment = (n) => n + 1;

// without pipeline operator
double(increment(double(double(5)))); // 42

// with pipeline operator
5 |> double |> double |> increment |> double; // 42

// with pipe function
pipe(5, double, double, increment, double); // 42

My question is does this merely bring some short-hand syntactic sugar for a set of functions that could be implemented in a few lines in current ES, or does it provide some substantial functionality besides that short-hand?

An alternative may be to add a set of equivalent functions with the functionality into the language. For example, a pipe function, as defined above, would probably suffice for most use-cases of the |> operator.

@chrants chrants changed the title Short-cut for function? Short-cut for function definition? Aug 28, 2018
@chrants chrants changed the title Short-cut for function definition? Shortcut for pipe function definition? Aug 28, 2018
@chrants chrants changed the title Shortcut for pipe function definition? Shortcut for pipe function definition? Aug 28, 2018
@chrants chrants changed the title Shortcut for pipe function definition? Shorthand for pipe function definition? Aug 28, 2018
@ljharb
Copy link
Member

ljharb commented Aug 28, 2018

Syntactic sugar is not "merely", it can be very important for clarity, lint-ability, expressiveness, teachability, etc.

@chrants
Copy link
Author

chrants commented Aug 28, 2018

It's correct that it is not "merely" syntactic sugar, I suppose. I think for this case, there may be new cases for linting or expressiveness, this is true.

However, I believe that you achieve some degree of expressiveness with having the function definition as well, the degree of difference in expressiveness is up to opinion.

I believe that clarity gain depends on two parts: it's teachability (if it's not easy to teach then it's not clear to many), and the extent that the people that understand the feature can come back to their code later and read their code. I think that the teachability (relevant: #82) aspect suffers because this is not a familiar construct in most C-like languages and breaks C function call semantics (relevant: #132). I think that how easy it is to read, as opposed to the pipe function definition above, is marginal in difference, though there definitely is some difference that makes it easier to read for those that understand it.

To summarize:

  • The syntax does benefit in lintability and probably a couple other things over a function definition
  • Expressiveness difference between the function definition pipe and the pipeline operator is up to opinion.
  • Clarity gain is potentially there, but the difference it makes might be negligible or even harmful net because of how clear it is to newcomers.

@ljharb
Copy link
Member

ljharb commented Aug 28, 2018

Teachability isn't solely/primarily about familiarity to other languages; the majority of JS developers in the future won't have ever touched another language before.

@chrants
Copy link
Author

chrants commented Aug 29, 2018

Let's say that teachability is or is not a concern, up for debate, for the sake of argument.

The question is now:
Which is a preferable option- a function definition of pipe, or a |> pipeline operator?

I know that the purpose of this proposal is for the operator, but let's put that aside for a moment.

I believe that (from a convenience sample in my workplace) introducing a new operator |> provides marginal benefit (if any) over the introduction of a pipe function.

In fact, the front-end developers in my company, mixed of different types of framework developers from different backgrounds and different language preferences, preferred the function definition over the operator. They said that the pipe function was clear and was easier to think about / adopt due to it being a mere function call over a completely new syntax.

The need for piping was clear. It provides a nice concise way of chaining function calls, among other things. They expressed, before the pipe function I proposed, that the |> operator was awesome. But when presented with the pipe function, they said that the pipe function was favorable.

@ljharb
Copy link
Member

ljharb commented Aug 29, 2018

Doing anecdotal gathering of statistically insignificant data will favor whatever you want it to favor, depending on who you ask. I (and many developers I've talked to, not that that's a meaningful way to make design decisions) find syntax far more preferable to a function.

@chrants
Copy link
Author

chrants commented Aug 29, 2018

My point isn't that my small, probably biased sample (as yours is as well), holds credibility to say a pipe function is definitely better than a |> pipeline operator.

My point merely is to illustrate that if the pipe function is seriously considered that it might be a viable alternative to the |> pipeline operator.

The reasons they express are also reasons to consider. It might be easier to think about / adopt in the established JavaScript community because it is simply a function call instead of an entirely new syntax. Plus, polyfills for functions are viable, but polyfills for syntaxes are not.

I ask you to simply consider that it might be a viable alternative for a minute.

@mAAdhaTTah
Copy link
Collaborator

A pipe function cannot handle await'ing in the middle of the pipeline.

@noppa
Copy link
Contributor

noppa commented Aug 29, 2018

Depends on the pipe function.

const pipe = (obj, ...funcs) => {
  if (!funcs.length) return obj
  const fn = funcs.shift()
  return fn === pipe.await ? obj.then(_ => pipe(_, ...funcs)) : pipe(fn(obj), ...funcs)
}
pipe.await = Symbol('pipe/await')

const double = (n) => n * 2;
const increment = (n) => n + 1;
const veryExpensiveCalculation = (n) => Promise.resolve(n)

pipe(5,
  veryExpensiveCalculation,
  pipe.await,
  double,
  double,
  increment,
  double
).then(_ => console.log(_)) // 42

Realistically, though, a combination of pipe and compose utility functions would probably be better.

const pipe = (obj, ...funcs) => funcs.reduce((o, f) => f(o), obj);
const compose = (...funcs) => (obj) => pipe(obj, ...funcs)

const double = (n) => n * 2;
const increment = (n) => n + 1;
const veryExpensiveCalculation = (n) => Promise.resolve(n)

// with pipe function
pipe(5, veryExpensiveCalculation)
  .then(compose(double, double, increment, double))

@chrants
Copy link
Author

chrants commented Aug 29, 2018

@mAAdhaTTah It's actually something that this base proposal doesn't specify either and is an error in the scope of this proposal, as well. Of course, there's still the other proposals out there that have this await as well in their spec.

@noppa
Copy link
Contributor

noppa commented Aug 29, 2018

I have been very eagerly waiting for this operator because this nonsense is such a frequent problem today (example use-case taken from the proposal description):

boundScore(0, 100, add(7, double(person.score)))

// Or with useless helper variables that add no value 
const doubled = double(person.score)
const adjusted = add(7, doubled)
const bound = boundScore(0, 100, adjusted)

That's hard to read, cumbersome to write, and easy to get wrong.
The current solution is to use something like that pipe helper function, but that's not quite ideal either because you still need to handle those additional arguments with arrow functions or currying.

Maybe the partial application proposal would be sufficient to solve this problem on its own?

pipe (
  person.score
  , double
  , add(7, ?)
  , boundScore(0, 100, ?)
)

Looks good to me.
Not sure how well that would be optimizable by the engine, though.

@mAAdhaTTah
Copy link
Collaborator

@chrants The base proposal is unlikely to get accepted, as the committee has made it clear they want a pipeline that handles async / await. It functions as a strawman for comparing the tradeoffs between the other proposals.


@noppa I wouldn't want to hitch the pipeline operator to partial application, but I'm of the opinion, if you get a pipeline operator, solving this with arrow functions is reasonable.

@brodycj
Copy link

brodycj commented Aug 29, 2018

Here is a more flexible pipe function that support both synchronous and asynchronous operation, with a couple tests (though missing error checking):

const pipe = (obj, ...funcs) =>
  funcs.slice(1).reduce((o, f) => (o.then) ? o.then(_ => f(_)) : f(o), funcs[0](obj))

const double = (n) => Promise.resolve().then(() => n * 2)
const doubleNow = (n) => n * 2
const increment = (n) => n + 1

pipe(5, doubleNow, doubleNow, increment, doubleNow) // returns 42

pipe(5, double, double, increment, double, console.log) // outputs 42 to console.log

Some conceptual credit goes to sindresorhus / p-pipe, which does include error checking.

I would personally favor this kind of a solution over adding yet another operator to the JavaScript language.

@gilbert
Copy link
Collaborator

gilbert commented Aug 29, 2018

Ignoring any subjectivity on syntax, the pipe function:

  • Is not easily optimizable by the vm
  • Is not able to be type-inferred by TypeScript
  • Requires writing import pipe from 'some-library' in every file you use it

@dallonf
Copy link

dallonf commented Aug 29, 2018

Devil's advocate here (I still prefer the operator) - I think the OP and advocates here are suggesting a first-class pipe function in the language, which means it could in theory be optimized by the VM, could be special-cased in TypeScript/Flow, and wouldn't need to be imported.

Of course, then we get into the question of where to put/what to call such a function that wouldn't break the web, and it's definitely weird that a particular function would get special treatment by TypeScript/Flow and VMs.

@brodycj
Copy link

brodycj commented Aug 29, 2018

  • Is not able to be type-inferred by TypeScript

(microsoft/TypeScript#25826 (comment))

That point makes me wonder if adding this operator may have an impact on the relationship between JavaScript and TypeScript?

@chrants
Copy link
Author

chrants commented Aug 30, 2018

In the end, I think that coming up with some concrete reasons why the pipeline operator should be used used over alternative approaches might help the operator gain even more traction, so I think that this discussion isn't bad for this family of proposals at all! If something seems to be more favorable, then great! Let's use that instead. Language authors knowing pros / cons of this particular approach can help decisively make it one way or another, and can help in explaining its particular utility to existing developers, as well.

@goodmind
Copy link

goodmind commented Jan 29, 2019

@gilbert @gilbert Flow actually already have special case for it

declare var compose: $Compose
declare var pipe: $ComposeReverse

But I think infix operator is more easy to type than variadic function

@tabatkins
Copy link
Collaborator

Closing this issue, as the proposal has advanced to stage 2 with Hack-style syntax, which brings additional benefits beyond just being syntax sugar for pipe().

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 11, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants