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

Replacing :: completely with pipelines #110

Closed
cshaa opened this issue Mar 19, 2018 · 20 comments
Closed

Replacing :: completely with pipelines #110

cshaa opened this issue Mar 19, 2018 · 20 comments

Comments

@cshaa
Copy link

cshaa commented Mar 19, 2018

I'm writing this issue in reaction to #107 and tc39/proposal-bind-operator#44 where there is a vivid discussion about the future of the :: operator and its relation to the proposed pipeline operators.

Let me start with saying that the :: operator proposal has been around for quite a long time and there have been some major advancements in the Pipeline Operator Proposal since then. For example the Smart Pipelines variant with Additional Feature PF is much cleaner and more universal than the unary binding variant of the :: operator can ever hope to get. I think that the ease with which +> console.log replaces ::console.log while offering much more functionality implies that the design of :: needs heavy rethinking.

Now let me dissect the three variants of ::, one at a time:

  • Unary Bind operator ::obj.fn

    • Meaning: obj.fn.bind(obj)

    • Use case: addEventListener('click', ::console.log)

    • Problem: People don't like the syntax – it's completely different from :: operators in other languages and if feels a bit weird that ::a.b.c means the same as a.b::a.b.c.

    • Problem: It produces a new function every time. This wouldn't work in the original proposal: removeEventListener('click', ::console.log) However the problem can be fixed quite simply using WeakMap internally to return the same bound function every time.

    • Status: The more universal Pipeline Functions proposal makes it quite redundant. Furthermore it solves the syntax problem – +> isn't clear at first sight, but after looking it up, the developers should recognize it as a “crossbreed of the pipeline operator |> and an arrow function =>”. The problem with returning a new function every time is however more complicated to solve with PF than with ::. We could therefore leave (+> foo.bar) === (+> foo.bar) as undefined behavior until we find a systematic way to enforce equality.

.

  • Context Pipeline operator context::fn(...args)

    • Meaning: fn.apply(context, args)

    • Use case: arr::unique()::sort()::join(', ')

    • Problem: The only major problem with this feature is that, despite it's quite uncontroversial and requested often by various people, it's being delayed by the rest of the proposal. See Drop the this binding altogether proposal-bind-operator#44 for more discussion.

    • Status: Right now, this feature isn't covered anywhere in the pipeline proposal, yet it's quite similar to the pipelines. There have been opinions that, because of the similarity with pipelines and the controversy of the other two variants of ::, we should change it to something more consistent with the pipeline operator. User @fatcerberus proposed -> instead of it, I would peronally go with .> or :>.

    • Differences from |>: There is some overlap between the Context Pipeline and the ordinary Pipeline Operator in terms of the extension method functionality. If you wanted to use |>, you could pretty much use the first parameter instead of this and rewrite the use case of this topic to arr |> unique |> sort |> join(#, ', '). You can see that the syntax looks lighter than with ::. However it doesn't feel exactly JavaScript-y, we're all used to this being in the spotlight, especially with native types like string or array. And the # in join(#, ', ') is just weird.

    • My proposal: I don't know if these arguments are enough to add the Context Pipeline to JavaScript despite the ordinary Pipeline can do much of its tricks. However I'd add the operator to the Smart Pipeline Proposal and let the community (and TC39) express their opinion. This would specifically mean removing Context Pipeline from the Bind Operator Proposal and replacing it with Additional Feature CP (i. e. Context Pipelines) for Smart Pipelines. This additional feature would introduce a new infix operator, for example :>, where these rules would apply:

      • expression_a :> expression_b translates to (expression_b).call(expression_a)
      • expression_a :> expression_b( ...argument_list ) translates to (expression_b).apply(expression_a, argument_list)
    • If I rewrote the use case again using this new syntax, it would be: arr :> unique :> sort :> join(', '), which is even cleaner than with the ordinary pipeline.

.

  • Binary Bind operator context::fn:

    • Meaning: fn.bind(context)

    • Use case: Eh, binding functions to context I guess?

    • Status: If Additional Feature PF and NP were accepted together with my proposed CP, Binary Bind operator could be replaced by +>( context :> fn(...) ). And with BP it could be written as +>{ context :> fn(...) } which is maybe even cleaner.

.

TL;DR

To sum up, we saw that the Smart Pipelines together with a light-weight pipeline-friendly Additional Feature CP would easily replace the whole Bind Operator Proposal. However there are some open questions, such as the implementation of removeEventListener('click', +> console.log) or even whether CP is worth introducing a new operator.

@js-choi
Copy link
Collaborator

js-choi commented Mar 19, 2018

Right now, [extension methods are not] covered anywhere in the pipeline proposal, yet [they are] quite similar to the pipelines.

Thanks for looking at the smart-pipelines proposal. For what it’s worth, the smart-pipelines proposal already discusses all three forms of the current proposal for the :: operator. I’d just like to make sure that you’ve read that section, since it does address extension methods, though maybe not by that particular term.

Although Feature PF does subsume, my current opinion is that function application and binding are orthogonal (as I say in #107). Being able to distinguish v |> join(#, ', ') from v |> #.join(', ') and v |> #::join(', ') is a desirable feature, since all three are reasonable interpretations of v |> join(', '). Because of that semantic ambiguity, v |> join(', ') is intentionally an early syntax error.

I’ll examine the rest of this more closely when I can—right now I’m hurrying to finish the explainer and specification as much as I can before the London TC39 meeting tomorrow. Thanks again; I’ll be thinking about this for the next days. Let me know if you read that existing section on ::.

@cshaa
Copy link
Author

cshaa commented Mar 19, 2018

Oh, I've seen this part but I stopped at “But the other two use cases (infix ::) are not addressed by smart pipelines.” since I thought Ah, I already know +> console.log 😀

I'm glad that you've thought about the possible coexistence of :: and smart pipelines. My point here is mostly that if we moved the pipelining part of :: to the smart pipes spec, it would speed up its now-stagnant progress and ensure it will be consistent with the rest of smart pipes. And also that the pipelining part of :: is the only relevant one, the rest can be pretty much dropped once smart pipelines arrive.

Nevertheless I wish you good luck at the TC39 meeting! I'm pretty sure that smart pipelines are the future of JavaScript and you will kick a*! 💪

@js-choi
Copy link
Collaborator

js-choi commented Mar 19, 2018

Thanks for the kind words. As the existing :: section says, if smart pipelines receive interest at TC39, then that would open up the possibility of simplifying ::, letting it focus purely on binding. I have planned to reach out to ::’s authors, as well as the authors of other proposals, to discuss this sort of intersectionality, but I haven’t yet had time.

In any case, I won’t be presenting to TC39 myself; @littledan will be presenting, though @mAAdhaTTah and I together wrote the presentation slides. We’ll see what happens. Thanks again.

@ljharb
Copy link
Member

ljharb commented Mar 19, 2018

There are benefits of the unary form (for “method extraction”) that are impossible to achieve with pipelines; namely, caching bound methods off of mutable objects (like builtin prototypes) for later use, while being robust against other code deleting or replacing those methods.

I will not be comfortable with the pipeline proposal advancing (and thus potentially killing the :: operator) unless this use case is met by something (in pipelines, or in a separate proposal) that the committee still considers palatable.

@js-choi
Copy link
Collaborator

js-choi commented Mar 19, 2018

@ljharb: Thanks for the heads up. I would be happy to investigate your use case. I know that TC39 is imminently meeting again, but, when you have time, could you write an example that concretely demonstrates the need to robustly cache bound methods from mutable objects? It’d make it easier for me to explore and/or specify, but I cannot yet find one in the current bind-operator proposal repository, in both its explainer and its issues. Thanks.

@ljharb
Copy link
Member

ljharb commented Mar 19, 2018

Every place in my packages that i do Function.bind.call(Function.call, Array.prototype.map) or similar. It’s not a common technique because it’s so verbose, but making code robust against later prototype modification is very important. When pipeline was initially presented (and potentially dropping the bind operator came up) i (hope i) made it very clear that while i loved pipeline, i wouldn’t provide consensus for it to advance unless some form for method extraction remained tenable.

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Mar 19, 2018

I will not be comfortable with the pipeline proposal advancing (and thus potentially killing the :: operator) unless this use case is met by something (in pipelines, or in a separate proposal) that the committee still considers palatable.

This concerns me a bit, because I don't think the F#-style has a good answer for this use case. The whole raison d'être of that proposal is to be minimal and lean on arrow functions to fill a lot of the gaps that are filled in Smart Pipeline by the lexical topic concept.

That said, F#-style isn't really intended to kill ::, as the binding use cases are not served by an F#-style pipeline, so maybe it's not an issue. I'm generally of the opinion that solving smaller problem spaces separately is a better approach, hence preferring F#-style.

Does a method extraction syntax, bind op or otherwise, have to advance in tandem or is leaving the possibility open enough to overcome your objections?

@ljharb
Copy link
Member

ljharb commented Mar 19, 2018

@mAAdhaTTah note that i don’t think pipelines have to provide for method extraction, i just want to make sure that something - including another proposal - can solve for it. It would be sufficient if some proposal exists, is intended to be advanced, and the committee response is positive.

@js-choi
Copy link
Collaborator

js-choi commented Mar 19, 2018

@ljharb: That’s also good to know. Robust method extraction also might be orthogonal to Proposal 4 (Smart Pipelines) with Feature PF, similarly to how it is orthogonal with @mAAdhaTTah’s Proposal 1 (F-sharp Style Only with await). I’ll investigate your packages for cases of Function.bind.call, and I’ll see if pipeline function can accommodate them.

If not, then I would be happy to help out with a proposal for robust method extraction, whether it is with the original binding operator or with another operator. I’ll also search the previous TC39 meetings’ notes for your discussion of robust method extraction. It must have been before last September, or else I would have already seen it…

@mAAdhaTTah: It’s short notice, but we might want to update the presentation in anticipation of this discussion about our proposals’ relationships with method extraction and to address @ljharb’s concerns. I don’t yet know how though. We might have to quickly look at @ljharb’s packages and the prior TC39 meeting notes, later today, then figure out where to go from there…

@mAAdhaTTah
Copy link
Collaborator

@js-choi Let's not, imo; some of the slides are already pretty jam-packed, with a lot for the committee to digest. Both proposals leave open the possibility of method extraction; let's see what direction we get (if any) from the committee at this meeting and proceed from there.

@ljharb
Copy link
Member

ljharb commented Mar 19, 2018

I agree that this presentation doesn’t need to address it; pipeline isn’t seeking advancement this go-round.

@js-choi
Copy link
Collaborator

js-choi commented Mar 19, 2018

@mAAdhaTTah, @ljharb : Understood.

I had forgotten that a discussion about robust method pre-extraction happened last September. By the time they returned to it in November, time was up. January was skipped while preparing for this, though @ljharb gave a comment about the importance of the prefix ::, as well as recently that it must be robust against delete Function.prototype.bind. ljharb/function.prototype.name gives examples of this pattern. There has been discussion of method-binding caching in tc39/proposal-bind-operator#46, in which a WeakMap is associated with each realm.

In any case, pipelines, including smart pipelines, and robust cached method pre-extraction can coexist. Using ljharb/function.prototype.name/implementation.js, line 19 as an example of useful coexistence:

return fn
|> &Function.prototype.toString
|> &String.prototype.match(#, classRegex)
|> !!#;

…which is just:

return !!&String.prototype.match(
  &Function.prototype.toString(fn),
  classRegex);

This security robustness is quite a different use case than simply wanting to express the callbacks in promise.then($ => console.log($)) more tersely, as well as the other goals of smart pipelines as well as the goals of other pipelines: most importantly, the untangled composition of expressions and calls.

I think secure cached method binding is out of scope for smart pipelines, even with Feature PF. It is also out of scope for @mAAdhaTTah’s Proposal-1 pipelines. These are orthogonal use cases. They cannot kill each other.

I will have to look into adding examples that use both pipelines and secure method extraction to the smart-pipelines explainer…

@js-choi
Copy link
Collaborator

js-choi commented Mar 20, 2018

@ljharb: I’ve pushed a commit to the smart-pipelines explainer that clarifies how it does not address or preclude an operator for robust method extraction. You can see some hypothetical examples in its section about the :: proposal. When you can, let me know if this ameliorates your concerns for now. Thanks. (You can also track tc39/proposal-smart-pipelines#10.)

@ljharb
Copy link
Member

ljharb commented Mar 20, 2018

It does somewhat :-) I’ll also be interested to hear the committee’s opinion on a proposal for method extraction.

@babakness
Copy link

I find that the order of operations useful. For example, lets say you have this sample library

https://gist.github.com/babakness/ba3c2ac03b030cecceca8a64877a79f4

export class Placeholder {}
export const _ = new Placeholder()
export const isPlaceholder = placeholder => placeholder instanceof Placeholder

export function bind( ...placeholders ) {
  return ( ...fillers ) => this.call( this, ...placeholders.map(
    item => isPlaceholder( item ) ? fillers.shift() : item
  ) )
}

Then you can do

import { _ , bind } from 'cool-stuff'

const getPointOnLine = (slope,offset,x) => slope * x + offset

const pointOnLine = document.querySelector('#whatever').value
  |> Number
  |> getPointOnLine::bind( .5, 10,  _ )

The fact that :: runs before |> is useful. One could have different version of bind that works like pipeline as well

import { _ , bind } from 'different-cool-stuff'

const getPointOnLine = (slope,offset,x) => slope * x + offset

const pointOnLine = document.querySelector('#whatever').value
  |> Number
  |> ( getPointOnLine |> bind( .5, 10,  _ ) )

But it is less intuitive.

@szhu
Copy link

szhu commented Jan 28, 2019

One advantage I see with the bind operator is that there is no illusion of what is actually being done -- you're calling the RHS function with the LHS set as this (or the first arg, depending on proposal). The pipelines syntax seems to reflect how the operator is meant to be used rather than implemented, so someone who learns about pipelines could get confused about its semantics even if they're familiar with the rest of JavaScript.

It appears that progress with this proposal appears to be moving faster than bind operator proposal -- can someone explain why the community favors pipelines? (I know it's likely that this discussion has already happened, but I can't find it, so thanks in advance.)

@TehShrike
Copy link
Collaborator

The bind proposal and pipeline proposal are generally desired by two different sub-communities - the pipeline operator is desired primarily by functional-programming advocates, and the bind operator is desired primarily by class-based-programming advocates.

I'm not sure what role that played in this proposal attracting so much attention.

@mAAdhaTTah
Copy link
Collaborator

I'll also add that the bind operator had momentum before it stalled out. The initial idea of the bind operator covered a lot of different usages, and it stalled out on speccing some of those behaviors.

@mmis1000
Copy link

mmis1000 commented Jan 30, 2019

function makeExtension(fn) {
  return function argumentReciever() {
    const arg = arguments
    return function thisReciever(self) {
      return fn.apply(self, arg)
    }
  }
}

var say =  makeExtension(function (word) { 
  console.log (this.name + ': ' + word)
  return this
})

;({name: "jack"}) |> say('Hello World')

I though, some of the usages may be simply described with a helper function to achieve the style of extension methods.

And this style would be nice to experiment new stage 1/2 methods as it can be used safely without pollute the prototype chain.

It could benefit future proposals by allow you to test brand new methods in production to see if it is really helpful or not without worry about it would breaks everything someday, or simulate virtual methods on native object.

However, the resolve order of pipeline operator and bind operator is reversed.
Means you can't write

;({name: "jack"}) 
   |> say('Hello World')
   .toString()

because it world be parsed as

;({name: "jack"}) 
   |> (say('Hello World')
   .toString())

@tabatkins
Copy link
Collaborator

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

This means it replaces some of the use-cases for :: (namely, creating method-like functions for fluent interfaces), but there are still remaining use-cases that it doesn't address and doesn't intend to.

@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