-
Notifications
You must be signed in to change notification settings - Fork 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
Alternative pipeline strategies and operators #55
Comments
Can you explain how option 3 is substantially harder to optimize? Or are you commenting on the performance of curried functions in general? |
For prior art I’d also like to mention R’s
I’m not proposing to adapt this. I think it’s too complicated, and especially confusing with higher-order functions: const adder = rhs => lhs => lhs + rhs
const plus1 = adder(1)
// work as expected
2@plus1 //→ plus1(2)
3@plus1() //→ plus1(3)
// the first one is a bug bound to happen
4@adder(2) //→ adder(2, 4)
4@(adder(2)) //→ adder(2)(4) Other people might think that the pragmatic usefulness outweighs its complexity, though. |
Regarding syntax:
*The key in the bottom left is used to type <>| on german keyboards. In R, |
Yes and no. For the operator itself, closure elimination is a necessity, but in order to do that, you have to first inline the function to eliminate the closure, and then come back to inline the closure itself. JS engines do optimize IIFEs, but that level of inlining is usually a bit deeper than what they tend to look for (and this is why heavily functional JS tends to be a bit slow compared to the procedural equivalent). It's not out of the realm of what they can do (consider native Promise performance and recent V8 optimizations for For curried functions, it's something that's exceptionally difficult to do performantly at the language level (as opposed to natively). I'll save the gory details for another time, though. |
Not really, considering Clojure's
I'd agree, but it doesn't look very JS-y. (Most languages with that operator also coincidentally use
Consider yourself lucky. 😄 QWERTY keyboards have ?// there, but | is located between the Backspace and Enter keys, which is hugely awkward IMHO. |
If you mean an operator call with a function like this: var y = 10
var result = get() |> x => x + y then yes, closure elimination will yield better performance. But as I understand it, the transformation is so straightforward that I wouldn't think to call it substantially harder for engines to optimize. |
@gilbert Don't forget this common case (as well as less contrived ones): var add = x => y => x + y
var result = get() |> add(10) Also, consider curried functions - those are when things get funky. (Ramda and lodash-fp both come to mind here, where you'll need a lot more than simple closure elimination, and the use of curried functions is quite idiomatic in those circles.) |
The performance of that example is no different than what would happen today: var add = x => y => x + y
var result = add(10)( get() ) This is orthogonal to the pipeline operator. Whether you use the operator or not, you're still working with a closure created by |
For the surface syntax, I like sticking with For the semantics, I wrote about my thoughts in a slide deck that I intend to present to TC39 soon. Personally, I like the track that this repository is on. This three-way semantic question is really important, and I hope to get committee feedback on it when the proposal is presented. |
There are subtleties in the third case that I think haven't been addressed: If Or I am concerned about these differences because of the impact on object methods, or |
In relation to the operator: is it out of the question to simply use I mean:
is currently parsed by nodejs at least, but is seemingly all ignored and the output is still 10. |
|
Personally I prefer the |
@littledan yes, but |
@MeirionHughes Well, it's defined--it will cast the function to a number in a particular way, getting |
okay yeah, it is defined what will happen to lhs and rhs; but from the looks of it its just coercing both sides to an int32: https://www.ecma-international.org/ecma-262/8.0/index.html#sec-signed-right-shift-operator I don't think that explicitly rules out the possibility of checking if the rhs is a function, and if so then pipe lhs into rhs - unless, of course, there is a use-case for wanting to coerce ToUint32( |
@MeirionHughes Everything here is defined. It does go through NaN as you say, deterministically. |
sorry @littledan, I should have delved deeper. You're right, its probably a bad idea because you could technically do this: let func = function () {}
func[Symbol.toPrimitive] = function(){return 2};
console.log(16 >> func);
oh well :( |
As a developer who is invested in the future of pipelining in JavaScript (I, and teams I've worked on, have heavily used the compelling Simply that, from a basic POV, I love JavaScript and I'm very passionate about the language. I would hate for anything to be introduced that would cause damage or grief. I'd also hate to go back to the dark ages when I had to write functions "backwards" or compose them with If you took a moment to read this, thank you. I hope I've contributed something positive to this discussion. I'm very happy to see it continue to evolve, and I'll be excited for whichever pipeline strategy becomes part of the language. |
@aikeru Thanks for your contribution. There's two cases in your desugaring, so I want to ask for details about the two parts separately. For the For the |
One use case I find particularly compelling is borrowing built-in prototype methods - eg, |
An additional use case is caching builtin methods for robust scripts; eg: const { map, filter } = Array.prototype; // assuming everything is untouched at execution time
export default (x) => {
x.map(…).filter(…); // breaks if `x` is not an array or if `delete Array.prototype.map`
x::map(…)::filter(…); // 100% robust against later modification of Array.prototype
}; |
@littledan Thanks for your response, and your question. I'll try to thoughtfully answer it from my personal experience.
I hope I understand -- if you mean that instead of being passed as the // before (current Babel)
foo::bar("baz") ---> bar.call(foo, "baz")
// after
foo::bar.call("baz") ---> bar.call(foo, "baz") I suspect this would make FP style a bit more natural/convenient, which is great.
I personally do think that the current implementation works more naturally with the new classes, etc. However, function parameters would work fine as well.
There are some really great things in that proposal, and I had not read it before. One thing that is really nice about the current foo |> bar() ---> bar(foo)
foo |> bar("baz") ---> bar(foo, "baz") It's been a long day and though I skimmed over the examples I did not see one that illustrates what happens if you do not pass a placeholder, and instead only pass one argument (I apologize if I missed it) after a pipe operator. If this proposal means I would be forced to pass a placeholder in order to get the above, that would be a bummer, but if it becomes baked into the standard, it would still be a great way to chain / pipeline functions together. // What happens?
const foo = "hello"
function bar(a,b) { console.log(a,b) }
foo |> bar("baz") Thanks again for your time and thoughtful consideration. |
@littledan Anyway, I've said a lot, and I'm sure there are people thinking about this very hard and I appreciate their efforts! Thanks for letting me participate. |
Just thought I'd drop in and say that I found this ensuing discussion very interesting to read and follow. 😄 (Mission accomplished! :-)) |
I'd just like to also point out that it would be useful if a thought could be given to how the pipeline operator works with Iterables and Generators; presumably, it should just-work™: function* where(source, predicate){
for(let item of source){
if(predicate(item){
yield item;
}
}
}
[1,2,3,4] |> _ => where(_, x => x > 2);
[1,2,3,4] |> where(x => x > 2) ; // needs partial application proposal? |
This is another problem I find with the
If I were to use either of the other operators they don't need the space, are easier to type
|
|
@aikeru "adding as the first argument" is a little vague; it can correspond to pipeline strategy 2 or 3 above, as defined by @isiahmeadows . Your examples are strategy 2, but this proposal is currently going for 3. Now is the time to think through these tradeoffs! In option 3, the function is called with the left operand as the sole argument, not added to the arguments list. You can combine with the partial application proposal to get the same effect: x |> f(?, y);
// is similar to
f(x, y); The advantage of this version is that the first argument isn't as privileged--partial application can put the argument anywhere. What do you mean by a "placeholder"? |
@MeirionHughes I believe all that should just work, modulo the decision about option 2 vs option 3. If we go with option 3, and have partial application, the code example would look like [1, 2, 3, 4] |> where(?, x => x > 2) |
@littledan please read "placeholder" to refer to the
I suppose under option #3 with a
Please correct me if I'm wrong! I'd like to freely share my opinion on option 3, which you mention is the current path of the proposal. It feels like option 3 is awkward in JavaScript and the If I'm trying to explain the current bind operator implementation to a newbie, I start with teaching I hope I haven't offended anyone, and whichever proposal or option makes it, I'll be cheering for it and helping my fellow developers master it. PS I wrote this from my cell so my apologies for any mistakes. I'll review it again and edit it when I can. |
@aikeru I agree that 3 only makes sense with the partial application proposal based on |
might be a hard sell on 1: 2: Unless backward pipeline support #3 is desired, I'd personally vote for
|
@MeirionHughes
I could easily refactor existing code that uses |
@aikeru aye, I have a sneaky suspicion that the babel binding-operator transform is being used in the wild quite a lot. Jeeewiz.... 1.5 million downloads last month. I would think that that download count is a testament to the fact that chaining is a problem people need fixing... So its useful to consider your refactor is a simple fix. |
Refactoring to add |
@aikeru no, because in your example, |
Another point to consider: private fields are already in stage 3. Ironically, this seems to make option 1 less useful. Take the following example: obj::foo() Here |
@gilbert neither would |
In other languages (C#), extension methods don't have access to private fields. |
With respect to the bind operator, My new current struggle is that while the I'm still very hopeful that some variant of this will find its way into stage-1-and-beyond status. I know we wont be the only ones celebrating this event, when it finally happens. I'm certain it will bring joy to many JS developers. It wont be the operator everybody wants, but it'll empower everyone to write more elegant code. Thanks. |
I mentioned about Option 2 in #59: function double(x) {
return x * 2;
}
function add(x, y) {
return x + y;
}
const score = 10;
const newScore = score
|> double // output of `score` is first argument: `double(10)`
|> add(7) // output of `double(10)` is first argument: `add(20, 7)`
console.log(newScore); // 27 |
I presented on this question at the September 2017 TC39 meeting. My understanding of the committee's feelings was roughly support for the current strategy, plus a number of of additional feature requests. |
Bringing over a summary of some of the insane amounts of discussion that occurred in the bind operator proposal's repo regarding function pipelining, since it's far more relevant here than there now (since this is a TC39 repo now dedicated to the topic), and I think we could use a historical record and starting point for this. Apologies for the long length.
Pipeline strategies
There are three primary strategies you could use to pipeline function calls:
(For expository purposes, I'm using
@
for all three variants to make them less visibly different while still making it seem like a fitting operator. Note that I'm not actually proposing it, since it conflicts with decorators.)foo@bar(...)
↔bar.call(foo, ...)
This was what was initially proposed in the bind operator proposal. It is fairly object-oriented in that it views callees more like extension methods than just functions, and calls them accordingly – the context (
foo
in this case) is passed asthis
, and the arguments as you would expect.foo@bar(...)
↔bar(foo, ...)
This was proposed by @gajus in this particular comment. It is more procedural, and can be viewed as threading a value through several functions much like Clojure's thread-first macro.
foo@bar(...)
↔bar(...)(foo)
This was proposed by @gilbert in this repo. It is more functional in nature, since it involves piping through a series of single-arg functions, usually returned from calling another function (in this case, from
bar(...)
).Pros and Cons
Of course, these are not all equal, and they each have their strengths and weaknesses. None of these are magic silver bullets, and they all have their shortcomings. I did go into some detail on this previously, but I thought I'd relay it here, too.
foo@bar(...)
↔bar.call(foo, ...)
Pros:
Array.prototype.join
andObject.prototype.toString
.func.call
,func.apply
, andReflect.apply
.Cons:
foo@bar
being equivalent tobar.bind(foo)
. This kind of behavior is much more contentious, and feel free to explore the bind operator proposal's issues to see this in action.this
, it's impossible to define such utilities using arrow functions, and inline utilities are pretty verbose (which could be rectified):foo@bar(...)
↔bar(foo, ...)
Pros:
this
, including much of the contention.Cons:
var dethisify = Function.bind.bind(Function.call)
can trivially wrap them.)this
. (This could turn off some functional programming fans.)this
.foo@bar(...)
↔bar(...)(foo)
Pros:
this
and related problems entirely.Cons:
this
dependence of the first option.)In addition, this could be simulated by using a native composition operator/method and immediately calling it with the value to pipe through, although it would cause the functions to all create their closures before any of them could be evaluated. (This does make optimization a bit more difficult without inlining first.)
Alternate operators
As expected, there have been numerous other operators already suggested for signaling pipelining. Here's a list of many of them:
::
– proposed with the original bind proposal~>
– proposed in this issue->
– proposed in this comment|>
– proposed originally in this repo..
– proposed in this issue&
– proposed in this issue, although it's obviously a no-goI've noted that
~>
is a bit awkward to type, and a few others pointed out that..
runs the risk of confusion with spread, a potential future range syntax. The remaining 3 (::
,->
, and|>
) are likely more viable, although I'll point out that|>
is itself a little awkward to type in my personal experience (small hands 👐 😄).The text was updated successfully, but these errors were encountered: