Why are logical operators in JavaScript left associative?

2.5k views Asked by At

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
3

There are 3 answers

1
Pedro Rodrigues On BEST ANSWER

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

2
Peter On

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.

5
drankin2112 On

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.