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

The |> operator would be great for function composition instead. Explanation... #50

Closed
TheNavigateur opened this issue Aug 24, 2017 · 38 comments

Comments

@TheNavigateur
Copy link
Contributor

The existing code

const doubleThenSquare = value=>square(double(value))

would be rewritable as:

const doubleThenSquare = double |> square

Reasons to prefer this instead of |> being a function call operator:

  1. Genuinely saves code
  2. The tersely composed functions can be called any time and be tersely used to compose other functions
  3. Has logical consistency (FunctionExpression |> FunctionExpression instead of InputExpression |> FunctionExpression |> FunctionExpression)

Fuller explanation here:

https://github.com/TheNavigateur/proposal-pipeline-operator-for-function-composition
https://esdiscuss.org/topic/native-function-composition

@Volune
Copy link

Volune commented Aug 25, 2017

With the current proposition, you can write

const doubleThenSquare = value=>value |> double |> square;

You can also write

const result = 42 |> double |> square;

With your change, this second example would become:

const result = (double |> square)(42);

In my opinion, the extra parenthesis are an issue. It's easier to compose functions, but chaining calls would be less maintainable.

const map = f => function*(iterable) { for(const v of iterable) yield f(v); };
const tap = f => function*(iterable) { for(const v of iterable) {f(v); yield v;} };
const toArray = () => iterable => [...iterable];
const filter = f => function*(iterable) { for(const v of iterable) if(f(v)) yield v; };

const result = (
    filter(x => (x % 2 == 0))
    |> map(x => x * 2)
    |> tap(x => {
       console.log(x);
    })
    |> toArray()
)([1,2,3,4,5]);

@TheNavigateur
Copy link
Contributor Author

TheNavigateur commented Aug 25, 2017

Not sure I agree that chaining calls would be less maintainable: Current proposal provides an "alternative syntax for chaining calls", which can lead to inconsistent code where some people choose x(y(v)) and others choose v|>y|>x or a combination thereof. Using |> for function composition on the other hand provides a clear separation of meaning, which I think could be better for readability, which I think could feed right into maintainability.

Also it is worth considering the use cases for such constructs. Is it more useful to create reusable composed functions or execute the composition in place? I would argue that having the terse syntax for reusable function composition adds a whole new dimension of expressive power to the language, whereas the proposal doesn't save any coding complexity as such over x(y(v)), just linearises it.

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Aug 25, 2017

FWIW, this example:

const result = (double |> square)(42);

isn't that different from how you have to do it now using a pipe implementation:

const result = pipe(double, square)(42);

so there are some parallels to current practice with @TheNavigateur's suggestion.

In my experience, a function composition is rarely called where it's defined; that suggests the syntax that favors composition over evaluation might be better for the language.

@littledan
Copy link
Member

I am not sure how the semantics for this should work. Do you want |> to do a different thing depending on whether the left hand side argument is a function or not?

@TheNavigateur
Copy link
Contributor Author

No, always function composition

@littledan
Copy link
Member

If it's always function composition, that would make the normal use case of x |> f |> g ==> g(f(x)) not work. Is this what you would intend?

@mAAdhaTTah
Copy link
Collaborator

I believe that's the intention. I would argue the use case of calling inline is less common than defining and executing functions separately.

@TheNavigateur
Copy link
Contributor Author

Yes. (f |> g)(x) for immediate calling, f |> g for creating a reusable composed function.

@dallonf
Copy link

dallonf commented Aug 25, 2017

I'm not an expert on language design, but if I may weigh in...

Both directions of the proposal do things that can already be done in the language...

// Equivalent in proposal as written
const value = x |> a |> b |> c;
const value = compose(a, b, c)(x); //assuming a third-party `compose` function, provided in many libraries
const value = pipeline(x, a, b, c); //assuming a third-party `pipeline` function, which I've never seen before in a library and in fact wound up writing one myself*
const value = c(b(a(x)));

// Equivalent in proposal according to this issue
const fn = x => c(b(a(x)));
const fn = compose(a, b, c); //assuming a third-party `compose` function, provided in many libraries
const fn => a |> b |> c;

The prior art cited in the proposal README suggests that several existing functional languages use the |> symbol for immediate execution (and some others use a different symbol but have the same feature); I don't believe any existing languages use this symbol for function composition. (do correct me if I'm wrong)

In my opinion, the proposal as written benefits everyone - functional and imperative styles alike - in fact, it can help imperative style developers begin to adopt a functional style; imagine if jQuery were written with this syntax instead of creating custom objects to allow chaining. Changing it to a function composition operator only benefits functional-centric developers.

To that point, I strongly disagree with @mAAdhaTTah's suggestion that "the use case of calling inline is less common than defining and executing functions separately"; this has not been my experience with JavaScript at all.

I think function composition is great, but please don't hijack a good proposal like this! How about a separate operator? Or even just including a native compose function in the spec?

*shameless plug: https://github.com/phunware/pipeline

@TheNavigateur
Copy link
Contributor Author

TheNavigateur commented Aug 25, 2017

I disagree that changing it to a function composition operator only benefits functional-centric developers.

Consider the following use case for declaring no-param, no-result functions to be executed in order:

Composition operator:

const switchOnEngineThenDrive = switchOnEngine |> drive

This is beautiful chaining of imperatively ordered functions in a short syntax.

Consider the equivalent for the immediate execution operator:

const switchOnEngineThenDrive = ()=>(undefined |> switchOnEngine |> drive)

(This already highlights the logical inconsistency in having InputExpression |> FunctionExpression |> FunctionExpression as opposed to just FunctionExpression |> FunctionExpression. There is a readability predictably about always having only a FunctionExpression before a |>.)

Now let's look at immediate execution for the same:

Composition operator:

(switchOnEngine |> drive)()

Immediate execution operator:

undefined |> switchOnEngine |> drive

I would argue that the composition operator approach is preferable for imperative style development too. Reusability, readability, intuitiveness

@mAAdhaTTah
Copy link
Collaborator

To that point, I strongly disagree with @mAAdhaTTah's suggestion that "the use case of calling inline is less common than defining and executing functions separately"; this has not been my experience with JavaScript at all.

Fair; that's likely specific to functional programming, which is what I expect the proposal is likely to be most popular/useful to.

@jasmith79
Copy link

jasmith79 commented Aug 25, 2017

@dallonf "never seen and ended up writing myself"?

http://ramdajs.com/docs/#pipe
https://lodash.com/docs/4.17.4#flow
https://vanslaars.io/post/create-pipe-function/

I'm too lazy to search for more. Or did I misunderstand what you were driving at?

@littledan
Copy link
Member

There's clearly a use case for both of these, and you can definitely use one to get the other, at the cost of a little bit more syntax. The question is more like, which one is more common and intuitive.

@dallonf
Copy link

dallonf commented Aug 25, 2017

@jasmith79 All three of the examples you gave are for function composition, ex. pipe(a, b, c) is equivalent to x => c(b(a(x))). I called this compose in my examples and, as I mentioned, it is quite common in third-party libraries. Less common is a function that replicates the functionality of this proposal, ex. pipeline(x, a, b, c) is equivalent to c(b(a(x))).

To other responses... to be clear, I see the value in function composition. But I see it as a tool to solve a different problem than the pipeline operator is intended to solve, so I believe deserves its own proposal. (I would be strongly in favor of making that compose function which is so common in third-party libraries part of the core language, for example).

However, this discussion is not a matter of proving that function composition is useful, or even that it is more useful; in order to turn the immediate execution pipeline operator proposal into a function composition proposal, you would have to determine that immediate execution has no value at all and has no place in the language, which I do strongly disagree with and I believe I am backed up by the fact that immediate execution pipeline operators can be found in F#, OCaml, Elixir, Elm, Julia, Hack, and LiveScript (I copied that list from the README - but I'll also add Clojure's equivalent).

In conclusion...
Why don't we have both?

@TheNavigateur
Copy link
Contributor Author

TheNavigateur commented Aug 25, 2017

I don't think it's a question of whether immediate execution has "no value at all". The composition operator version achieves immediate execution with mere parentheses:

(switchOnEngine |> drive)(100)

The converse is not true: doing composition with the immediate execution operator requires value=> and value |> in each case, at the least. Therefore I would suggest that the composition operator would more effectively take care of a wider range of use cases already. I would even argue it handles immediate execution more readably and intuitively. (And for immediate execution with no args, do you really prefer undefined |> switchOnEngine |> drive more than (switchOnEngine |> drive)()?)

The other issue, which maybe you can address, is the unintuitiveness of expression arguments InputExpression |> FunctionExpression |> FunctionExpression |> ... and so on. Logically you would normally want expressions surrounding an operator to be treated the same regardless of which part of the code you are looking at (e.g. + - / * % || >> && etc.). However, this treats the first argument differently than the 2nd argument onwards, even though it's the same operator. Does this not bother you at all?

@TheNavigateur
Copy link
Contributor Author

TheNavigateur commented Aug 25, 2017

I just had another thought. If you want to compose an array of functions, identified e.g. from server side data, in a loop you could simply do:

composedFunction |>= iteratedFunction

as an analogy for how x += y is shorthand for x = x + y, as shorthand for

composedFunction = composedFunction |> iteratedFunction

...with a composition operator.

@mAAdhaTTah
Copy link
Collaborator

you would have to determine that immediate execution has no value at all and has no place in the language

I strongly disagree with this characterization of the argument. It's not that immediate execution has no value at all; it's that the value of immediate execution for this particular syntax is lower than using said syntax for composition.

I'm happy to have both, but if we wanted both, we'd need either a way to disambiguate between the two intents for this syntax, or an alternative syntax for one of the options. To my mind, wrapping the |> in parens to execute, as in @TheNavigateur's example here:

(switchOnEngine |> drive)(100)

already is that alternative syntax for immediate execution. By treating the |> as function composition instead of immediate execution, you get both, without needing to add two different syntactical constructs to achieve similar aims.

@mAAdhaTTah
Copy link
Collaborator

I would also point out that the languages you point to with the |> operator as immediate execution already have means/syntax for function composition, whereas JS does not.

To counter-point my own argument, using |> to mean something different from what it means in other languages might be counter-intuitive for those transitioning from JS, which would be a good argument for my suggestion above: two different syntaxes for pipelining vs. composition. Elm uses <<, for example, so there may be alternative syntaxes for composition we might consider.

Counter to the counter: I much prefer piping over composition; right to left makes more intuitive sense to me, personally, and I pretty much exclusively use R.pipe > R.compose. |> is a really elegant syntax for piping, and would much prefer using it to build up a pipeline of functions than executing a pipeline, but that's just me.

Lastly, on deeper thought, the iteration example is quite cool. Would make for some clean implementations fpr generating new functions w/ reduce.

@Kerrick
Copy link
Contributor

Kerrick commented Aug 25, 2017

Both proposals have an "ugly" use case when you use them for the other. As-written, you have to use an arrow function to make composition work with the |> operator. As you propose, you have to use parentheses and change the order to make immediate execution work with it.

You're proposing that x => x |> foo |> bar is "uglier" than (foo |> bar)(x), then?

@mAAdhaTTah
Copy link
Collaborator

I'd consider them equally ugly, personally, but the composition use-case more consistent (everything is a function; the first param isn't unique) and more useful (imo, obvs; I see declaration/ execution separation as more common in fp style).

Also (foo |> bar)(x) is more explicit; we're already primed to see someFunc(x) to be an execution, so that translates to executing a pipeline with this syntax.

@matthewwithanm
Copy link

matthewwithanm commented Aug 26, 2017

I think function composition is great, but please don't hijack a good proposal like this! How about a separate operator? Or even just including a native compose function in the spec?

👍

Your proposal seems unrelated to this one except for the fact that you want to use the same character combination. Given that that's a pretty small thing and that there's ample precedent for using |> for piping in other (and related) languages, it seems like there's no reason to hold this one up imo.

@dallonf
Copy link

dallonf commented Aug 26, 2017

I think the (a |> b |> c)(x) example of immediate execution is oversimplified compared to actual use cases.

Consider a case where you mix classic dot-chaining with piping:

// before, pretty ugly, doesn't represent the flow well:
renderLeaderboard('#my-div',
  Lazy(
    getAllPlayers()
      .filter( p => p.score > 100 )
      .sort()
  ) .map( p => p.name )
    .take(5)
);

// after, immediate pipeline:
// It reads in the order it happens
getAllPlayers()
  .filter( p => p.score > 100 )
  .sort()
|> _ => Lazy(_)
  .map(player => player.name)
  .take(5)
|> renderLeaderboard.bind(undefined, '#my-div') // (partial application with .bind)

// after, with composition:
// Oh no now it's even worse than before
(_ => Lazy(_)
  .map(player => player.name)
  .take(5)
|> renderLeaderboard.bind(undefined, '#my-div'))(
  getAllPlayers()
  .filter( p => p.score > 100 )
  .sort()
)

The fact that the input comes first (which @TheNavigateur keeps calling out as unintuitive) is important to this proposal. It's the whole point of it, really: the value flows through the functions in reading order.

Again, I re-iterate that we need two different operators. Or an operator and a function. Or two functions. Two proposals, in any case. Function composition and value piping are not the same problem and trying to fix both with one is going to leave one or both problems with a suboptimal solution.

Given the prior art for |> for value piping, I think this proposal makes sense as is. Other functional languages have proven the value of the feature and established that particular combination of symbols as the norm.

Which leads us to, again, a new proposal for function composition. This is also a feature with lots of prior art and a proven value - particularly in the languages that have a pipeline operator! (This is evidence for the value for having both.)

Those languages, unfortunately, tend to use . or >> for function composition... which are sort of taken in JavaScript. So that's a bummer. Unfortunately I don't know enough about the JavaScript spec to suggest alternatives that don't conflict... >.>? >|>? Or maybe even just a compose() function? Actually, isn't there a proposal floating around for a Function.prototype.compose()?

Or, perhaps you do take |> for function composition (although I'm not sure why, given the prior art) and we find a new one for the pipeline. Possibly Clojure's ->?

@TehShrike
Copy link
Collaborator

Well said by @matthewwithanm and @dallonf.

From the readme of this repo:

This proposal introduces a new operator |> similar to F#, OCaml, Elixir, Elm, Julia, Hack, and LiveScript, as well as UNIX pipes. It's a backwards-compatible way of streamlining chained function calls in a readable, functional manner

The pipeline operator is essentially a useful syntactic sugar on a function call with a single argument. In other words, sqrt(64) is equivalent to 64 |> sqrt.

If there is a new proposal for function composition to be added to the language as a function or operator, I would happily support and follow it. But that is not what this proposal is.

I would encourage anyone interested in pursuing this further to create a new proposal repo. Pasting a link to that proposal would be appropriate.

I'll close this topic in a little bit barring some unexpected input, but it would be good if this topic could be closed out with a link to the new proposal or discussion. I think there is a lot of overlap between people following this proposal and people who would be interested in a function composition proposal.

@TheNavigateur
Copy link
Contributor Author

Allowing InputExpression at the start but with a different operator would be perfect for everybody:

100 |> startEngine +> drive

where +> becomes the function composition operator, which glues with a higher precedence to avoid having to write parentheses here (and exactly like * does compared to +).

I consider this a significant enhancement to this proposal.

@Kerrick
Copy link
Contributor

Kerrick commented Aug 27, 2017

I could back a second, separate proposal like +> (or another) for function composition, which could be used in concert with the pipeline operator as proposed here (since it would theoretically return a FunctionExpression, which could exist as the second item in a pipeline chain) -- but I don't think it needs to be a part of this one.

Perfect is the enemy of good.

@mAAdhaTTah
Copy link
Collaborator

Given the above comments, I'm in favor of the current proposal. Definitely would be interested in working on composition as well.

@TheNavigateur
Copy link
Contributor Author

TheNavigateur commented Aug 27, 2017

Should both 100 |> startEngine +> drive +> useCruiseControl and 100 |> startEngine |> drive |> useCruiseControl be valid and mean the same thing in JavaScript? The first is perfectly valid and de-necessitates the second, IF we accept a function composition operator, because it is semantically identical.

Can, then, the

Parameter |> Function |> Function |> ...

syntax be sustained in light of 2 things:

  1. The logical inconsistency of the |> operator meaning 2 different things depending on whether or not it's the 1st one in the statement
  2. The ability to express the same thing in the same order via a syntax that doesn't suffer from 1., while adding a whole dimension of expressive power to the language

?

I would argue not, and so surely either +> should become part of this proposal OR this proposal should be simplified to Parameter | Function

@TehShrike
Copy link
Collaborator

I'll close this now, as the consensus seems to be that a pipe operator has no reason to be complected with function composition, but a link to a function composition proposal or further discussion in another forum would still be appropriate.

@TheNavigateur
Copy link
Contributor Author

I would argue not, and so surely it would become part of this proposal

@TheNavigateur
Copy link
Contributor Author

TehStrike I pressed close by accident. I belive this very much open as it would impact the contents of this proposal

@mAAdhaTTah
Copy link
Collaborator

We could make this valid syntax if we proposed a composition operator that made it work thus:

100 |> startEngine +> drive +> useCruiseControl

// same as

const startCart = startEngine +> drive +> useCruiseControl

100 |> startCar

if we propose +> on its own is a composition operator. The two proposals aren't mutually exclusive, and advancing the proposal as-is doesn't make it contingent on accepting +> as well.

@js-choi
Copy link
Collaborator

js-choi commented Aug 27, 2017

Just so, as @mAAdhaTTah and @TehShrike said. The hypothetical binary functional-composition operator +> and this proposal’s binary functional-application operator |> are coupled only insofar their order of operations affects their syntax when they’re used together.

Whether 100 |> startEngine +> drive +> useCruiseControl would be equivalent to 100 |> startEngine |> drive |> useCruiseControl is equivalent to asking whether |> and +> have the same operator precedence. The answer is yes if |> has equal (or tighter) syntactic precedence than +>—i.e., if the first expression is equivalent to (100 |> startEngine) +> drive +> useCruiseControl. Otherwise, if |> has looser precedence than +>, then the first expression would be equivalent to 100 |> (startEngine +> drive +> useCruiseControl).

That question will depend on which of the latter two expressions is more generally useful as the default affordance for programmers. But that is purely a syntactic question for the proposal that will come later in time: +>’s proposal. Even though they might be synergistic, functional composition and functional application are orthogonal. |>’s proposal may independently advance of +>’s.

I personally would love to see both.

@TheNavigateur
Copy link
Contributor Author

TheNavigateur commented Aug 28, 2017

Of course +> should have higher operator precedence than |>, precisely to allow it to be used for a straightforward piping immediate execution statement.

Then Function |> Function is the same as Function +> Function and is hence a language redundancy. Removing it would be a net benefit - it would provide a clear readable and consistent separation of intention for the operands supplied. This is why I don't think the +> idea is detached from this proposal.

What I'm really saying, is that this proposal should be simplified to Parameter |> Function. All of the the rest belongs in +>. The benefit of having it all in 1 proposal is that there is less chance of conflict or redundant overlap. Otherwise I would be fine with 2 proposals, 1 for Parameter |> Function and one for +>

@dallonf
Copy link

dallonf commented Aug 28, 2017

I'm not seeing the difference, at a low level, between a proposal that only allows Input |> Function vs Input |> Function |> Function... if only the former is allowed, then the latter would still work, as it would be interpreted as (Input |> Function) |> Function.

@TheNavigateur
Copy link
Contributor Author

You got me. My mistake I had not thought about it properly

@TheNavigateur
Copy link
Contributor Author

OK I've updated the proposal to +> as a function composition operator: https://github.com/TheNavigateur/proposal-pipeline-operator-for-function-composition

Please provide your feedback. Cheers, N

TehShrike added a commit that referenced this issue Sep 6, 2017
We keep getting requests to include function composition in this proposal - hopefully this note will point people in the right direction.

See #50, #54, #57
@AlexGalays
Copy link

AlexGalays commented Aug 18, 2018

To that point, I strongly disagree with @mAAdhaTTah's suggestion that "the use case of calling inline is less common than defining and executing functions separately"; this has not been my experience with JavaScript at all.

Fair; that's likely specific to functional programming, which is what I expect the proposal is likely to be most popular/useful to.

Just wanted to chime in and say that I both love functional programming and never seen a programming style where functions are composed and prepared in advance even when they're only used in one place after working on dozens of apps. Seems like a huge maintenance burden to me, trying to reuse things at all cost.

On the contrary, using anonymous functions and pipelines is by far the more common case in JS land.

@pigoz
Copy link

pigoz commented Aug 18, 2018

Just wanted to chime in and say that I both love functional programming and never seen a programming style where functions are composed and prepared in advance even when they're only used in one place after working on dozens of apps.

It's called point-free style and very widespread in FP languages. For example, it's very popular in Haskell and other ML inspired languages. It can make the code more terse and allows you to document your intent, since you can name the function that's the result of composition, even in the local scope.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 24, 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