-
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
Pipeline Placeholders #75
Comments
It could be a bit confusing
|
@gilbert This is an interesting idea. I'm not sure if we're ready to forclose on partial application completely, even if it got somewhat chilly reception the first time around (lots of proposals do); I'd like to form this as a strict subset of what could grow into a partial application proposal if it's picked up again. So far, though, this sketch seems without any particular contractions, except for the @jEnbuska I don't really see what's ambiguous about the semantics in those cases. Could you explain what the confusion is? I could write out the semantics I'm imagining |
@littledan I just wasn't syntax, but I think I get it now. |
@gilbert What should we get from limiting usage of partial application to pipleline operator? All examples you provide could exists without this limitation |
@Alexsey A worry from TC39 is that partial application is too confusing, syntactically and otherwise, in general. Maybe the restriction to just this pace could make it easier to understand. |
@Alexsey It's a bit like a shorthand for A: B: C: D: But I have problems with these which make me want to say that this syntax should be scoped to the pipeline:
|
@davegregg The examples you provide are out of the scope of partial application proposal as well as out of scope of discussion of limiting partial application usage to pipeline operator. They are like about usage of partial application in function bodies. There is already a discussion on that in the partial application repo |
@gilbert I've made a proposal which would enable your suggestions. According to that proposal, your examples could be rewritten as: let newScore = person.score
|> double
|> #add(7, ?)
|> #boundScore(0, 100, ?); let ex1 = person |> #double(?.score);
// I'm not sure what you meant here, but tried to guess
let ex2 = #(person.score |> #add(??, ?));
// But without pipeline, it would be more pretty
let ex2v2 = #add(person.score, ?)); // Note that the evaluation if deferred let x = 10 |> f |> #?.foo |> g; |
We would get a removal of ambiguity without needing to introduce another syntactic marker. |
@gilbert Ok, that sounds like an argument. So may be discussion should be about allowance of not using syntactic marker for partial application in the the pipelines? I think it could be a good addition to the discussion you have mentioned in your post |
I think this is a great idea! IMO, this is the main use case for partial application. Other than that, arrow functions work perfectly. This is how Facebook’s Hack does it ( function piped_example(array<int> $arr): int {
return $arr
|> array_map($x ==> $x * $x, $$)
|> array_filter($$, $x ==> $x % 2 == 0)
|> count($$);
} One nice trait of this extension is that it doesn’t affect code written for the proposal as it currently exists. It is completely optional. |
I first thought this idea should be limited to just argument placeholder positions, but it's interesting to think of One of the issues I see with the pipeline operator is that it doesn't integrate well with normal method chaining. If I'm doing some operations on arrays, for instance, I might want to chain using some built-in array methods and some functions coming from another library. Currently I'd have to use arrow functions: I'm not very good at these examples... anArray
|> pickEveryOther
|> (_ => _.filter(...))
|> shuffle
|> (_ => _.map(...)); This feels awkward and unfortunate. With a placeholder binding, perhaps I could do something like: anArray
|> pickEveryOther
|> ?.filter(...)
|> shuffle
|> ?.map(...); Given ternary expressions (and the other potential uses for |
There are a lot of open questions about how pipeline placeholders should work. In addition to the receiver position, a placeholder could also go in nested function calls, in an object literal and as an argument to a binary operator. The partial application proposal decided to scope all of these out, and this was a point of criticism for that proposal. We'll have to think carefully about many cases as part of deciding which token makes sense. |
One issue that I see with placeholders is that you'd have to switch between "partial expression" mode and "normal" mode based on whether there are any placeholders (which could be well on down the garden path). It would be strange (at first glance) that these two lines would behave so differently: val |> fn(_);
val |> fn(?); So it seems that we may have to choose between either the current form (where the RHS is implicitly called with the LHS) and a Hack-like form, where the RHS is evaluated with the LHS as a special binding scoped to the RHS expression. The Hack-like form has some advantages:
The disadvantage is that it's more typing for the one-arg-call case. |
It also looks like the grammar can be significantly simplified with Hack-style pipelining:
Pretending that val |> await $; |
Expanding the previous example and using Hack-style pipelining: anArray
|> pickEveryN($, 2)
|> $.filter(...)
|> shuffle($)
|> $.map(...)
|> Promise.all($)
|> await $
|> $.forEach(console.log); Everything seems to fit together intuitively and without any magic: multiple arguments, normal method chaining, |
I'm not particularly swayed by the "garden path" concern. There are already two features introduced in ES2015 that have the same concern, yet are still highly valuable: // parenthesized expression...
(a = 1, { b }, c, [d, e])
// but add `=>` its now an arrow function
(a = 1, { b }, c, [d, e]) => x
// object literal expression...
({ a, b: { c, d }, e: [d, e] })
// but add `=` and now its destructuring
({ a, b: { c, d }, e: [d, e] } = x)
// array literal expression...
[a = 1, [b, c], { d, e }]
// but add `=` and now its destructuring
[a = 1, [b, c], { d, e }] = x That said, any feature that does introduce a "garden path" concern needs to be valuable enough to outweigh concern.
We could introduce
These restrictions would still provide significant value for pipeline and would give plenty of room for partial application to proceed (assuming no syntactic marker to avoid the "garden path" concern). |
One option for passing the LHS as the
The syntax Awaiting in a pipeline could be done via a syntax like Something like:
|
@rbuckton You could do that, but it's a pretty high cognitive burden to place on users that just want easy left-to-right chaining. With Hack-style pipelining, you don't need any of those special-case grammars. The fact that Hack-style pipe doesn't need any extra syntax to cover all of our use cases indicates to me that partial application is not the right problem to solve here. It may be a useful thing on its own, of course, but it's orthogonal to the needs of a pipeline operator. The reason that partial application seems like a good fit for pipeline is that the current proposal has a "magic" function call (magic in the sense that there is no argument list) and partial application allows you to program around that magic function call. |
If we accept the above restrictions for However, there's no meaningful way to handle Another option is an implicit |
Hack-style placeholders look promising! I like the idea of "enforcing" them. Here are some issues I can see so far: (1) Considering the (2) Arrow functions are still useful for collecting variables. For example: getConfig()
|> setDefaults(?)
|> c => f(c,10)
|> g(c, ?) Notice how there's no placeholder needed in the second pipe. How would this be handled? (3) Using curried functions looks a bit awkward, e.g. |
@rbuckton could you explain what you mean by "there's no meaningful way to handle |
Hack style looks alright to me, with the exception that I don't want to be forced to use a placeholder if the thing on the right-hand side of the operator is a function or method that doesn't need to be partially applied. In other words, I think I'd be pretty happy with this syntax (recycling zenparsing's previous example):
|
Partial application always returns a function, but
If we want to avoid having placeholders here block partial application, then we shouldn't leverage placeholders with |
@kurtmilam Maybe I'm wrong, but I don't think the |
@mAAdhaTTah , the dollar sign wasn't my choice. Rather, I carried it over from the example in this comment. It's my understanding that all of the functions in the original example are to be pipelined, and that was my intention, as well. |
|
getConfig()
|> setDefaults(?)
|> c => f(c,10)
|> g(c, ?)
setDefaults( getConfig() )
|> c => f(c,10)
|> g(c, ?)
let c = setDefaults( getConfig() )
f(c, 10) |> g(c, ?) A bit contrived, but it demonstrates how you can gather arguments using an arrow fn. @rbuckton Ohh I see, you're saying a pipeline-placeholder version of this proposal should not allow the |
I'm a bit wary about mixing pipeline styles between
I'd love to see F#-style and partial application advancing, but we still need to consider dot-properties. Corner cases like I've been wondering if partial application should have a form like |
If we are considering
Basically making
Which would be the same as this:
It lines up visually with |
@rbuckton: With regard to accidentally using F#-style/tacit/call pipe where a Hack-style/placeholder/binding pipe would have been appropriate, at the very least the bug could be immediately detected during compilation, if the placeholder is not a valid variable identifier. That is, if the F#-style pipe is If the placeholder is chosen to be a valid variable identifier like @littledan: The remaining questions that you listed should probably each get its own issue, right? For instance, the question of the Hack-style-placeholder has been getting discussion in #84, but it may deserve its own thread. @kurtmilam, @mAAdhaTTah: Ah. I see now how the inlining of one-off arrow functions in F#-style pipes could be trivial. It would still force the programmer to rely more on compiler magic to reason about their programs’ memory, though. And the big point of having both F#- and Hack-style pipes is to reduce magic. By “magic” I mean giving special treatment to an edge case, and by “uniformity” I mean the absence of magic and special casing, like how Hack-style pipes could be transformed into nested lexical blocks (like @dallonf: I sympathize with the worry about ASCII-symbol soup. The solution is obviously to go the Perl 6 route and start using non-ASCII But, yes…Readability that new syntaxes may afford must be balanced with the readability that too many symbols might compromise. |
Come to think of it, there’s another use case that Hack-style/placeholder/binding-style pipes would cover that arrow functions with F#-style/tacit/call pipes would not. Incidentally, if optional method calls ( |
@dallonf |
@js-choi Of course, there's Generically any "control-flow" type expression will fit into this category, including any possible future "statements as expressions" like |
|
I've brought up
Isn't that just like |
@jridgewell I don't think so. Let's take this example: async function foo(data) {
return data |> processData
|> fetch
|> await
|> processResponse
} The caller of this function passes in Taking the same example (assuming function* foo(data) {
return data |> processData
|> fetch
|> yield
|> processResponse
} If the caller does this: const gen = foo();
gen.next(); // run to first yield
gen.next('some new value'); // pass in a new value I think I've got this right, but maybe the first then |
It has replaced fetch's promise with the thenable's result. That's injecting new data. It's best explained with an example, showing how async function test(promise) {
const value = await promise;
return value + 1;
}
test(Promise.resolve(1)).then(console.log.bind(console));
// as a generator
// see co: https://github.com/tj/co
test = co(function *test(promise) {
const value = yield promise;
return value + 1;
});
test(Promise.resolve(1)).then(console.log.bind(console)); |
Yes exactly, in my example, |
I think I'm arguing a finer point. What is the functional difference between halting a async function's execution to await on promise, and halting a generator function's execution to yield a value and get a result? await promise;
// internally translates into
promise.then(value => {
gen.next(value);
}, err => {
gen.throw(err);
}); Async is internally just a generator. Who cares that it only unwraps the promise's result? It has halted and returned a brand-new, different value (the original value is a promise) to the function's execution. That's a generator. If we can agree on that, I don't see why we would only special case |
It's a difference in who has control over that value. When the pipeline calls down into a function that returns a promise, the pipeline has control; it calls into the promise-returning function, so it know the function will return a promise that will resolve with a specific (type of) value, based on the implementation of that function. If a pipeline yields up, the caller of the pipeline has control; the function with the pipeline thus has no means by which to control the value that gets If your example above, we're yielding up into test = co(function *test(promise) {
const value = yield promise;
return value + 1;
}); If Async is just a generator, but a generator is not just async. Moving into the more general generator case, I am arguing that the semantics of all the non-async use cases for generators make it a less useful candidate for use in the pipeline. |
If we don't have
The same can be said for dot-properties, etc:
Adding Another option I mentioned in #75 (comment) might be to introduce |
Using this criteria, the lack of pipeline itself is just adding a minor inconvenience. 😄 I think it's more appropriate to say that lack of await or yield support would break the left-to-right composition that the operator was supposed to solve. |
Perhaps, but pipeline adds more value than just replacing
I'm not discounting this at all. I think having edit: I accidentally |
@mAAdhaTTah: I think we're getting too far off topic now. Let's open up a new issue to specifically discuss
Agreed. If we do not have them in the proposal, I'd like to at least forbid them appearing (un-parenthesized) as the RHS so we can add them later. |
@jridgewell I was about to suggest the same thing. Continued in #90. |
This issue overlaps a lot with #89, but its discussion has sprawled across many topics. Should this issue be closed in favor of #89, as @mAAdhaTTah suggests in #89 (comment)? (Also, should there be a separate issue for bikeshedding the placeholder’s spelling? |
@js-choi I like both of those suggestions for issue organization--go for it! |
Closing this issue, as the proposal has advanced to stage 2 with Hack-style syntax. |
I'd like to explore an alternative to the partial application proposal that would only be used in conjunction with the pipeline operator.
There has been discussion around using
?
as a placeholder for functions with multiple arguments. Consider the readme example:If the
?
were to be built into the pipeline operator, we could rewrite the example to the following:and since this placeholder is scoped to the pipeline operator, it has the potential to be more expressive:
and potentially allow member expressions?
On the other hand, any sort of
?
placeholder might have people let write strange things likex |> f(? ? ? : 10)
. Is this problematic?The text was updated successfully, but these errors were encountered: