-
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
Operator Precedence - Higher or Lowest? #23
Comments
Personally, I think the lower precedence makes more sense since I can see people having stuff like |
What would we expect to happen in this case? [1, 2, 3, 4]
.filter(x => x < 4)
|> (arr) => { return arr.map(x => 2 * x) }
.length; Or, more simply: "test"
|> func
.length; |
@eplawless Pipeline [1,2,3].filter(...) |> (arr) => { ... }.length
//=> (arr) => { ... }.length( [1,2,3].filter(...) )
"test" |> func.length
//=> func.length("test") |
I think it's likely to be a common pitfall, I'm not sure there's anything to be done about it. I agree that lower precedence makes more sense. |
Yes, it's definitely necessary to be able to use namespaced functions: getInputs()
|> User.validate
|> User.create On the bright side, there are two different ways you can accomplish your original task: [1, 2, 3, 4]
.filter(x => x < 4)
|> (arr) => { return arr.map(x => 2 * x) }
|> _ => _.length;
// or
([1, 2, 3, 4]
.filter(x => x < 4)
|> (arr) => { return arr.map(x => 2 * x) }
).length; With "ghost methods" you would be able to do the following: [1, 2, 3, 4]
.filter(x => x < 4)
|> (arr) => { return arr.map(x => 2 * x) }
|> .length; One can dream... :) |
Upon further reflection, I think it might make more sense to have higher precedence. Taking the original examples, assuming higher precedence, it's easier to go from default to non-default than it is vice-versa: // With higher precedence:
obj.x || defaults.x |> process //=> obj.x || process(default.x)
// But it's easy to be explicit, and arguably more readable:
(obj.x || defaults.x) |> process //=> process(obj.x || defaults.x)
// Another example:
arg |> m.f || m.g |> h //=> m.f(arg) || h(m.g)
// Easy to be explicit, and arguably more readable still:
arg |> (m.f || m.g) |> h //=> h( (m.f || m.g)(arg) ) Not only that, but I imagine it's fairly uncommon to want to do |
On the other hand, having lowest precedence would keep a consistent relationship with all other operators. Considering |
I'd personally put it between 11 and 12 in this table, i.e. with a higher precedence than |
Just stopping by to say that I think a lower precedence, as outlined in @mindeavor's original examples above, is more appropriate. The main reason being that it doesn't undermine the way we already use The following examples do not make it immediately clear what precedence
If we consider each pipeline operator to take an expression then I think it is right that
It would be quite confusing if what this actually meant is:
With a lower precedence, if we wanted we could then force the whole expression to break at
|
The focus on a |> b |> c === d would mean a |> b |> (c === d) // uh ooohhh... Another factor to take into account is what other languages with similar operators do. If they agree, we should follow suit. |
Personally, I don't have a strong opinion about what the precedence should be. As long as it's documented, we can work with it using parenthesis. |
I lean strongly toward low precedence. We want the "vertical stack" use-case to work "correctly" by default as often as possible. It also has to be lower than Directly below level 3 (assignment) is yield, and I think this should be above yield, both for usability and to match everything else in the world. So add it between the current levels 2 and 3. This does mean that inline usage will suffer slightly, when used inside of an expression, but I'm fine with that. Use parens, it makes things clearer. Or just call things normally. ^_^ |
Taking the precedence of |
@tabatkins I don't see any arrows on that table, neither |
From the proposal:
|
If I understand you correctly, I previously discovered that in the simple case, both of these are equivalent: (arg |> x => f(x)) |> y => g(y)
arg |> x => (f(x) |> y => g(y)) However, if you want the ability to gather arguments from previous pipes, the latter version is better: arg |> x => (f(x) |> y => x + y) |
That's tricky. I think that However, I can't see how we could have
That was thus idiotic :-) The precedence of So, I'll stand by #23 (comment) (between level 11 and 12) and #23 (comment) (take into account what other languages do). |
Right, I took that conclusion from the wiki proposal for arrow functions, where Brendan states that they're equivalent to assignment operators. (which @pygy linked)
If I'm reading it right, they're equivalent in the impure world, too, which is nice. |
Can play devils advocate and ask based purely on all of this discussion is it really worth being added to the language (The discussion indicates developer cognitive load when being used)? Some explanation here: |
@jonathanKingston That's a very good point and I was beginning to think the same thing. This is so easy to replicate without the sugar and can be more readable due to the lack of new, odd-looking operators. |
I somewhat agree with @jonathanKingston and @joezimjs here. I like the idea of the pipeline operator and it would sadden me to see it fade off; however, at the end of the day there are multiple ways you can preform a "pipe". In fact, I really think the bind operator could squat this. One of Mindeavor's original arguments against the bind operator:
I disagree with this. Since many features do use and support the use of |
The proposal isn't invalidated by having functional equivalents in current Javascript. Bind operator is incredibly similar: function doubleSay () {
return this + ", " + this;
}
function capitalize () {
return this[0].toUpperCase() + this.substring(1);
}
function exclaim () {
return this + '!';
}
let result = "hello"
::doubleSay()
::capitalize()
::exclaim(); Or, if we can't stand to write function as(fn){
return fn(this)
}
let result = "hello"
::as(doubleSay)
::as(capitalize)
::as(exclaim); But this is all besides the point — the proposal is about developer ergonomics, isn't it? |
I agree totally as functionally most new features can be written in JavaScript pretty simply with the exception of features like Workers and Crypto.
I agree also, I'm worried however that we end up with more operators used that languages like Perl which are often criticised in having far too many operators making the language harder to read like lengthy RegEx. Again sorry for playing devils advocate etc 😅 |
Though, I have to admit, this bit of discussion is out of place. It should be a separate issue, or it should be brought up during the process while trying to move it past the "proposal" stage. Right now, let's stick to the matter at hand: operator precedence. (sorry for my own involvement in side-tracking us) |
Considering everything said here, I seem to think that the operator precedence needs to be very low. Lower than @pygy comment, however, gives me reservations about the arrow function situation. I tried to overload the The comment above seems to suggest that no matter what we make the precedence of pipeLine, the arrow Function situation will not be solved. (The parser will keep on reading while it finds valid expressions). So keeping that in mind, I think the precedence should be the lowest possible. This will probably mean that we can't support this case:
|
I guess I'd expect
I'm unsure if that's helpful, but that's what I would expect. FWIW, I think this would be the outcome in F#, but it's been a very long time since I've even played with that language. The F# operator precedence is listed here, and it seems pipes take precedence in most cases, which supports what I would expect. |
Looking at what others do, there are basically two trends: either give the pipe a precedence close to the lowest (F#, LiveScript, Elm) or give it a precedence slightly higher or equal to the comparison operators (OCaml, Julia, Elixir). Note that the precedence order is flipped in some tables (lowest on top). Drats, nothing decisive here... |
Put together a table to help out with this. Personally I find the last column the most intuitive, which places the operator very low in precedence. Below
† |
Merged the draft specification with a specific precedence; you can see it at http://tc39.github.io/proposal-pipeline-operator/ . |
Would it be possible, technically, to lower the precedence to the assignment/arrow function level while keeping the pipeline left-associative (when the assignment operators are right-associative)?
would then mean
This would make the pipeline/arrow function combination more palatable, and possibly alleviate the need for papp/ #75 / #84... (which are even terser, specialized lambdas, do we really want that?) |
Doubling down here, this could be made to work by limiting the operators that are recognized within a bare arrow function body to the assignment operators, operators with a higher precedence, and the arrow function, while adding the pipeline operator between assignment and ternaries in expressions found in other contexts. So a pipe would end a bare arrow function body the same way a coma does today. |
@pygy How would you handle the ambiguity about whether the arrow function body includes following pipeline operators or not? |
@littledan Actually, it could be placed between assignments and conditionals, provided we add an exception for the body of arrow functions: Currently, we have this (simplifying a bit, this may not be completely accurate but hopefully you'll get the idea):
I propose this:
but we keep the old "Assignment" definition for arrow function bodies
It gives us two expression roots, but the grammar changes for the custom It can be explained in plain English as "a non-nested pipeline operator terminates an arrow function's body". You can use pipelines within arrow functions using parens: const foo = x => (x |> bar |> baz) |
The use case for RxJS with this looks WONDERFUL.. but the operator precedence question is interesting. The pipeline operator will work with RxJS OOTB, but would it be: const subscription = someObservable
|> filter(x => x % 2 === 0)
|> map(x => x + x)
|> mergeMap(n =>
interval(100)
|> take(n)
|> mapTo(n)
)
|> map(x => x * x)
.subscribe(x => console.log(x))
// or
const subscription = (someObservable
|> filter(x => x % 2 === 0)
|> map(x => x + x)
|> mergeMap(n =>
interval(100)
|> take(n)
|> mapTo(n)
)
|> map(x => x * x)
).subscribe(x => console.log(x)) I feel like the latter is more readable, if a little less ergonomic. |
@benlesh Your first example would need to be written like this: const subscription = someObservable
|> filter(x => x % 2 === 0)
...
|> map(x => x * x)
|> (obs => obs.subscribe(x => console.log(x)) Otherwise the last line is parsed differently. To use a simpler example: const s = obs
|> map(f)
.subscribe()
// ...parses as...
const s = map(g).subscribe()(obs) |
@gilbert ... well I'm glad, because I liked the second example better anyhow. Obviously, RxJS could add a simple helper function to subscribe: const subscribe = (...args) => (source) => source.subscribe(...args);
const subscription = someObservable
|> filter(x => x % 2 === 0)
...
|> map(x => x * x)
|> subscribe(x => console.log(x)) |
In #75 (comment), @rbuckton proposed a syntax for this case: const s = obs
|> map(f)
|> .subscribe()
// ...parses as...
const s = map(f)(obs).subscribe() What do you all think of that option? Seems relatively intuitive to me, though it's another case to learn. |
How would that work with bracket access, like say if you needed to call a Symbol-named method? |
@ljharb seems like it could just work like |
While |
Interesting, so the grammar ends up with |
I'm very much in favour of placing the pipeline operator between assignment and conditional (3.5) I very much oppose to put it higher than conditional A pipe asignment operator would be interesting aswell (to keep conventions: |
Closing this issue, as the proposal has advanced to stage 2 with Hack-style syntax. (The precedence for |
At first I imagined the pipeline operator to be the lowest precedence, but now that the question has been raised, I'm wondering – should it instead have higher precedence?
The text was updated successfully, but these errors were encountered: