tc39 / proposal-extensions

Extensions proposal for ECMAScript
MIT License
150 stars 6 forks source link

Relation with The Pipeline Operator #2

Open Huxpro opened 3 years ago

Huxpro commented 3 years ago

As per the discussion from the Nov 2020 TC39 meeting: what would be the relation between this proposal and the pipeline operator proposal? Would they be mutually exclusive or would they coexist?

If they are going to be mutually exclusive, I'm concerned about the harm to the FP party of the JS community, considering:

  1. the heavy reliance on this of this proposal,
  2. also I'm unsure whether or not this can cover all use cases of pipeline operator gracefully.
hax commented 3 years ago

Personally I think they can coexist, and if we have both, I would prefer F# style pipeline which can be seen as a specialization sugar/optimization for FP party. Smart style is much like a (unsuccessful) compromise between FP and OO.

The problem may be, not every TC39 delegates agree that. šŸ¤·ā€ā™‚ļø

About your two questions:

the heavy reliance on this of this proposal,

One of the important design goal of extensions is, ensure extension methods/accessors have the same developer experiences as close as real methods/properties. Essentially it's not this proposal heavily rely on this, but all the built-in methods and ton's of our ecosystem heavily rely on classes and methods which use this.

I don't think we can change that, and even I love FP, I don't think we can throw all the built-ins and all assets which based on classes and this, at least, not on the language level. (Frameworks/libraries can be designed for specific coding style or paradigm, but not a language which already have OO style in its soul.)

So if there are problems of this, what we should do is developing our tools, educations, and new language features to help developers conquer them, at least help to mitigate the issues. We should not reject a proposal just because it rely on this, a proposal which can help to solve this issue have to rely on this!

also I'm unsure whether or not this can cover all use cases of pipeline operator gracefully.

I'd like to explore that in next step, personally I believe it could cover all use cases but there will be differences in some aspects (ergonomics, performances, optimization prospect of engines, etc.) compare to different pipeline operator proposals.

I will create a separate doc for this topic and update this issue.

Huxpro commented 3 years ago

@hax thanks for the elaboration.

One of the important design goal of extensions is, ensure extension methods/accessors have the same developer experiences as close as real methods/properties. ... I don't think we can change that...

Yeah, I totally see why extensions should be designed around this and I'm not here for changing it. My point is if extensions proposal is considered as a replacement of pipeline operator proposal, then this (no pun intended) might not be valued by FP party of JS community.

I'd like to explore that in next step

Great and let's see how that goes ;)

theScottyJam commented 3 years ago

I'm actually not so sure that these two proposals will co-exist very well.

What's the benefit of doing array::removeNullish() instead of removeNullish(array)? Well, I suspect it's the same reason why people sometimes prefer doing array |> removeNullish - it removes nesting and linearizes their code.

// Before
removeNullish(array.flat())
  .map(x => x + 1)

// After
array
  .flat()
  ::removeNullish()
  .map(x => x + 1)

The thing is, the pipeline operator gives us the capability of doing this already.

array
  |> ^.flat()
  |> removeNullish(^)
  |> ^.map(x => x + 1)

Sure, we had to refactor it a bit, but we could certainly have tooling help to automatically refactor fluent APIs into pipelines. Besides, it's more important that the code be easy-to-read than easy-to-write.

I also wouldn't be surprised if the pipeline operator will cause the number of fluent APIs out there to die down, as the pipeline operator removes a lot of the need for them. e.g. if the array stdlib were rewritten today, it could very well look like this instead

array
  |> Array.flat(^)
  |> removeNullish(^)
  |> Array.map(^, x => x + 1)

Fluent APIs and pipelines both solve the same problem, but in slightly different ways. I think it would be better if, as language designers, we specifically try to encourage just one style, not both. (Sure, people can use both styles if wanted, but the syntax should focus on just one)

ljharb commented 3 years ago

@theScottyJam because a lot of prototype methods already exist in the language, like Array.prototype.map, for example - and when someone wants to author code that doesn't break if someone runs delete Array.prototype.map, they'd need to const { map } = Array.prototype;, and then they could arr::map(), robustly.

theScottyJam commented 3 years ago

OK, I can understand that usage.

Though, if I understand the current iteration of this proposal, there's still a couple of bumps for that use case. I believe you have to do this? Because the extension methods go in a separate namespace?

const { map: ::map, filter: ::filter } = Array.prototype // <-- Three colons next to each other looks pretty weird :p

[1, 2]
  ::map(x => x + 1)
  ::filter(x => x > 0)

A syntax shorthand for function.call() could arguably do a better job then the current proposal. (Which might not be a bad idea - perhaps I might open another thread to discuss it more, if there's not any blatent flaws in it). Let's say Array.prototype.map.call([2, 3], x => x + 1) is the same as Array.prototype.map@([2, 3], x => x + 1), then we could do this instead:

const { map, filter } = Array.prototype

[1, 2]
  |> map@(^, x => x + 1)
  |> filter@(^, x => x > 0)

// or...

const $Array = { ...Array.prototype }

[1, 2]
  |> $Array.map@(^, x => x + 1)
  |> $Array.filter@(^, x => x > 0)

I guess ::map(x => x + 1) is a little more concise at the usage sight than |> map@(^, x => x + 1), but honestly, I would be ok using the latter version. I'm already used to having all of my functions be static and piped that way from my experience in functional languages. But, I could understand if others don't like the extra verbosity.

ljharb commented 3 years ago

the separate namespace is a nonstarter, so i wouldnā€™t think too hard about it.

theScottyJam commented 3 years ago

I just found @js-choi's bind proposal repo - from what I understand, there's plans to present that one soon. I do like that proposal repo a little better - it seems much simpler.

Umm... but it doesn't look like it provides a namespacing option either - perhaps that's a nonstarter either until that gets updated?

js-choi commented 3 years ago

I just found @js-choi's bind proposal repo - from what I understand, there's plans to present that one soon. I do like that proposal repo a little better - it seems much simpler.

Yes, Iā€™ve been talking about my resurrected-simple-bind-operator proposal and this extensions proposal with its creator, @hax.

I havenā€™t been able to talk with @hax about it recently, though. My hope is to collaborate and present a united proposal together, next meeting. But itā€™s still in flux.

Umm... but it doesn't look like it provides a namespacing option either - perhaps that's a nonstarter either until that gets updated?

If youā€™re referring to my resurrected-simple-bind-operator, it purposefully does not include a separate variable namespace. @hax is concerned about difficulty creating unique names for bound-function variables, but developers already solve this problem with variable-naming conventions. And the Committee is generally very against new namespaces (hence @ljharbā€™s referring to the separate namespace as a ā€œnonstarterā€).

theScottyJam commented 3 years ago

Ahh, the fact this proposal "has a separate namespace" is a nonstarter, not that the function.call syntax not having a separate namespace is a nonstarted - got it :)

I was almost about to open another thread asking for clarification as to why a namespace was so important šŸ˜„ļø