-
Notifications
You must be signed in to change notification settings - Fork 111
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
Vote to drop the smart operator from this proposal #158
Comments
I would hate to give up the ease of use afforded by the smart proposal (e.g. |
I pretty much agree. There's already such a accompanying proposal: partial application proposal There could be also a proposal which does property access and we would end up with basically the same functionality as smart pipeline, except more universal, because it could be used e.g. with |
According to @ljharb (I guess he's TC39) we have to make the ironclad case that the F# Style, and not the Smart Style, is the only one that make sense -- forever -- smh
How can we prove a negative? How can we prove that the Smart Style is not a valid choice -- to some joe blow with an opinion? I don't see how we get past this enpass with this mentality smh |
That's how every addition to JavaScript works - it's always forever, because we can never break the web, so it's always a bad idea to rush into anything.
Perhaps you're starting to appreciate why proposals can sit, for years, with no progress - not because people are shirking their duties, but because often there's unknowable questions, and doing nothing is highly preferable to doing the wrong thing? |
It's hyperbole to suggest moving forward in the F# style will break the web. And a proposal that will help bring JS into the functional era, after two years, still without a decision on the style is not what I would call rushing smh
The only reason this has not seen progress is because this proposal has not dropped Smart style long ago and focused efforts on cleaning up remaining items on the F# side. Also going with the F# style for pipes is not what I would call a wrong choice. Just read the head post on here, he sums things up nicely. |
What I mean is, if we move forward with F# style, and in 1, 5, 10, 50 years, we decide it was the wrong choice, we're stuck with it forever.
I don't think that's a statement that any non-delegate can possibly be a credible expert on - and I doubt even a delegate could claim that authoritatively. |
Safe to say there is no ironclad right answer here, the pipelines eventually do the same thing. F# promotes currying and Hack promotes partial application. Both of them have been proven to work in other languages so let's just write up the exact differences, pros and cons, interactions/conflicts with other proposals (partial application) for both and just have the committee have a vote on the matter. |
@KristjanTammekivi The committee itself doesn't function by vote but by consensus. The goal then is not to write up the differences & let them vote but to convince those who are opposed to the pipeline operator or prefer a different alternative that your preference is best and worth proceeding with. |
...do you not see how this statement is exactly mirrorable for the other side? If we're pretending that it's only the objections of the topic-style-likers that are preventing pipeline from being added to the language, then equally if the F#-style-likers dropped their objections and embraced topic-style we could have it in just as easily. Neither is true, of course - the committee does not yet have consensus that either (or any) pipeline syntax is worth adding to JS; new syntax is always a very hard sell because JS only has a limited amount of syntax to use, and every addition reduces the set of future possible syntaxes we have available to us, both in technical terms (can't introduce new ambiguous syntax) and practical terms (JS shouldn't become Perl). (I myself am frustrated at this, fwiw - I think pipeline would make an excellent addition to the language if done properly. "Properly" here means "topic-style" as far as I'm concerned, however; I don't think F#-style brings enough to the table to be worth adding. That all said, this is a topic I want to put more effort into championing in the near future.) |
Just to clarify, @ljharb isn't saying that this proposal in either form is bad for the internet or something. 😄 One of the guiding principles of web standards is "don't break the web": the changes we make must be compatible with the web as it already is, warts and all. Being subject to this principle makes JavaScript unique as a programming language, since it means we generally can't deprecate anything. If the F# proposal were to make it to the end of the TC39 process, that certainly shouldn't "break the web", as we should have already verified its web compatibility during Stages 2 (in spec text) and 3 (in browsers). What it would do is become part of the web that we're obliged not to break. That doesn't mean we should try to predict what killer ES2030 feature we might be inhibiting, but it does mean that we have to take the little details seriously, because we carry their repercussions with us for as long as the web lives on. |
@mAAdhaTTah who is opposed to the pipeline operator or prefers something else, and most importantly why (or do you know) |
@aadamsx First, @tabatkins, who posted in this thread, is on the committee and prefers Smart Pipeline. If you search for him in this repo, he's made his arguments around here. There are two threads (one, two) on preference for the bind operator for this kind of pipelining (I believe OP on the second one is a committee member). The proposal champion has suggested exploring Elixir style pipelines. I've heard there are some committee member who don't think any of these are necessary additions to the language (I'm not sure who they are, to be honest), nor am I sure of their specific objections (@ljharb has shared general objections for adding too many proposals in conversations with you, so could be related to that). I hope that provides some context. There's still a lot of ground to cover. |
I wish there was a way to better organize the discussion around this. Maybe a state-of-the-union doc? Maybe close some issues? The elixir thread suggests there really isn't any interest in exploring that avenue, yet the issue is still open. |
Personally, I'm in favor of moving ahead with the F# proposal (or somewhere between the F# proposal and minimal proposal). I think we can firmly conclude that, if we want to have partial application, it will need to be an independent, first-class feature, not limited to pipeline contexts like the "smart" proposal does--it's just too confusing for the thing after the |
The belief that all viewpoints are equally valid is a problem with design by committee. I've participated in bureaucracies who after years were unable to act because they too believed in consensus. A committee is most effective when it has a trusted chair who ultimately owns the mission and is empowered to decide. Consensus doesn't trump time. No one would argue that if we reached consensus in 10 years, that would be better than deciding sooner. And consensus doesn't guarantee no regrets. Clearly there are bad choices, but after this much deliberation the bad choices have been highlighted. All that's left is trade-offs. I was originally a proponent of smart pipelines, but I now see that with partial application syntax F# style pipelines do the job sufficiently—and they're well established and proven. I'm in agreement. Drop the smart operator if this helps the proposal advance. |
This discussion seems to be focused on implemented F#-style pipes, and adding a way to do partial application anywhere after-the-fact. It's a good idea, but this is not the same as the hack-style pipes proposal. A globally available partial application syntax would have problems such as:
The current hack-style pipes proposal does not have these kinds of issues. This means, even if we decide to follow this route, it's vital that we decide now that hack-style pipes are not the path we want to take, because as soon as we add F# pipes, the current hack-style idea will not be an option. So you can hate me for saying this, but going this route won't speed the proposal up. We still have to eliminate all alternative options. And I personally hope that TC39 doesn't rush this kind of decision. |
This is part of the F# proposal: url
|> fetch
|> await
|> x => x.json()
|> await
We've been discussing this proposal for, I think, 4 years now. Nothing is being rushed 😄 |
It's been a while since I looked at the syntax for the F# proposal - this has been sitting around for a while :), so I misremembered how the proposed await-pipeline syntax was supposed to be done. But yeah, this example should highlight the issue I was explaining: // hack-style
x |> await f(?, 2)
// F# style
X |> _ => f(_, 2) |> await
// F# style + current partial application proposal
x |> f(?, 2) |> await This is why we can't just "rush" F# style pipes in and tack on partial application afterwards. And thank goodness it's not being rushed. (but ... it is pretty slow-going waiting for this all to get figured out ... |
@theScottyJam I suspect someone writing a new minimal proposal could get the ball rolling again. It seems clear that even if you drop the |
IMO the It introduces a side effect into function composition. What if the I'd go so far as raising a syntax error when using |
It's not the await that introduces side effect, but a non-pure function. (any function, not just async ones) |
What if the const roles = user
|> getUserId(?)
|> await fetchGroups(?)
.catch(err => /* handle it if you can */)
|> rolesFromGroups(?) The catch handler can be defined outside the pipeline and passed in, if you find that that reduces clutter. You can even define general-purpose helpers: async function handleError(fn, { type, handler }) {
try {
return await fn()
} catch (err) {
if (err instanceof type) {
return handler(err)
}
throw err
}
}
const roles = user
|> getUserId(?)
|> await handleError(fetchGroups, { type: NotFoundError, handler: () => [] })
|> rolesFromGroups(?) This topic on the TC39 form has discussed other syntax ideas to make error handling easier in the middle of a pipeline operator. But in short - this shouldn't be any more of an issue then using await inside of something like this: const roles = rolesFromGroups(await fetchGroups(getUserId(user))) |
My memory is we added |
@mAAdhaTTah Interesting. I see how that makes sense, the idea being a 1:1 between nested function calls and pipeline. Unfortunately, this is one of the areas where smart is clearly better than F#, so I would rather wait until we know about placeholders to implement it. |
valueExpression = (* https://tc39.es/ecma262/#sec-ecmascript-language-expressions *)
variableName = (* https://tc39.es/ecma262/#sec-identifier-names *)
functionBlock = (* https://tc39.es/ecma262/#sec-ecmascript-language-statements-and-declarations *)
HackShorthand = ... (* HackShorthand is merely sugar for a unary fat arrow with a convention-based argument name, e.g. `# + 2` or `? + 2` become `X => X + 2` *)
namedFunction = "function" , variableName1 , "(" , variableName2 , ")" , "{", functionBlock , "}"
anonymousFunction = "function" , "(" , variableName , ")" , "{", functionBlock , "}"
fatArrowFunction = "(" , variableName , ")" , "=>" , valueExpression
| variableName , "=>" , valueExpression
variableContainingUnaryFunction = valueExpression (*such that the type of the expression's value is callable... and the developer intends for the callable value to receive a single argument as a valid use case.*)
unaryFunction = namedFunction | anonymousFunction | fatArrowFunction | variableContainingUnaryFunction | HackShorthand
pipelineOperator = "|>"
pipelineExpression = valueExpression pipelineOperator unaryFunction
(* evaluation of pipelineExpression is a valueExpression equivalent to the return of the RHS unaryFunction when applied to the LHS argument ... this allows pipelineExpressions to be chained without using a sea of parentheses, such as in existing function application syntax, and without using objects to "dot off of" for "fluent syntax" -- this is the basic usefulness proposition of |> operators. *) RHS of Thus, (await, as I see it, is just a unary function that unwraps a Promise. And no, not a PURE function, nor should it be, since in general, JavaScript functions are NOT pure, and any of them can throw errors and disrupt control flow at the point of function evaluation.) Can you elaborate on a couple examples in which HackShorthand as a sort of sugar for fat arrow functions could not be used orthogonally with |
This is not true and this common confusion is my biggest concern about Hack pipelines in general. |
What am I missing then? It definitely LOOKS to me like we can apply a desugaring rule and an arbitrary generator of unique variable identifiers to expand any Hack Style pipeline expression to a combination of functional application and unary function. A pipeline could even be desugared to just a giant nest of function calls url
|> # + '?query=foo'
|> fetch
|> await
|> x => x.json()
|> await
// becomes
await (
(x => x.json()) (
await (
fetch (
(x1 => x1 + '?query=foo') (
url
)
)
)
)
) |
@davidvmckay an |
In addition, you example is wrong: url
|> # + '?query=foo'
|> fetch
|> await # // needs a placeholder here
|> (x => x.json()) // this would need parens but can also just be #.json()
|> await # // and here and can be simplified: url
|> await fetch(# + '?query=foo')
|> await x.json() |
That grammar seems much more restrictive than how I currently understand F#. Right now, you're saying on the RHS of the pipeline operator, there must be a unaryFunction, which is one of these: If you added support for function calls, then what would happen with this:
Ultimately, if you think you've found a way to make a more powerful and intuitive partial expression syntax, I would bring it up on the partial application proposal because right now, they're thinking of having heavy restrictions on it (limiting the "?" token to just within a function parameter list), because it's difficult to draw boundaries for a partial expression. This issue in particular addresses some of those problems in more detail. |
This is correct – the Hack style is intentionally restrictive to avoid footguns. A placeholder is required on the RHS of all pipelines in Hack style. |
Yeah, note that In the rare case you do want to completely ignore the piped-in value and continue on with something entirely new, the comma operator can do it: (Swapping the order around the comma is also how you can insert side-effects into the middle of a Hack pipe easily; |
I am expecting that @theScottyJam I made a mistake in my EBNF, i meant to allow RHS to be ANY expression where evaluation according to standard precedence rules yields a value of callable type. @ljharb yes, the await operator works because the (implied) containing function is a sync — in either syntactic form. @ljharb , @mAAdhaTTah a further set of equivalent expansions: const myFunc = async url => url
|> ? + '?query=foo' // @theScottyJam using the `?` character instead of `#`, but I mean them the same way: contextual argument name placeholder.
|> fetch
|> await
|> x => x.json()
|> await
;
// and here is a simpler, equivalent definition that just makes different artistic choices about how to form RHS unary functions
const myFunc = async url => url
|> await fetch(# + '?query=foo')
|> async x => await x.json()
;
// becomes
const myFunc = async url => await (
(x => x.json()) (
await (
fetch (
(x1 => x1 + '?query=foo') (
url
)
)
)
)
);
/////////////
// or even becomes
const myFunc = (url) => fetch (
// note this is immediately executed fat arrow function definition applied to url
((( // demonstrating parentheses are not always special to function definition, such as forcing a different order of evaluation
((x1) => x1 + '?query=foo')
)))
// the parentheses around argument lists for function invocation are different than parenthetical value expressions
(url)
// will now fetch `${url}?query=foo` and return a Promise…
)
.then((x) => x.json()) // x is the value of the Promise returned by fetch; assuming json parsing returns a Promise, then:
.then((x) => x) // totally superfluous, just like the outer await was in the earlier examples.
// It unwraps the Promise, but there’s no point because then it just returns the value…
// which in an async function, is immediately re-wrapped in another Promise
// for further up the call stack to deal with. @theScottyJam regarding how to disambiguate scoping to decide how to bind a placeholder argument name while desugaring to an anonymous function definition, I would apply some kind of precedence rule like “Just go right on up until you hit either invocation of the expression like a function (whether with parents or pipeline or call / bind etc) or until assigning the expression to a variable, and then prefix the implied arg list and fat arrow.” Also, I agree with @tabatkins — |
Well, then that's should be the example you were looking for with how F# pipes + partial expressions differ from hack pipes. I think your proposal also doesn't address async/await very well. Take this example: x |> await f(#, 2) |> g We're bounding the x |> await (_ => f(_, 2)) |> g This is not what we were trying to do. We're awaiting a function instead of a promise! Edit: I thought of a couple more examples of something where F# + partial expressions differ from hack pipes:
(In writing all of these examples, I've learned that I really dislike the |
That's essentially the "minimal" proposal, i.e. no Fsharp = input |> foo |> await;
Hack = input |> await foo(#);
Then please consult #83 and #104. Long discussions showing that the F# flavour has intrinsic parsing issues that need to be solved. Anyway, this topic — vote to drop the smart operator — is over. It has already been withdrawn. |
Technically, yes, but Hack has been introduced to "replace" it, so the impasse described in the OP is still present. |
Smart operator dropped, and replacement impasse resolved (by the committee advancing this proposal to stage 2 with the Hack syntax), so closing this issue. |
Can there be a community vote on this and we move forward?
I'm on team F# because it works well with the JavaScript we have today. A future partial application addition to the language would work well with the F# proposal and so would a placeholder proposal.
I feel that the Smart operator is a "revolutionary" approach where as F# style operator is "evolutionary". The problem I see with the former is that it is embedding a parameter placeholder proposal within it and in such a way to warrant conversations outside of the scope this proposal.
I don't doubt that having parameter placeholders would make Point-Free programming more readable, especially when not paired with a static type system like TypeScript; but that is a separate problem to address. We can have a proposal where this possible
It is independent of a pipeline operator. When we have it, it will work with the F# proposal just fine. Right now it feels like the Smart proposal is blocking.
My vote is to remove it from this proposal and break it up into different proposals.
which just means
That's it! Simple, handy, and easy to understand
Separately, we can have some placeholder proposal
would mean
or whatever it is, it is independent.
I appreciate the desire to discourage point-free in some codebases and to enforce placeholders; I see that easily solved with linters.
The text was updated successfully, but these errors were encountered: