-
Notifications
You must be signed in to change notification settings - Fork 108
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
Allow pointfree pipelines #96
Comments
Starting a pipeline with |> seems relatively elegant, but I’d be concerned about grammar conflicts. |
Background: Upon a request from this proposal’s champion, @mAAdhaTTah and I have been drafting two formal specifications of Proposal 1: F-sharp Style Only and Proposal 4: Smart Mix respectively. For what it’s worth, my smart-pipe draft includes an appendix of ideas for extending its pipeline placeholder into a general “topic reference” concept (like the topic variables of other languages). This could enable new tacit versions of many syntaxes, which would tacitly use those topic references. For instance, if there was a arrow function But all this is very preliminary. I’m currently focusing on formally specifying the smart pipe operator first while making sure it would be forward compatible with such future tacit topicalized syntaxes. It’s very unfinished, though; I hope to be done with the first draft by the end of this week. |
I kind of like this, I'm pretty sure this works with both proposals, and it would eliminate the need for a separate composition operator. |
@mAAdhaTTah: I like the idea a lot too. It could explain not only composition but also (for Proposal 4: Smart Mix) partial application to unary functions. I had opened #97 to inquire whether including such functionality in the new proposal (at least for Proposal 4) would be a good idea, given that TC39 has blocked the previous pipe proposal because it did not address partial application. In fact, for Proposal 4, a unary headless-pipe-function operator would solve not only functional composition but also method extraction and partial application (at least into unary functions). Proposal 1 would also benefit, though just for composition and extraction and not for partial application. I might have made a mistake in framing the question #97 as an alternative to using I like |
This difference might make OP's suggestion untenable. |
@mAAdhaTTah I actually think it’s tenable…if another operator is added, rather than overloading But recent TC39-member feedback suggests so far that, despite the results of the September 2017 meeting (pipeline proposal blocked to coordinate further with other proposals), it would be best for us to hold off on adding another operator to our proposals, for now. Let’s focus on |
Agreed, and the current syntax: const myFun = x => x |> someFunc |> someOtherFunc is reasonable as-is for any of the current proposals / mixes. Adding another operator should probably be a separate proposal. |
A really great thing about headless const y = x |> a |> b |> c |> d to this: const e = |> c |> d
const y = x |> a |> b |> e |
@ivan7237d The annoying thing about the pipe operator as specified is that it breaks equational reasoning. Given Because The proposal above might give you a few more options for refactorability, but it doesn't really solve the core problem. |
I’m hoping that What matters right now is, whatever the case, having More to the point, how is this any different from #97, which is the same thing, except with another operator? |
@js-choi I don't know what the difference is, because I can't understand what #97 is proposing (I'm not very involved with the proposal process and don't know about smart/mixed/etc. pipes). Regarding "one result of which was given by ...", the problem I'm referring to is a result of the pipe operator as-specified, i.e. as it is currently implemented in the Babel transform, without any further changes. |
@ljharb: I’m sorry – I’m confused about the semantics of |
@js-choi ah, maybe i misunderstood. I'll step back until I've reread :-) |
@masaeedu: If you’re confused about all the different proposals, then check out the wiki. @gilbert wrote a pretty good summary of the four competing proposals. @mAAdhaTTah is writing a formal draft and Babel plugin of Proposal 1. I am writing a formal draft and Babel plugin of Proposal 4. I do wish the issues here were more organized by Proposal. Maybe the readme should be updated… As I understand it, in your original post, you proposed a unary operator – let’s call it Anyways, #97 focused on Proposal 4, in that I pointed out that TC39 last September wanted more coherence between pipelines, function composition, method binding, and partial application – and |
@masaeedu Yes, I'm just looking at it from the perspective of my TypeScript codebase where in maybe two thirds of the cases I use a utility function |
I don't follow how having a headless pipeline operator return a function qualifies as polymorphism by arity. If it does qualify, don't all function declarations qualify, as well? For instance: x => x
// vs
(x => x)(1)
// or
function (x) { return x }
// vs
(function (x) { return x })(1)
// or
Ramda.pipe(Ramda.identity)
// vs
Ramda.pipe(Ramda.identity)(1) I don't see any polymorphism by arity or footguns there. |
@js-choi Thanks for the info, I'll do some reading. The gist of what I'm saying is that This is fine from a pragmatic standpoint, but when someone's just trying to compose a bunch of functions, they need to dance around the fact that the first of several A better solution IMO is to just have two unambiguous operators. A reverse function composition operator (reverse of Based on what you're saying, it seems you're focusing on making reverse function application more ergonomic. This is great, and probably the |
@masaeedu the are several links to userland composition operator proposals in the pipeline operator readme. For the record, of the various pipeline operator proposals, I prefer the F# style, and would be pleased if a headless operator returned a function. Otherwise, I'm hoping it won't take too long to see a composition operator added to the language, and hope it won't be compromised by a desire to turn it into a jack of all trades, master of none. |
@kurtmilam: Sorry – I don’t yet understand how your examples are parallel to the idea of “a A more illustrative example I can think of would be the APL and R programming languages, which integrate polymorphic operators (by unary vs. binary use) at their cores, and which also benefit from a very uniform concatenative syntax. JavaScript does not have many operations, if any at all, that do dispatch on arity similar to APL, R, and its ilk; and JavaScript has a much more variable expression syntax, so it is important make it not easy to have composed things suddenly become something unexpected. Now, I could be wrong about the “not many operations in JavaScript that are polymorphic on arity” part – but even if that were wrong, I think it would be undesirable in JavaScript. I still see mode errors when pipelines unexpectedly result in functions rather than other values. I would prefer a separate operator for headless pipe functions. But this is premature bikeshedding, I would think. In any case, I am quite generally enthusiastic about tacit programming myself, and I admire your work in applying it to JavaScript. Perhaps if you could elaborate your examples on how they are applicable to “ Remember that a headless function operator’s particular spelling is just bikeshedding. Whether it’s unary |
I must be overlooking something, because I don't see anyone suggesting that there should be any differences in behavior based on arity. I see at least a couple of participants suggesting that they'd like to see a headless pipe be equivalent to a function declaration, but that's it. |
Upon further consideration, I withdraw my proposal. If you look at the
If you look at it as left associative however, it is consistent:
So the problem is that I'm misunderstanding the fixity and trying to abuse a function application operator as a function composition operator. As @kurtmilam has pointed out, there's other repos tracking proposals for function composition operators, which I'll go look at. I'll leave this issue open since it seems there's some people in favor of it, although I personally don't think it's necessary anymore. |
This analysis is understandable, except that I don’t think the mapping is from JavaScript
JavaScript You need two operations in JavaScript to get all expected semantics, just like how you would need two operators in Haskell. It’s just flipped in a different way: in JavaScript, composition has looser precedence than application. In Haskell, composition has tighter precedence than application. That’s the true difference. Otherwise, it’s quite similar. In Haskell, Imagine that, in Haskell, I will point out, however, that Proposal 4: Smart Mix’s
With this JavaScript example, yes, you would have to cap The core problem is actually a core tradeoff. It emphasizes application over composition. But both are still “solved”. You just have to cap your expression chain with the opposite kind of operator sometimes, and leave your expression chain bare in other times. A tradeoff between two kinds of situations.
I wouldn’t characterize This was a very fun exercise, and I thank you for the food for thought. Hopefully concerns and benefits will become more definite once @mAAdhaTTah and I finish the specs/plugins for Proposals 1 and 4, after which both you and TC39 will be able to try them out hands on. |
@kurtmilam: Thanks for your patience. Let me explain my understanding of the original proposal, which I had thought was similar to an idea that I have been having. My understanding is that a headless pipe operator is tantamount to a unary pipe operator.
This I like this loose, function-returning unary operator, except I am not a fan of how it has the same name as the tighter, value-returning binary But perhaps I have been severely misunderstanding the original post. I had thought @mAAdhaTTah has had a similar understanding as me, though. |
You can avoid a tradeoff if both reverse composition and reverse application are available as distinct operators. When you want to perform composition, use the composition operators. When you want to perform application, use the application operators. Then you won't have to cap an extracted sub-expression with anything while refactoring. For this reason I think a "capping operator" like I proposed is probably unnecessary if both the current pipeline operator and something like https://github.com/isiahmeadows/function-composition-proposal are available in the language. |
@masaeedu: Thanks for the reply. The tradeoff cannot be avoided, I think. It is still present even with reverse composition and reverse application. With the Haskell style, you still have to use an operator (application) to cap many expressions (to convert them from composition). Other times (composition alone), you do not. It makes some refactoring harder and some easier. It’s the opposite tradeoff to JavaScript (use composition to convert from application; bare application). But it’s good food for thought as I write this draft. It’s not their RTL dataflow that matters so much as it’s their relative precedence. Even if JavaScript introduced RTL composition and RTL application, it would still need to wrangle with this reversal of operator precedence. We are currently going with “application tighter–composition looser”, in the same manner that arrow functions are very syntactically loose (at the same level as assignment operations). JavaScript is a language of expressions, not just functions, but functions can still be composed with a single additional prefix operator. This is more ergonomic in most JavaScript than “composition tighter–application looser”. I in particular want to avoid allocating new function contexts where possible, especially given function-scoped operations like The tradeoff isn’t very big anyway, either way, I think: the pros and cons are small on either side. I had forgotten that reversed precedence was a thing though. I’ll have to re-review @isiahmeadows’ repository too to double check which precedence levels he chose… |
@js-choi Hmm. I'm not quite following how the refactoring tradeoff persists if I have both operators available. Could you perhaps give me an example? E.g. if I use the composition operator proposal and change the thing in the OP to If I did indeed mean to use reverse function application, which is left-associative, I can left-associatively factor out Perhaps there should be a |
@masaeedu Let me make sure I get this right — please correct me if I'm wrong. In my code I sometimes need application ( What caused the confusion for me is that the operator is called the pipeline operator, so I assumed that that's what you'd use to chain expressions together. But while the reverse application operator is a necessary ingredient to creating pipes in JavaScript (without it you'd have to write |
It's low-precedence like this, but I didn't state whether it's higher/equal/lower than this. (I'd prefer higher, as it would enable things like @ivan7237d's idea, and it'd be easier to reason with.) That was something I left more open for interpretation and discussion in the future. |
@masaeedu @ivan7237d I understand better now, after writing out the table below. I had thought that application This problem—having to add one prefix operator when extracting functions—is not very large, but it is indeed a disadvantage in terseness. However, a loose prefix pipe-function operator would not only accommodate composition, it would also address method extraction and (for Proposal 4) partial application. This versatility is something that a tight binary composition operator would not bring. That still is a tradeoff. Nevertheless, the two are not mutually exclusive, except insofar that TC39 is generally conservative about adding new operators to JavaScript. In any case, thanks @masaeedu for your patience as I figured out my own little mistake from last night.
@isiahmeadows: Thanks. Yes, the precedence of a specialized function-composition operator would probably have to be tighter than |
@ivan7237d I think you and I are in agreement. @js-choi That table is a good illustration, thanks. Regarding:
I don't think this is the case. If Additionally, if you have a corresponding right associative |
As a slightly off topic comment, all of this discussion really highlights the need for a first class infix operators proposal for JS. If user-defined infix operators are in the language (or at least in transpilers), people can much more easily play around with proposals for the semantics of various operators. As semantics and patterns mature, common operators can be standardized and optimized for in runtimes. This would be in line with how the "what's a good way to do Promises" debate played out in JS. I understand that with great power comes great responsibility, and undisciplined use of arbitrary infix operators can result in APL programs embedded in your language. Nevertheless, I think the benefit of being able to have these kinds of debates at the library level rather than the language level outweighs the costs. |
@masaeedu This proposal may interest you. |
@masaeedu There’s a long path to go before extensible operators, although it would allow more “paving the cow-paths” à la the Extensible Web Manifesto. In addition to the proposal for HOF operators that @mAAdhaTTah just linked, you may also be interested in prior TC39 discussions about extensible operators and operator overloading, which became much more concrete after progress on the BigInt and Decimal types.
The decorator proposals are also relevant insofar they provide a specialized AST API, which user-defined operators may also need. As for equational reasoning, I see your point about the relative operator precedence being not as important for that case. In general, however, I would point out that equational reasoning is a continuum, not a binary. A syntax may facilitate ER more or less. For instance, even with a binary function-composition operator, it is still necessary to add something like Having said all that, a prefix pipe-function operator and a binary composition operator are not mutually exclusive anyway. So we’ll see how it goes in the future. 👍 |
Yeah, I'd probably prefer to see composition & application as separate operators, rather than overloading the pipeline with two different behaviors depending upon its head. |
In some other threads, we've talked about considering the composition operator a separate, follow-on proposal. I'm happy with this effort to think through prefix |
Although #97 framed it in terms of a new operator such as
You and @ljharb expressed concerns about Like @mAAdhaTTah, I really would not like it to be prefix But what the prefix pipe-function operator My impression had been that the question had been “no, wait until an add-on proposal for I am actually nearing the end of my own initial drafts for the explainer and specification of Pipeline Proposal 4. The explainer already has a brief informative appendix discussing the prefix pipeline operator |
I'm excited to read your draft, and glad you'll be including a section about this possibility. Even if this is just another use of the pipeline operator, it's easy to specify and implement, we still might be able to decide to split off this other use into a follow-on proposal (to give us more time to consider it, and collect real-world feedback from the initial pipeline operator). |
Closing since it seems the consensus is that this is unnecessary. |
There's a lot of technical discussion going on here, and I might not be up to speed on every goal in the pipeline proposal, but I think point free form makes a lot of sense. Disclaimer, I write tons of pipeline style code using promises already: return await Promise.resolve().
then(() => operation()).
then(compose(a, b)).
then(c => f('foo', c)); and created some tools like I haven't published any of these tools (I would have to rewrite them, as I don't have rights to the source), but to me pipelined code is elegant and intuitive; but! it's so easy to do already: const fn = [
f,
g,
h
].
reduce((g, f) => x => f(g(x)))
const y = fn(x) or after cleanup const fn = rcompose([
f,
g,
h,
]);
const y = fn(x); ... There's just no automatic application to a literal, so an extra step is required. But this is actually quite intuitive. I just know if there's a Is there any value added over just creating right-compose and left-compose operators, and handling a few special cases around the compose operators, such as Or can we get a flow-right and flow-left operator pair, that just give me my point-free style back out? I know this isn't a proposal for function composition, but the entire time I was reading it, I have wondered why composition isn't preferred, since we aren't doing any monad Actually, so there's two things that pipelining seems to be doing here:
Could there just be an abstraction like this? let (~>) = new Operator~>(middlewareFactory);
let it = await $ a ~> b ~> c ~> d;
// example
const middlewareFactory = (g, f) => Promise.resolve(g(x)).then(f); |
I have the following definition:
Which is basically equivalent to
o => fromPairs(map(...)(pairs(o)))
. Theo => o |> ...
abstraction seems kind of unnecessary, since it is in a position where it can basically be omitted.With something like the
pipe
primitive from an FP library I can do:or with a hypothetical
.
composition operator I would be able to do:Neither of these require me to explicitly introduce a new function with an
o
parameter.It would be nice if the
|>
syntax allowed me to do something like:I'm not hung up on the syntax, I just want to be able to compose sequences of functions without having to introduce wrapping functions where not necessary.
The text was updated successfully, but these errors were encountered: