Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> When trying to define default values, people get confused with expressions like a === b ?? c and assume it will be parsed as a === (b ?? c). When in actuality it will be parsed as (a === b) ?? c.

I'll never understand why programmers don't simply put parens where they want the expression to be evaluated, rather than relying on their (sometimes incorrect) assumption about operator precedence. I want to chalk this up to hubris, but it's probably just laziness.



Author of the rule and blog post here. I agree that, for me, I appreciate the extra clarity of explicit parens. This lead me to explore a VSCode plugin which visually show the implicit parens even if they are not present in the code: https://jordaneldredge.com/blog/a-vs-code-extension-to-comba...

I like the idea that different readers of the same code base could opt for differing levels of explicitness when it comes to operator precedence. One thing that working on the project helped demonstrate for me is that adding parens around _every_ subexpression is _way_ too noisy. So, you need to draw the line somewhere. But for me, I prefer drawing that line on the noisier side.


I've written a static code analyzer for C/C++ code. The javascript operator precedence table is almost the same as the C/C++ operator precedence table. So I was pleasantly surprised when I could also run the script on javascript code and have reasonable errors reported as well.

One bug that appeared is bitwise-AND followed by a comparison with no parentheses grouping the bitwise-AND operation. Obviously developers thought the bitwise-AND had higher precedence than the comparison. They were wrong. They obviously didn't test the code either.

Javascript code doesn't twiddle bits as often as C/C++ code so this bug doesn't appear as often.

For text editor support of this problem, what may work is to show that an expression with higher precedence IS evaluated first before the expression using an operator lower on the operator precedence table.

There was a suggestion in the Vim mailing list years ago to use background color to show the nesting depth of a block.

One could use background color or a font attribute, say underline or font-size, to indicate the evaluation order of a non-parenthesized multi-operator expression.

HN posts only support italics for font formatting so I'll use that in my example - hmmm, looks like I can't use block formatting in addition - just imagine the code indented in a fixed-width font

====

if (x & 3 == 1) { // do something }

===


> One thing that working on the project helped demonstrate for me is that adding parens around _every_ subexpression is _way_ too noisy. But for me, I prefer drawing that line on the noisier side.

I prefer the noisier side as well. When adding parens to indicate/force precedence, I will sometimes split my expressions into multiple lines, with indentation, as an aid to legibility, e.g. instead of ( ( a + b ) / ( ( c - d ) / e ) ) I might write:

  (
    ( a + b )
      /
    (
      ( c - d )
        /
      e
    )
  )
I've found this not only quite legible, but also fairly easy to edit because you can easily match parens visually.


I appreciate the attempt, but don't the explicit parens make it pretty clear?

I feel like if you just remove the spaces around the parens themselves, it's the most clear: ((a + b) / ((c - d) / e))

Nowhere I have worked would pass a code review for what you have proposed.


I'm curious, what is it about my multi-line expression is unclear? I find it easier to match parens when they line up vertically (similar to aligning braces in C functions) than when they're all on one line. True, most IDEs will happily show you a matching paren when you hover over one, but that still doesn't help you understand how a long, one-line expression really breaks down.

Another example: do you write { foo: bar, baz: bat, bing: bang } or:

  {
    foo: bar,
    baz: bat,
    bing: bang
  }
If you use the latter method, why wouldn't you write your expressions the same way? Obviously not the ultra simple ones like a + b, but the longer ones.

Incidentally, I used to write code with no spaces between parens, which is what you suggested: ((a + b) / ((c - d) / e)), but eventually found it much easier to always put spaces around parens: ( ( a + b ) / ( ( c - d ) / e ) ). I would say this is similar to the indent-with-spaces-vs-tabs argument, which will rage forever.


If it isn't just a b c d e but longer variable name or function call, then I imagine it wouldn't be as clear.


This. An extreme example, where I think my solution works best, is SQL statements that are hundreds of lines long. No sane developer would put that all on one line.


Ahh, I think your example would have communicated the benefit of what you were sharing more clearly with longer variable names. For single word variables, though, it feels needlessly verbose.

To answer your earlier question about object definitions, in cases where the objects are small, I do think more concise (single line) notation can be more readable. An example, though not a great one because it's data more than it's code:

  items: [
    { type: "a", quantity: "12", price: 123.45 },
    { type: "b", quantity: "7", price: 456.78 },
    { type: "c", quantity: "3", price: 9.00 },
  ]
If the number of properties exceeds say 3, or the names of them are complex, I would lean toward the longer form.


I would rather argue to add temp variables and name parts of the expression, so you can actually understand and communicate what this does.


Tell me you're a lisp programmer without telling me you're a lisp programmer.

The thing for me is my text editor visually matches the braces, so the second option makes that harder.


Thanks, I hate it.


Because there's a threshold of where you know how things will be parsed. You wouldn't write:

   a=(b+c)
Because you know it's not necessary. But we're human and sometimes make mistakes when we're sure we didn't.

That's also why we allow for some misunderstanding, (rather than (writing (using) (sentence trees))).


Agreed, that would be one level too far, and thus unnecessary. I'm not arguing that every operator should be parenthesized, but I think parens should be used more than most programmers tend to do.

On a related note, I occasionally write expressions that both assign and test, e.g. if ( ( a = b ) == c ) and in those cases I parenthesize liberally and always write a comment that indicates yes, I intend to make an assignment in the middle of a test for equality.


Oof really? That’s a horrible pattern! In my professional opinion of course. Separation of causes, man! :)


Yeah, but separation of causes then violates the DRY principle because you have to repeat the 'a' expression twice:

  a = b
  if ( a == c ) ...
The repetition is even worse if you replace 'a' or 'b' with a more complex expression:

  a[foo/bar] = b[baz/bat]
  if (a[foo/bar] == c) ...
Oof indeed! Just rewrite it as:

  if ( ( a[foo/bar] = b[baz/bat] ) == c ) ... // Yes, assign and test!


Hmm, no. You rewrite it as

    key1 = foo/bar
    key2 = baz/bat
    a[key1] = b[key2]
    if (a[key1] == c)
I'm sure a coder who understands the context can come up with better names than key1 and key2.

edit: or better yet

    key2 = baz/bat
    normal_var = b[key2]
    if (normal_var == c)
    ...
    key1 = foo/bar
    a[key1] = normal_var


While your solution splits the whole thing into smaller and arguably more easily digestible steps, you have introduced two new variables, key1 and key2, which need to be carefully scoped (if they aren't already) so they don't clash with existing variables. If your project consists of many instances of code like this, you end up with many variables which can become harder to manage.

You're also splitting what is intended to be essentially an atomic operation into multiple steps, which can be good if you want to analyze and tweak them in the future, but it's now no longer clear where the process begins and ends in relation to the code that comes before and after: you have to add more comments, or split the whole thing out into its own function.

I'm not saying your code is bad or wrong, just that there are downsides to any solution (including mine), and ultimately everybody has to pick whichever has the fewest negatives for their particular project.


Wow - I’ve been coding for a long time and find your assign and test really hard to parse.

In a code review I would definitely reject this.


Just curious, do you also find the ternary operator hard to parse? This is a serious question: I've found some programmers tend to avoid it entirely, while I think there are some clear cases where it's advantageous to use.

What I'm getting at is one person's "easy to parse" is another person's "difficult to parse", and there may be no objective answer which makes one any better than the other.


To be honest I think it really depends. And in some contexts it is unavoidable.

I would think to myself if I was writing one “have I made a mistake somewhere that means I have to use this?”

I don’t think it’s necessarily about “can I parse this”, it’s much more about will all the devs on the team be able to parse this.

To me, code is all about communication - to myself, but also to an audience I haven’t met with varying skill, knowledge and context levels.

I’ve been bitten too many times with code that only one or two people can work on.


Mission critical automotive code under ISO(ASIL) that usually follows MISRA, assign and compare in the same clause is banned. If you're writing C code that lives don't depend on, do whatever you want, but I pity people that have to own your code.


I hope this is satire, DRY is more about extraction of shared function not about individual expressions. You write code once, no need to save yourself typing time at the expense of you and other peoples ability to read and understand the code afterward.


> DRY is more about extraction of shared function not about individual expressions

I'm not sure where you get that idea. Repetition anywhere is usually a code smell.

> You write code once

And then you update it when you add new features or the requirements change or you fix bugs, etc. Having to change two symbols is more error prone than changing one. And having to parse more code is harder than parsing less code.

The assign-and-test pattern is common in several languages (e.g. C), and adding a comment that explains the logic should remove all doubt as to what is happening and why, so I see it as a win/win.

In any case, there is a trade-off between terseness and legibility, and while I usually favor more verbose code, I tend to draw the line at needless repetition. But that's my personal preference, and everybody draws that line in different places.


> And having to parse more code is harder than parsing less code.

I'd rather parse three straightforward copies with minor differences than one chunk of dry spaghetti. And in assign & test case, splitting them makes debugging much easier.


It's very common in C

  while((buf = resultofstreamingoperation() != null) {
    
  }
The ` != null ` is redundant here but you can imagine where it'd be required.


A more common form (and slightly less evil) is for error checking:

    if ((error = do_a_thing())) {
        // some error happened, good thing we saved the error code
    }
Given the standard "zero is success" paradigm, handling errors is very succinct.


You are arguing "common" == "good". That's not true.


Also it’s easy to make assumptions and have them slip by when more experienced. Like how the ?? operator works in C# or even just the bitshift or logical operator order which is pretty much standardized across languages by now. But still, this can confuse readers or even yourself on a tired day.


I personally make liberal use of parens to make the precedence of operators as clear as possible. Then I run formatting tools like Prettier or rustfmt on my code, which remove any parens that are not strictly required. This way, I end up with an expression that is clear as well as concise. Best of both worlds.


This is the approach I take as well. But it's not necessarily best of both worlds. Removing (or not inserting) technically useless parens can obscure bugs if you are not perfectly fluent in the precedence rules of the language. Their doubly easy to miss if there's a lot going on on one line.

Here's one example that the rule caught: https://github.com/captbaritone/vscode/blob/ab86e0229d6b4d0c...

I've been writing JS for over 10 years now, and I'm not sure I would have caught that in code review.


Ahh... there's a strict equality operator comparing undefined (both a value and a type) to something that's being coerced to a boolean... it will always return false. The first thing I look at when I see a strict equality comparison is "are the operands actually the same type" because that's a very common mistake (or so I've noticed)!

Huh, now I'm curious if my IDE would highlight this with a warning... will have to check!


Prettier oftentimes removes a lot of parentheses that I had hoped it would leave.

I’ve taken naming them as variables instead. Definitely more verbose but more readability.

    const aIsB = a === b;
    … aIsB ?? c …


That is how it evaluates, not what the original author intended in the example in the OP.

    return a === b ?? c;
The intent was

    const bOrDefault = b ?? c;
    return a === bOrDefault;
You posted the actual (buggy) evaluation. I agree that its better if that is what you intend.


Wait, your example doesn't even make sense to me on second thought (and was the ultimate point in the article)

aIsB is guaranteed to not be null or undefined so the ?? is a no-op. You just did a comparison so it's either true or false.

For general clarify for precedence I still agree but in this case it's a silly thing to do. It would be more clear without the syntax shorthand:

    const ret = a === b
    if(ret == null || ret == undefined)
    {
        ret = c
    }
    return ret


I was referring particularly to my parent's point about adding parentheses to remove ambiguity. That prettier oftentimes removes parentheses that I explicitly want. So rather than risk that, I remove the ambiguity by moving one of the comparisons off into a named variable.


I agree.

I was just pointing out that this is a case prettier wouldn't do anything. The expression parses differently and adding or removing parentheses to any subexpression will change its meaning.

That by manually doing the transform in your example that you would realize your cody was a no-op or that the intended version was what I posted.


I put them there, but some linter will then complain about "unnecessary parenthesis", so you can't win.


I think one reason is that when I am refactoring I am thinking about other larger logic, and as expressions get reduced, I become blind to the details that I and previously internalized as correct. Linters really help because I can’t always be thinking at both ends of the abstraction.


> I want to chalk this up to hubris, but it's probably just laziness.

You may be able to chalk it up to some “senior” reviewer who asks a junior developer to remove the parens because “they aren’t needed”, and the junior developer doesn’t know how to argue their case.


In fact, I’ve seen an extension several times: senior developers performing code review, and requesting that unnecessary, but clarifying, parens be removed. Not excessive parentheses, a single pair in a line or statement.


Precedence was a mistake and new language designs should throw it in the garbage.

Left to right is fine. So is right to left. Just pick one and stick with it.

“But PEMDAS!” Ok, YOU can use parens.


LISP has entered the chat.




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: