Skip to content

Should we do infix :: instead? #107

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

Closed
erights opened this issue Mar 15, 2018 · 22 comments
Closed

Should we do infix :: instead? #107

erights opened this issue Mar 15, 2018 · 22 comments

Comments

@erights
Copy link

erights commented Mar 15, 2018

I refer to the old infix :: bind proposal operator (TODO link needed), which satisfies some of the functionality people are looking for, from

  • pipeline operators (this proposal)
  • private class methods (TODO link needed to old proposal) (class-1.1)
  • so-called extension methods (TODO link needed)

Redoing the first example of this proposal: Given

function doubleSay() {
  return this + ", " + this;
}
function capitalize() {
  return this[0].toUpperCase() + this.substring(1);
}
function exclaim() {
  return this + '!';
}

...the following invocations are equivalent:

let result = exclaim.call(capitalize.call(doubleSay.call("hello")));
result //=> "Hello, hello!"

let result = "hello"::doubleSay()::capitalize()::exclaim();

result //=> "Hello, hello!"
@ljharb
Copy link
Member

ljharb commented Mar 15, 2018

This sounds like two suggestions in one:

  1. the sigil: what's the benefit of using :: over |> here?
  2. using this instead of the first argument: it seems that while being able to use this is important, the preponderance of community usage seems to be FP-style arg-taking functions. What's the value of diverging from that here?

@mAAdhaTTah
Copy link
Collaborator

I'm not sure what this suggestion is; it sounds like you're saying we should support the bind operator instead of pipeline.

@erights
Copy link
Author

erights commented Mar 16, 2018

the sigil: what's the benefit of using :: over |> here?

My point is not :: vs |>, but rather to revive the bind operator (which was known as infix ::) and do it instead of 1) pipeline, 2) hidden/private methods, and 3) so-called extension methods or extension interfaces. We can postpone arguing about what it looks like.

using this instead of the first argument: it seems that while being able to use this is important, the preponderance of community usage seems to be FP-style arg-taking functions. What's the value of diverging from that here?

Regarding the purposes addressed by the pipeline operator specifically, I won't argue. My point rather is that the bind operator satisfies most of that need as well as many others. IMO the bind operator pulls its weight.

I'm not sure what this suggestion is; it sounds like you're saying we should support the bind operator instead of pipeline.

Yes, that is what I'm saying. And also instead of private/hidden methods. Besides these, it also continues to be as good as it always was at its original motivations. When we find ourselves repeatedly inventing new things for special cases of the needs bind would have satisfied...

attn @allenwb @zenparsing @BrendanEich @littledan @Waldemar

@js-choi
Copy link
Collaborator

js-choi commented Mar 16, 2018

@erights: As background, there are now five competing pipeline proposals, some of which are in rapid flux, and each of which addresses a different set of use cases. I myself am the author of Proposal 4: Smart Pipelines. This new issue does not address any one of these three specifically; rather, the issue’s premise is that a function-binding operator would address the major functionality of all five of the pipeline proposals—unless I am misunderstanding its language.

Let me preface this by saying that I am a big fan of you and your work, Dr. Miller; it is an honor to communicate with you.

Having said that, I do not yet see how the claim above (“a function-binding operator would address the major functionality of all five of the pipeline proposals”) could be correct.

The primary functionality and purpose of the pipeline operators in all five proposals are to untangle deeply nested expressions, especially deeply nested unary function calls, into linear, unidirectional sequences of postfix steps. And a function-binding operator, such as the previously proposed infix ::, is orthogonal to that use case. Untangling deeply nested expressions into postfix-step sequences is not directly related to function binding, and it is not addressed by a function-binding operator. In particular, function calling and application are not equivalent to method calling. Function calling and application are important on their own.

It is a fait accompli that numerous JavaScript APIs, including the DOM, have functions that are designed to transform values as arguments—not as method callees. For instance, the WHATWG Fetch API uses a global fetch function that does not use its callee. Fetch instead uses its first function parameter as the “primary” value it uses, which may be the result a deeply nested expression.

There are numerous other functions similar to fetch in the DOM and hundreds of other popular APIs—particularly those APIs that group functions into namespaces by making them static methods.

Many APIs also have methods that do use their callee object, while still requiring arguments that may be deeply nested. One such API that readily comes to mind is the Selenium WebDriverJS library.

Bearing this fait accompli in mind, then examining the rewritten example given in this issue’s original post immediately shows the problem: The use of its solution requires the modification of these already-written functions that do not use this. But such a requirement, simply to use a new application syntax, is unreasonable—and many of them cannot be modified anyway.

The solution therefore cannot be used with WHATWG Fetch. It cannot be used with WebDriver methods. It cannot be used with most functions from Underscore, Lodash, Ramda, RxJS, and numerous other popular third-party libraries whose functions do not use this. It cannot even be used with console.log and console.error.

It can only be used with functions that happen to be written such that they use this as its possibly deeply nested “primary” pseudo-parameter, as in the example in the original post. Functions that do not meet this criterion must be rewritten to be used with the function-binding operator, or they cannot addressed by the function-binding operator at all. In contrast, the pipeline operator (from any of the five proposals) would address such frequent non-this-using functions.

(And for the smart-pipelines proposal specifically, a function-binding operator also does not address its other use cases, including terse function composition, terse function partial application, and terse method extraction. The smart-pipelines explainer, in fact, already discusses the intersection and orthogonality of smart pipelines with the :: proposal.)

A function-binding operator may well address use cases from private class methods (zenparsing/js-classes-1.1#38). The function-binding operator would certainly address extension methods.

But not only would a function-binding operator not address “some of the functionality” of the five competing pipeline proposals—it would not address any of their functionality. (To review, the major functionality here is the untangling of deeply nested expressions—in particular, unary-parameter function calls—which is orthogonal to matters of function binding.)

Again, I am a huge fan of your work; it is an honor.

@charmander
Copy link

But not only would a function-binding operator not address “some of the functionality” of the five competing pipeline proposals—it would not address any of their functionality.

What it does address, it addresses with a much simpler syntax than whatever will come out of the pipeline proposals. If it were usable with arrow functions, I think :: would be best – but it isn’t, and this can’t be treated as just a parameter for that reason. (Which is too bad.)

@erights
Copy link
Author

erights commented Mar 16, 2018

Hi @js-choi , first, thanks for the kind words! Always appreciated.

I confess that I am responding here without having read all the proposals deeply. Please continue to object, correct, and question if my responses seem to miss the points of some or all of the pipeline proposals, thanks.

Let's start with the stated required goals:

  • Requirement: Easy composition of functions for immediate invocation

Given that the composition is via this, yes. I agree that this does not address composition of existing functions where we want to compose on non-this parameter positions. Since the emphasis here is on using the first parameter position instead, perhaps an adapter function such as

function adapt(f) {
  return function(...args) {
    return f(this, ...args);
  };
}

would be useful. Returning to the original example, without rewriting

function doubleSay (str) {
  return str + ", " + str;
}
function capitalize (str) {
  return str[0].toUpperCase() + str.substring(1);
}
function exclaim (str) {
  return str + '!';
}

we could use an adapter such as adapt together with :: to do:

let result = "hello"::adapt(doubleSay)()::adapt(capitalize)()::adapt(exclaim)();

If indeed most things need to be adapted, as in the above example, the terseness is lost. But the main benefit I see in the pipeline proposals is not the terseness, but the left-to-right composition.

@charmander adapt also accommodates arrow functions for further rearranging of parameters. Though again, with loss of terseness.

  • Requirement: Usable with any function arity

yes

  • Requirement: Able to await in the middle of a pipeline

yes

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Mar 16, 2018

Since the emphasis here is on using the first parameter position instead

This isn't really true, depending upon the proposal. The Smart Pipeline's placeholders can put in any argument position. F#-style probably would, but I don't really a large difference between requiring an adapt wrapper function for every non-this method and requiring arrow functions in the pipeline to do a similar adaption.

Given that the bind operator isn't in the language proper, my feeling is the ecosystem of functions you're likely to rely on to do this sort of composition isn't going to operate on this, so you're more likely to need to adapt every function you deal with than wrapping everything in arrow functions. All else being equal, this is a major advantage pipeline has over the bind operator.

I also believe there were other issues @zenparsing pointed out on this repo; I'll see if I can find them.

Update: Found his comment here: #101 (comment)

@js-choi
Copy link
Collaborator

js-choi commented Mar 17, 2018

[@charmander] What [a binding operator] does address, it addresses with a much simpler syntax than whatever will come out of the pipeline proposals. If it were usable with arrow functions, I think :: would be best – but it isn’t, and this can’t be treated as just a parameter for that reason. (Which is too bad.)

I’m not sure which of the five competing pipeline proposals you are referring to, but I think all of them have fairly simple syntax, though each with various tradeoffs. The Core Proposal syntax grammar of Proposal 4: Smart Pipelines can literally be written on a single notecard; the syntax grammar of the original proposal is similarly tiny. In the end, they’re all infix operators, just like how the binding operator is a binding operator.

I’m interested in reasons that you feel any or all of the five proposals may be overcomplex despite their tiny grammars—but, in any case, those reasons are probably not relevant to this particular issue. Feel free to open a new issue in this repository; be sure to specify which of the five proposals you are discussing.

In any case, the use cases that a binding operator might address (such as private class methods and extension methods) is not relevant to the uses cases that a pipeline operator would address: unidirectional postfix application, as well as (for smart pipelines) function composition, partial application, and method extraction.


[@erights] I confess that I am responding here without having read all the proposals deeply. Please continue to object, correct, and question if my responses seem to miss the points of some or all of the pipeline proposals, thanks.

If you need a brief overview of the two frontrunner proposals by @mAAdhaTTah and me, take a look at the unfinished presentation for TC39 London next week that we’re working on. I’ve also written a detailed explainer document for smart pipelines that goes over their use cases, with many examples from real-world code.

[@erights] adapt also accommodates arrow functions for further rearranging of parameters. Though again, with loss of terseness.

  • Requirement: Usable with any function arity
    yes
  • Requirement: Able to await in the middle of a pipeline
    yes

I will address the rest of this comment below, but I am wondering how adapt would accomodate either of these requirements. Would adapt have to take a numerical parameter that denotes which of parameters it “lifts” to this? And would await work by wrapping values in promises that are passed between each adapted function?


[@erights] Since the emphasis here is on using the first parameter position instead

[@mAAdhaTTah] This isn't really true, depending upon the proposal. The Smart Pipeline's placeholders can put in any argument position. F#-style probably would, but I don't really a large difference between requiring an adapt wrapper function for every non-this method and requiring arrow functions in the pipeline to do a similar adaption.

Given that the bind operator isn't in the language proper, my feeling is the ecosystem of functions you're likely to rely on to do this sort of composition isn't going to operate on this, so you're more likely to need to adapt every function you deal with than wrapping everything in arrow functions. All else being equal, this is a major advantage pipeline has over the bind operator.

I also believe there were other issues @zenparsing pointed out on this repo; I'll see if I can find them.

Update: Found his comment here: #101 (comment)

[@zenparsing]: There has historically been significant resistance to [proliferating] this usage outside of class and object literal methods.

@mAAdhaTTah is correct here. Although the original proposal and Proposal 1 (F-sharp Style Only with Bare Await) do focus on unary calls, Proposals 2 (Hack Style Only), 3 (Split Mix), and 4 (Smart Pipelines) all can easily deal with piping into arbitrary parameters—and, indeed, arbitrary expressions. This is something that binding alone simply cannot naturally address.

That a third-party adapt function is required for the primary use case under discussion is not desirable. The adapt function must be imported from a third-party library or implemented by the developer themselves, complicating the codebase before even its first use. And the adapt function is presumably megamorphic, which makes compiler optimization significantly more difficult. And the repetition of adapt obscures the original meaning of the code with clutter and ritual, compromising its original goal of improving comprehensibility by the human reader.

There is a fundamental impedance mismatch between the model of this binding and the model of pipelining—in which the model of pipelining is simply the model of function application (for the original proposal and Proposals 1, 3, and 4) and also the model of expression application (for Proposals 2, 3, and 4). this binding simply does not address function application or expression application; to attempt to do so greatly complicates readability for the developer and is not an appropriate use of its unique semantics. Binding and function/expression application are simply orthogonal concerns.


As an aside: Expression application (as with Proposals 2, 3, and 4) can address method extraction—for instance, with smart pipelines + Additional Feature PF: promise.then(+> console.log), which is the same as promise.then((...$) => console.log(...$)) and promise.then(console.log.bind(console)).

But even expression application does not address method binding, in which a value is applied to a function as its this binding. To address terse method binding requires another operator—such as the original infix ::.

For more discussion about the relationship between pipelines and the bind operator, see @jakearchibald’s issue #101, as @mAAdhaTTah already linked above.

Eventually, pipelining may support more abstract forms of composition. @tabatkins, @isiahmeadows, and I have been discussing such possibilities (see #106). This would be quite difficult to achieve if this binding was shoehorned into addressing pipelining with a userland adapt function.

Thanks again for the disucssion, Dr. Miller. Let me know if you find the time to read the smart-pipelines explainer or spec. I’m looking forward to seeing it and @mAAdhaTTah’s alternative discussed next week at TC39.

@cshaa
Copy link

cshaa commented Mar 19, 2018

I wanted to share my opinion on this topic, however my post grew in size until I decided it's best to post it as a separate issue. The main point was that the features of Smart Pipelines are almost a superset of those of the Binding Operator despite they don't suffer the same problems that kept the Binding Operator stalled for ages.
In #110 I propose introducing a :> operator to Smart Pipelines that would give pipelines the missing features of :: and the two proposals could be merged into one consistent syntax. (Which is funnily quite the opposite of what @erights proposes.)

@littledan
Copy link
Member

When we discussed pipeline in the March 2018 TC39 meeting, it seemed like the committee had a number of advocates for :: as well. Maybe we should consider :: to be a third/fourth alternative proposal to continue comparing within this effort.

@jakearchibald
Copy link

@littledan It feels like the use-cases for :: are being crowbarred into |> and vice-versa. Aren't they solving different problems?

@littledan
Copy link
Member

@jakearchibald I don't think the problems they are solving are all that different. Some libraries, like RxJS, may be able to adapt to any one of the two. As we were discussing in the March 2018 TC39 meeting, we only have so much budget to add punctuation while remaining intelligible. I'd rather figure out which pattern is more important, add that, and maintain skepticism for adding both, even as there is no hard blocking concern.

@surma
Copy link
Member

surma commented Apr 4, 2018

My 2¢: I think :: is closer to “traditional” JavaScript (merely some sugar over someFunction.call()) and as such is easer to explain to developers imo. :: can be used to implement a pipelining syntax as well (making RxJS happy, I presume?), without having to create closures every time you want to parameterize.

The original :: proposal had some additional features like ::console.log being equivalent to console.log.bind(console) and a::b being equivalent to b.bind(a) — I think these closure-generating use-cases are secondary to the main problem we are trying to solve. If we re-evaluate ::, we should check if there’s less resistance to :: without those closure-generating use-cases.

I’m don’t have strong opinions on the actual shape of the operator.

@mAAdhaTTah
Copy link
Collaborator

@surma I'm not sure I understand this bit:

without having to create closures every time you want to parameterize.

Can you elaborate?

@surma
Copy link
Member

surma commented Apr 4, 2018

Yeah, my bad! I was making a sweeping statement without thinking about the “smart pipeline operator”.

Example:

myObject |> add(4);

The implementation of add would have to be a function that returns a one-off closure:

function add(val) {
  return obj => obj + val; 
}

The “smart” version of the pipeline operator solves this:

function add(obj, val) {
  return obj + val;
}

myObject |> add(?, 4);

but I think that’s quite magic and much farther away from what JS developers are used to, compared to how :: would solve this.

@mAAdhaTTah
Copy link
Collaborator

Yeah, F# Pipelines would require a new closure. Although I don't personally consider that drawback, I understand that's not universal.


It's almost too bad we have a limited syntax budget, because I do think there's a combination of bind ::, partial application a.f(?), and pipeline |> that could work quite well together, although I don't think there's an appetite for introducing all of this new syntax at once.

@andrewbanchich
Copy link

Would some sort of hybrid operator like :=> be open to discussion?

@mAAdhaTTah
Copy link
Collaborator

@andrewbanchich Everything is open to discussion. If you have a more fleshed out idea that combines pipeline & bind, I might suggest opening a separate issue with the details.

I was just pondering how something like that would look / work, but I'm concerned it would quickly stretch beyond the complexity of even the current Smart Pipeline.

@andrewbanchich
Copy link

I was thinking since JavaScript has double and triple equals for equality comparison, we could have a similar interpretation of the pipeline operator like :=> and :==> that changes its functionality in terms of binding/closures.

@cshaa
Copy link

cshaa commented Apr 5, 2018

@andrewbanchich That doesn't sound like a good idea – people would argue whose operator is bigger 🤔

@ljharb
Copy link
Member

ljharb commented Apr 5, 2018

@m93a please keep comments appropriate and inline with our code of conduct.

@tabatkins
Copy link
Collaborator

Closing this issue, as the proposal has advanced to stage 2 with Hack-style syntax.

@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