Why are logical operators in JavaScript left associative?
Asked Answered
J

3

25

The logical AND and OR operators are the only lazy operators in JavaScript along with the ternary conditional operator. They are tested for short-circuit evaluation using the following rules:

false && anything === false
true || anything === true

This is the same way it is implemented in Haskell:

(&&) :: Bool -> Bool -> Bool
False && _ = False
True  && x = x

(||) :: Bool -> Bool -> Bool
True  || _ = True
False || x = x

However according to MDN logical operators in JavaScript are left associative. This is counter intuitive. In my humble opinion they should be right associative. Haskell does the right thing. Logical operators in Haskell are right associative:

infixr 3 &&
infixr 2 ||

Consider the following expression in Haskell:

False && True && True && True

Because && is right associative in Haskell the above expression is equivalent to:

False && (True && (True && True))

Hence it doesn't matter what the expression (True && (True && True)) evaluates to. Because of the first False the entire expression is reduced to False in a single step.

Now consider what would happen if && was left associative. The expression would be equivalent to:

((False && True) && True) && True

It would now take 3 reductions to evaluate the entire expression:

((False && True) && True) && True
(False && True) && True
False && True
False

As you can see it makes more sense for logical operators to be right associative. This brings me to my actual question:

Why are logical operators in JavaScript left associative? What does the ECMAScript specification have to say about this? Are logical operators in JavaScript actually right associative? Does the MDN docs have incorrect information about the associativity of logical operators?


Edit: According to the specification logical operators are left associative:

LogicalANDExpression = BitwiseORExpression
                     | LogicalANDExpression && BitwiseORExpression

LogicalORExpression = LogicalANDExpression
                    | LogicalORExpression || LogicalANDExpression
Jainism answered 15/12, 2013 at 6:27 Comment(29)
The result, including short-circuiting, is the same either way, isn't it?Entourage
The result will always be the same because of the way the operators are defined. However left associative logical operators would require more evaluations/reductions as described in the question.Jainism
@AaditMShah it's best to regard logical operators as variadic and ignore formal associativity. That's what most implementations do, I assumeAntimasque
@JanDvorak How would you regard logical operators as variadic? They aren't functions. I doubt that's what most implementations do.Jainism
The compiler should be able to convert it into efficient code that doesn't do any more evaluations than are necessary.Entourage
@Entourage In that case wouldn't you agree that the compiler would treat the logical AND and OR operators to be right associative so as to generate an efficient parse tree?Jainism
If the associativity doesn't make any difference in the result, the compiler can do it either way. The authors of the specification probably didn't worry about this since it doesn't matter.Entourage
@Entourage So the associativity of logical operators is left for the implementation to decide? What does the ECMAScript specification have to say about this?Jainism
@Aadit es5.github.io/#x11.11Athalee
You can find it here: ecma-international.org/ecma-262/5.1/#sec-11.11 It's described in terms of grammar productions rather than associativity, and I'm not sure how it converts.Entourage
@Entourage It's left associative according to the specification. Hence MDN was right.Jainism
From the MDN table you linked to, it looks like they simply made all binary operators left-associative, except for assignments. In most cases, the choice is arbitrary because the operations are commutative.Entourage
Short-circuiting operators aren't commutative, but as long as you short-circuit from left-to-right, it still doesn't matter how you associate them.Entourage
@Entourage you mean associative, right? Lot of them aren't commutative, even if most of those that aren't are anti-commutative.Antimasque
I think I mean that they're associative and commutative. Ignoring the short-circuiting, a && b is equivalent to b && a. Most commutative operators are also associative, but I found this: unspecified.wordpress.com/2008/12/28/…Entourage
@Entourage && and || are only commutative for boolean arguments. Sure, it's the primary use-case (or at least the original intended primary use-case), but still..Antimasque
That's why I said "ignoring the short-circuiting".Entourage
Well, in your example if last value is False and first value is True then left association will be efficient as compared to right association.Joung
@Joung No, it wouldn't. Because of the way logical AND and OR is defined the first operand is always evaluated and the second operand is only evaluated when the first evaluation doesn't short-circuit. You cannot evaluate the second operand before evaluating the first operand. Hence even if the last operand is False you'll still need to evaluate the first operand, which in turn would need to evaluate its first operand and so on.Jainism
Following your example, wouldn't True && True && True && False be more efficient if left associative?Rocher
@Rocher No, It wouldn't. The expression True && True && True && False would be explicitly parenthesized as ((True && True) && True) && False. However because of the way && is defined you always have to evaluate the first operand before the second operand. Hence it would reduce to False in 3 reductions as follows: ((True && True) && True) && False would reduce to (True && True) && False which would reduce to True && False which would reduce to False. This behavior is demonstrated in the following fiddle: jsfiddle.net/Yp5GN Note that every term in the expression is evaluated.Jainism
Ah, so it's left associative, but still evaluated left-to-right. Why would '&&' be defined that way if it's commutative?Rocher
@AaditMShah, your comment about reductions is not relevant, since on sensible interpreters JavaScript is precompiled. I don't see how different bytecode could be generated depending on associativity of &&, ||. Thus it really doesn't matter.Cornia
@Rocher It is defined that way so that you may use them as guard and default operators respectively.Jainism
@KarolisJuodelė I don't claim to know about the internals of a JavaScript engine. The generated code may be optimized. Nevertheless because of the way they are defined in my humble opinion it seems more intuitive to make the logical AND and OR operators right associative. The associativity of operators indeed makes a difference in lazy languages like Haskell where evaluations are simply graph reductions and hence I suspect that it might also make a difference in this case as the logical operators have short-circuit evaluation. Would you show me the relevant compiler code to back your claim? =)Jainism
@AaditMShah optimization is a fancy word to use. A short circuiting if (A || B) is going to be implemented run A, conditional-jump, run B, conditional-jump. That's how C did it and that's how JS will do it (though I don't know of a way to look at the latter). What you're doing is mistaking theoretical models for reality.Cornia
It continues to short circuit, despite its associativity : function sideEffect() {console.log('run');return true}; false && true && sideEffect() === false /* with nothing printed*/Riccardo
@Riccardo The result will always be the same irrespective of the associativity. However the number of reductions for left associative logical operators would be more. For example false && true && sideEffect() is equivalent to (false && true) && sideEffect(). This would reduce to false && sideEffect() and then reduce to false. Hence we have two reductions. However if the logical operators were right associative it would only take a single reduction. The expression false && (true && sideEffect()) would reduce to false in a single reduction. Hence they should be right associative.Jainism
You can check this easy: (function(){ console.log(1); })() && (function(){ console.log(2) })() && (function(){ console.log(3) })()Lam
C
15

For any decent compiler the chosen associativity of these operators are pretty much irrelevant, and the outputted code will be the same regardless. Yes, the parse tree is different, but the emitted code doesn't need to be.

In all of the languages of the C family that I know of (to which Javascript also belongs), the logical operators are left associative. So the real question becomes, why do C-like languages define logical operators as left-associative? Since the chosen associativity is irrelevant (in terms of semantic, and in terms of efficiency), my suspicion is that the most "natural" (as in "what the majority of the other operators use") associativity was chosen, although I don't have any sources to back up my claims. Other possible explanation is that left associative operators take less stack space to parse using an LALR parser (which is not a big concern nowadays, but probably was back when C appeared).

Cocker answered 17/12, 2013 at 18:54 Comment(1)
Evaluation of boolean expressions in C-like languages behave vaguely like foldl', which is fine because they are always strict anyway.Histochemistry
E
5

Consider this code:

console.log(   true || (false && true) );   // right associative (implicit)
console.log(  (true || false) && true  );   // left associative (Javascript)

Both of those examples do in fact return the same result, but that is not the reason to worry about operator associativity. The reason it's relevant here is because of the unique way that logical operators determine their outcomes. Even though all permutations ultimately make the same final conclusion, the order of the calculations change, and that can have big implications for your code.

So, now consider this:

    var name = "";

    // ----------------------------------------
    // Right associative
    // ----------------------------------------
    name = "albert einstein";
    console.log("RIGHT : %s : %s", true || (false && updateName()), name);

    // ----------------------------------------
    // Left Associative
    // ----------------------------------------
    name = "albert einstein";
    console.log("LEFT  : %s : %s", (true || false) && updateName(), name);

    function updateName() {
        name = "charles manson";
        return true;
    }

The output is:

    RIGHT : true : albert einstein
    LEFT  : true : charles manson

Both expressions return true, but only the left associated version had to call updateName() in order to return an answer. The right associated version is different. It only evaluates the first argument in (false && updateName()) because the second argument can't change the false into a true.

Remember these two points:

  • Operator Precedence describes the nesting order of compound expressions of different operator types.
  • Operator Associativity describes the nesting order of compound expressions with the same operator precedence.

Notice that neither of the two points above change the way an individual expression is interpreted, only the way compound expressions are interpreted. Associativity happens at a higher level.

Operator associativity, as well as operator precedence, have enormous impact in how a language behaves. Understanding what those differences are is critical to being able to use and quickly grasp the functional differences in diverse programming languages. You definitely get a thumbs up from me for your question and I hope this clarifies things a little bit. Take care.

Entwine answered 24/12, 2013 at 4:4 Comment(5)
What do you mean by "...the logical operators in JavaScript are definitely right associative"? The grammar in the specification defines the logical operators as left associative. The interpreter example code shows short-circuit evaluation, it doesn't show associativity at all.Cocker
You are right. I had remembered it backwards. My main point was to address everyone's focus on the expression outcomes as opposed to their impact. Thank you. Post updated.Entwine
This is probably why JavaScript defines || and && as different operator "types" so that they are resolved with precedence, not associativity.Fluxmeter
Thank you, this answer helped me with my efforts to grasp the essence of what associativity means.Mumps
This answer has nothing to do with the original question, which is about the left associativity of each of the individual logical operators, and not about expressions mixing both (which, as this very answer explains, is resolved by precedence).Purposeful
C
-4

Simple answer: Imagine var i = null; if (i == null || i.getSomeValue()) ... When not being left associative, the second test would be evaluated first giving you an exception.

Cushy answered 23/12, 2013 at 22:54 Comment(2)
That's not what the question is about. Associativity only matters in expressions with multiple operators in a chain, like if (i==null || i.isBad() || i.getSomeValue()), which may be parsed either as (i==null || i.isBad()) || i.getSomeValue() (left-associative) or i==null || (i.isBad() || i.getSomeValue()) (right-associative).Shade
Associativity doesn't decide which operand is evaluated first. They way the operator is defined decides which operand is evaluated first. Read the question carefully.Jainism

© 2022 - 2024 — McMap. All rights reserved.