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

Open
gilbert opened this Issue Dec 11, 2015 · 44 comments

Comments

Projects
None yet
@gilbert
Collaborator

gilbert commented Dec 11, 2015

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?

obj.x || defaults.x |> process
//
// With lowest precedence:
//    process(obj.x || defaults.x)
//
// With higher precedence:
//    obj.x || process(defaults.x)
//

arg |> m.f || m.g |> h
//
// With lowest precedence:
//    h( (m.f || m.g)(arg) )
//
// With higher precedence:
//    m.f(arg) || m.g()
//
@RangerMauve

This comment has been minimized.

RangerMauve commented Dec 11, 2015

Personally, I think the lower precedence makes more sense since I can see people having stuff like m.f || m.g in the middle of their pipeline.

@eplawless

This comment has been minimized.

eplawless commented Dec 11, 2015

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;
@gilbert

This comment has been minimized.

Collaborator

gilbert commented Dec 11, 2015

@eplawless Pipeline |> will always have lower precedence that dot ., so both of those cases will apply .length before invoking with |>

[1,2,3].filter(...) |> (arr) => { ... }.length
//=> (arr) => { ... }.length( [1,2,3].filter(...) )

"test" |> func.length
//=> func.length("test")
@eplawless

This comment has been minimized.

eplawless commented Dec 11, 2015

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.

@gilbert

This comment has been minimized.

Collaborator

gilbert commented Dec 11, 2015

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... :)

@gilbert

This comment has been minimized.

Collaborator

gilbert commented Dec 11, 2015

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 arg |> (m.f || m.g), which is effectively saying "use this function if it exists, else this other function". Usually short-circuiting || is used for values as opposed to functions.

@gilbert

This comment has been minimized.

Collaborator

gilbert commented Dec 11, 2015

On the other hand, having lowest precedence would keep a consistent relationship with all other operators. Considering || might be the only potentially confusing case, maybe |> should stay lowest precedence...

@pygy

This comment has been minimized.

Contributor

pygy commented Dec 13, 2015

I'd personally put it between 11 and 12 in this table, i.e. with a higher precedence than < and friends, in and instanceof, but lower than >> etc.

@sdgluck

This comment has been minimized.

sdgluck commented Dec 14, 2015

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 || to select a truthy value from a pair, as in m.f || m.g.

The following examples do not make it immediately clear what precedence || has:

arg |> m.f || m.g |> h // (A)
arg |> m.f || m.g

If we consider each pipeline operator to take an expression then I think it is right that || should be considered a part of the expression given to the pipeline operator, not the expression as a whole. This is better demonstrated by moving each pipeline expression from example (A) to its own line:

arg
    |> m.f || m.g
    |> h

It would be quite confusing if what this actually meant is:

( arg
    |> m.f ) || ( m.g
    |> h )

With a lower precedence, if we wanted we could then force the whole expression to break at || by wrapping each part in brackets:

( arg |> m.f ) || ( m.g |> h )
@pygy

This comment has been minimized.

Contributor

pygy commented Dec 14, 2015

The focus on || and vertical disposition misses the forest for the tree, IMO. || already has one of the lowest precedence. With a low precedence,

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.

@joezimjs

This comment has been minimized.

joezimjs commented Dec 14, 2015

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.

@tabatkins

This comment has been minimized.

tabatkins commented Dec 15, 2015

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 -> if we want it to be reasonably ergonomic, and -> is already low, at level 3 iirc in the MDN table.

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. ^_^

@pygy

This comment has been minimized.

Contributor

pygy commented Dec 15, 2015

Taking the precedence of => into account is indeed crucial. If it is so low, then we need |> to be even lower, indeed.

@joezimjs

This comment has been minimized.

joezimjs commented Dec 15, 2015

@tabatkins I don't see any arrows on that table, neither -> or =>. I don't think the fat arrow syntax is an "operator"... just syntax denoting an arrow function.

@pygy

This comment has been minimized.

Contributor

pygy commented Dec 15, 2015

From the proposal:

  • => parses as if it were a low-precedence (assignment) operator joining a restricted comma expression (implicitly quoted) to a body.
    • This avoids precedence inversion when the body is an AssignmentExpression.
    • No preference for object literal over block (see [[strawman:block vs object literal]] to keep grammar simple and match expression-statement.
@gilbert

This comment has been minimized.

Collaborator

gilbert commented Dec 15, 2015

Taking the precedence of => into account is indeed crucial. If it is so low, then we need |> to be even lower, indeed.

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)
@pygy

This comment has been minimized.

Contributor

pygy commented Dec 15, 2015

That's tricky. I think that |> has to be left-associative, i.e. a |> b |> c means (a |> b ) |> c.

However, I can't see how we could have a |> b => foo(b) |> c interpreted as (a |> b => foo(b)) |> c because the parser keeps on reading the right side of => as long as it finds a valid expression, and foo(b) |> c is one. This leaves us with your second option, which is nice.

Taking the precedence of => into account is indeed crucial. If it is so low, then we need |> to be even lower, indeed.

That was thus idiotic :-) The precedence of => is of little concern (actually, ()=> is a prefix, unary operator). We don't want to be lower than level 3 (yield a |> b shouldn't mean (yield a) |> b), but we could be higher.

So, I'll stand by #23 (comment) (between level 11 and 12) and #23 (comment) (take into account what other languages do).

@tabatkins

This comment has been minimized.

tabatkins commented Dec 16, 2015

I don't see any arrows on that table, neither -> or =>.

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 understand you correctly, I previously discovered that in the simple case, both of these are equivalent:

If I'm reading it right, they're equivalent in the impure world, too, which is nice.

@jonathanKingston

This comment has been minimized.

jonathanKingston commented Dec 16, 2015

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)?
As much as I syntactic sugar can be useful there isn't much this adds that a simple function could. This would add an extra function call in the expression though.

Some explanation here:
https://gist.github.com/jonathanKingston/4df71289a2cd8dd8306a

@joezimjs

This comment has been minimized.

joezimjs commented Dec 16, 2015

@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.

@jamen

This comment has been minimized.

jamen commented Dec 16, 2015

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 don't like the requirement to use the keyword this to compose functions. JS already has many features to support the keyword this: prototypes, method invocations, function binding, arrow functions, and probably others. I prefer a feature that assists the other side of the spectrum.

I disagree with this. Since many features do use and support the use of this, they correlate better and make the operator more usable with those features. Making it a overall more useful operator.

@barneycarroll

This comment has been minimized.

barneycarroll commented Dec 16, 2015

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 this in our utility functions (like Trine does), we can write another function to make bind operator work with our existing toolkit:

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?

@jonathanKingston

This comment has been minimized.

jonathanKingston commented Dec 16, 2015

The proposal isn't invalidated by having functional equivalents in current Javascript.

I agree totally as functionally most new features can be written in JavaScript pretty simply with the exception of features like Workers and Crypto.

But this is all besides the point — the proposal is about developer ergonomics, isn't it?

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.
I'm also aware of the argument "if you don't want it - don't use it", I'm not sure how much that helps personally.
I would much rather browsers concentrate on fixing current hard issues like for example RegEx character classes which requires large libraries to use: http://xregexp.com/plugins/

Again sorry for playing devils advocate etc 😅

@joezimjs

This comment has been minimized.

joezimjs commented Dec 17, 2015

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)

@nmn

This comment has been minimized.

nmn commented Jan 2, 2016

Considering everything said here, I seem to think that the operator precedence needs to be very low. Lower than ||. In Babel (Babylon) terms this would mean an operator precedence of 0.

@pygy comment, however, gives me reservations about the arrow function situation.

I tried to overload the | operator to act like the pipeLine operator instead, and it threw errors on arrow functions. I had to wrap my arrow functions to make them work inline.

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:

async function(a){
  return a
    |> await someFunc
}
@benlesh

This comment has been minimized.

benlesh commented Jan 4, 2016

I guess I'd expect

sugar desugared
obj > func
obj > func1
obj > func1
obj > (func1

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.

@pygy

This comment has been minimized.

Contributor

pygy commented Jan 4, 2016

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...

@azz

This comment has been minimized.

azz commented Jan 7, 2016

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 ... and above ,.

# Expression 10 < P < 11 P = 3 0 < P < 1
19 (y) |> (z) (y) |> (z) same same
18 y.a |> z.a (y.a) |> (z.a) same same
y[a] |> z[a] (y[a]) |> (z[a]) same same
new y(a) |> new z(a) (new y(a)) |> (new z(a)) same same
17 y(a) |> z(a) (y(a)) |> (z(a)) same same
new y |> z (new y) |> (new z) same same
16 y++ |> z (y++) |> z same same
15 !y |> z (!y) |> z same same
++y |> z (++y) |> z same same
typeof y |> z (typeof y) |> z same same
delete y.a |> z (delete y.a) |> z same same
14 y ** a |> z (y ** a) |> z same same
y * a |> z (y*a) |> z same same
13 y + a |> z (y+a) |> z same same
12 y << a |> z (y<<a) |> z same same
11 y < a |> z (y<a) |> z same same
a in y |> z (a in y) |> z same same
a instanceof y |> z (a instanceof y) |> z same same
10 y == a |> z y == (a |> z) (y==a) |> z same
y !== a |> z y !== (a |> z) (y!==a) |> z same
9 y & a |> z y & (a |> z) (y&a) |> z same
8 y ^ a |> z y ^ (a |> z) (y^a) |> z same
7 y | a |> z y | (a |> z) (y|a) |> z same
6 y && a |> z y && (a |> z) (y&&a) |> z same
5 y || a |> z y || (a |> z) (y||a) |> z same
y || a |> z || a (a |> z) || a (y||a) |> (z||a) same
4 y ? y : a |> z y ? y : (a|>z) (y?y:a) |> z same
y |> z ? z : a (y|>z) ? z : a y |> (z?z:a) same
3 y = a |> z y = (a |> z) y = (a|>z) same
y += a |> z y += (a |> z) y += (a|>z) same
y |> z = a ReferenceError z |> (z=a)? ReferenceError
2 yield y |> z yield(y |> z) same yield(y) |> z
y |> yield z (y |> yield z) same y |> yield(z)
1 ...y |> z ...(y |> z) same (...y) |> z

...y |> z would be z(...y), which is quite handy. e.g. ...[arg1, arg2] |> fn

@gilbert

This comment has been minimized.

Collaborator

gilbert commented Jan 8, 2016

Awesome report, @DerFlatulator. I agree that the last column (lowest precedence) seems the most intuitive.

@TehShrike

This comment has been minimized.

Collaborator

TehShrike commented Jan 9, 2016

I have to say, I'm super-happy to see this moving forward, I can't wait for a babel plugin I can start using!

@littledan

This comment has been minimized.

Member

littledan commented Aug 25, 2017

I'm a little skeptical of being lower precedence than assignment operators. If we make this lower precedence than assignment, then x = y |> z would be parsed as (x = y) |> z. Would you really want to have to put parentheses explicitly for such a common case, e.g., x = (y |> z)?

Maybe we actually want a different precedence for the left and the right sides, in a sort of asymmetrical way, similar to ConditionalExpression. ConditionalExpression would become

// EDIT: bad example
      ConditionalExpression[In, Yield, Await] :
        LogicalORExpression[?In, ?Yield, ?Await]
        LogicalORExpression[?In, ?Yield, ?Await] `?` AssignmentExpression[+In, ?Yield, ?Await] `:` AssignmentExpression[?In, ?Yield, ?Await]
        LogicalORExpression[?In, ?Yield, ?Await] `|>` AssignmentExpression[+In, ?Yield, ?Await]

If we don't want to require parens for the above common assignment case, we're sort of stuck in a position of not permitting the ...x |> f form suggested above, since... has a lower precedence than =.

This is still pretty low! Any thoughts?

EDIT: Sorry, this would make it right-associative rather than left-associative, a clear no-go. I need to think about the grammar a little more. Does anyone know how the other languages rank |> vs ternary operator? Some of those operator precedence tables don't even mention assignment, comma or ternary operator; maybe |> is obviously lower precedence.

@littledan

This comment has been minimized.

Member

littledan commented Nov 10, 2017

Merged the draft specification with a specific precedence; you can see it at http://tc39.github.io/proposal-pipeline-operator/ .

@pygy

This comment has been minimized.

Contributor

pygy commented Feb 1, 2018

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)?

a = b = 5 
  |> x => x + 6 
  |> console.log

would then mean

a = ( b = ( 
  ( 5 |> x => x + 6 ) 
  |> console.log
) )

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?)

@pygy

This comment has been minimized.

Contributor

pygy commented Feb 2, 2018

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.

@littledan

This comment has been minimized.

Member

littledan commented Feb 2, 2018

@pygy How would you handle the ambiguity about whether the arrow function body includes following pipeline operators or not?

@pygy

This comment has been minimized.

Contributor

pygy commented Feb 2, 2018

@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):

Conditional ::= ... `?´ Conditional `:´ Conditional | ...;

Assignment ::= LHS `=´ Assignment | Conditional; #omiting `+=´ and friends

ArrowFunction = ArrowArguments `=>´ (
  `{´ Statement* `}´ | Assignment
);

Yield = `yield´ Assignment

I propose this:

PipelineOp ::= `|>´ `await´?;

Pipeline ::= Conditional | PipeLine PipelineOp Conditional;

Assignment ::= LHS  AssignmentOP Assignment | PipeLine;

but we keep the old "Assignment" definition for arrow function bodies

ArrowFuncExpression ::= LHS  AssignmentOP ArrowFuncExpression | Conditional

ArrowFunction ::= ArrowArguments `=>´ (
  `{´ Statement* `}´ | ArrowFuncExpression
)

It gives us two expression roots, but the grammar changes for the custom ^^ placeholder would be worse AFAICT: duplicate the expression tree, i.e. the whole grammar, with and without ^^, or make the parser context-sensitive. Now that I think of it, the parsers are probably already context-sensitive anyway (for generators), so it may not be that big a change, I don't know how they are implemented in practice.

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)
@benlesh

This comment has been minimized.

benlesh commented Feb 2, 2018

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.

@gilbert

This comment has been minimized.

Collaborator

gilbert commented Feb 2, 2018

@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)
@benlesh

This comment has been minimized.

benlesh commented Feb 2, 2018

@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))
@littledan

This comment has been minimized.

Member

littledan commented Feb 4, 2018

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.

@ljharb

This comment has been minimized.

Member

ljharb commented Feb 4, 2018

How would that work with bracket access, like say if you needed to call a Symbol-named method?

@benlesh

This comment has been minimized.

benlesh commented Feb 6, 2018

@ljharb seems like it could just work like |> ['foo']() I can't think of anything offhand that would conflict with.

@rbuckton

This comment has been minimized.

rbuckton commented Feb 6, 2018

While ['foo']() is legal javascript, it would normally throw at runtime, so we aren't really stepping on anything here.

@eplawless

This comment has been minimized.

eplawless commented Feb 6, 2018

Interesting, so the grammar ends up with |>, |>.identifier, and |>[expression] constructs?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment