New: Logical Assignment Operators

Since I finished the book, a new proposal has reached Stage 3 4 (as of the July 2020 meeting): Logical Assignment Operators. It adds three new compound assignment operators to the language: ||=, &&=, and ??=. At first glance it seems like these would be just like the existing compound assignment operators (+=, -=, and such), but they have a twist: They short-circuit (even the assignment part).

First let's look at basic operation, then we'll come back to short-circuiting.

Basic Operation

Like their mathematical cousins, the new operators combine an operation with an assignment. ||= (the proposal calls it "Or Or Equals") combines || with assigning back to the left-hand side:

let left = false;
let right = true;
left ||= right;
console.log(left); // true

Similarly, "And And Equals" does the same for &&:

let left = true;
let right = false;
left &&= right
console.log(left); // false

And "QQ Equals" (as the proposal calls it; I prefer "Nullish Equals") does it for nullish coalescing (see Chapter 17 of the book):

let left = undefined;
let right = 42;
left ??= right;
console.log(left); // 42

So far, so simple. Now the twist: Depending on the value of the left-hand side, the assignment part of that may not happen at all thanks to short-circuiting.

Short Circuiting

You may know that the logical operators short-circuit their operation. For instance, left || right doesn't evaluate right at all if left is already truthy; there's no need to evaluate the right-hand side, the operation will result in left's value. If the right-hand operand involved side-effects (perhaps it's a function call), the fact it isn't evaluated is important.

The logical assignment operators short-circuit too, but they don't just skip evaluating right's value, they also skip the assignment back to left! After all, there's no point in writing left's own value back to it. You can see that in practice if you're using an accessor property:

const obj = {
    _left: true,
    get left() {
        const result = this._left;
        console.log(`Getting left: ${result}`);
        return result;
    },
    set left(value) {
        console.log(`Setting left: ${value}`);
        this._left = value;
    }
}

// Using a simple assignment and `||`:
obj.left = obj.left || false;
// =>
// Getting left: true
// Setting left: true

// Using compound assignment:
obj.left ||= false;
// =>
// Getting left: true

(Live copy on Babel's REPL)

Notice there's no "Setting" log line; that's because the setter wasn't called, because the operation short-circuited.

Details in the proposal of course, but here's the fundamental logic (slightly simplified) for left op= right where op is ||, &&, or ??:

  1. Get the Reference for left and call it lref. (A Reference is basically a spec mechanism for identifying where a value came from, such as a variable, parameter, property, etc.)
  2. Get the value of lref and remember it as lvalue
  3. Perform the operation that op would perform on lvalue to determine whether to short-circuit; if short-circuiting, return lvalue and skip the remaining steps
  4. Get the Reference for right, get its value, and remember the value as rvalue
  5. Store rvalue in lref
  6. Return rvalue

For ||= Step 3 is:

  1. Convert lvalue to boolean; if the result is true, return lvalue and skip the remaining steps

For &&= it's the same but with false:

  1. Convert lvalue to boolean; if the result is false, return lvalue and skip the remaining steps

For ??= it's just a straight check:

  1. If lvalue is neither undefined nor null, return lvalue and skip the remaining steps

There were arguments on both sides of whether the assignment part should be short-circuited. Concerns about short-circuiting came from at least two perspectives:

  • It makes these operators different from the mathematical compound assignment operators, which never short-circuit; left += 0 always writes back to left, which is something you can observe if it's an accessor property.
  • It makes the naïve explanation "left ||= right is basically left = left || right" incorrect, since left = left || right will always write back to left but left ||= right may not; which, again, is observable (for instance, if left is an accessor).

Countering those concerns:

  • The ||, &&, and ?? operators are already different from mathematical operators. ||, &&, and ?? short-circuit; mathematical operators don't. Since they're already different, it's reasonable that the compound assignment versions of them are different as well.
  • The naïve explanation is already incorrect. left += right is observably different from left = left + right even though it always writes back to left: In the compound form, left is only evaluated once, but in the simple form, left is evaluated twice. That's observable if left isn't just a simple variable or property reference, but something with a side-effect, like counter[index++] += 42, which is quite a bit different from counter[index++] = counter[index++] + 42.
  • Ruby, C#, and CoffeeScript all have these operators, and all short-circuit them. Ruby's have had this behavior for well over a decade. Making JavaScript different in this regard would break people's intuition coming from other languages with similar operators.

Destructuring...?

You may be wondering, as I was, whether this is valid:

// Is this valid...?
({ left } ??= obj); // Meaning something like `left = left ?? obj.left`

No, it isn't. Which is consistent with how the mathematical compound operators are handled. For now, the assignment target must be simple, not destructuring.

I don't know why I never thought to wonder about that with mathematical compound assignments, but I never did; and yet, I immediately wondered about it with these logical ones, particularly ??=. That said, the cognitive load of understanding that destructuring nullish coalescing logical assignment is...well...pretty high. :-)

What I didn't think to ask, though, was a question asked by others in a couple of proposal issues, such as #7: Can you use ??= to provide a default value for a destructuring target when the source value is null (rather than only when it's undefined), like this?

// Can you do this...?
const obj = { value: null };
const {value ??= "default"} = obj;
console.log(value); // "default"

The answer is no, you can't, but not because people necessarily thought it was a bad idea; it's just that destructuring patterns are a completely different beast from compound assignment operators, so it was out of scope for this proposal. It could be the subject of a future proposal, though...

Wrapup

I'm really glad to see these new operators, and glad the semantics ended up the way they did. I'll definitely be using them.

Happy coding!

Have a question or comment about this post? Ping me on Mastodon at @tjcrowdertech@hachyderm.io!