Dennis Ritchie on the priorities of && || vs. == etc. (1982)
lysator.liu.se
lysator.liu.se
if (addr & mask == 0)
without adding extra parentheses: if ((addr & mask) == 0)
TFA is not about the relative precedence between & and | (which is well understood, although disputed, and has mathematical basis), or between && and || (which mimics & and |). And TFA is not even about the precedence of the logical operators && and || vs. the comparison operators.I'm writing this because I've seen a lot of comments that, while interesting in themselves, completely misunderstood the premise.
So in short, bitwise operators have lower precedence than comparisons to allow you to write:
if (a==b & c==d) ...
but of course, this means you can't write bitwise checks like this: if (addr & mask == 0) ...
The problem could theoretically have been solved when the shortcut operators were introduced, by increasing the precedence of & and | to be higher than comparisons, but have the shortcut operators be lower. So you would be able to write both: if (a==b && b==c) ...
if (addr & mask == 0) ...
But this was not done due to concerns of backward compatibility with existing code, since now every expression using the old pattern would subtly change semantics. E.g. the first example would now be parsed as: if ((a==(b & c))==d) ...I found this rather difficult to read. You could write those expressions. They're legal C code. Whether they will have the expected semantics will depend on, well, what you expected.
The more general problem is code that relies too heavily on precedence rules in the first place. Precedence-related bugs and readability issues are easily avoided, just use parentheses. As I mentioned in another comment in this thread, some languages force the programmer to do this.
((window.location).href) == foo;
is more readable than: window.location.href == foo;In another comment [0] I mentioned that the Ada and Pony languages force the programmer to use parentheses when the expression would otherwise be confusingly reliant on precedence rules. Neither language requires unwieldy overuse of parentheses.
This C programming style advice article similarly recommends a middle-ground approach. [1]
I agree that unnecessary syntactic noise is bad (although this is essentially true by definition, as it's always a derogative). It can harm readability and make bugs more likely.
[0] https://news.ycombinator.com/item?id=38886613
[1] https://wiki.sei.cmu.edu/confluence/display/c/EXP00-C.+Use+p...
if (a==b & c==d)
I always thought using the bitwise operator as if it were a logical operator was simply a mistake, even though it works because false is 0 and true is 1.Edit: Mea culpa for reading and responding to the comments before the article.
>The priorities of && || vs. == etc. came about in the following way.
I think it takes some serious concentration to tease out that he is indeed trying to explain what you indicated. Either that, or my brain is just thoroughly cooked.
I'm not arguing with you -- I think you're absolutely right -- but man, Dennis made it hard on us here
I've always thought of it as mimicking * (multiplication) and + (addition), and you can actually use * and + in place of && and || if you wanted to (not advisable, though the idea can be used in expressions to produce branchless code, common technique in GPU shader code). I used to use this trick in TI99/4a BASIC which lacked "AND" and "OR" in "IF" statements, but because boolean expressions evaluated to integer 1 and 0 for true and false, multiplication and addition could serve as AND and OR.
found = (p != NULL) * (*p ! = 0); if (a != null && x != 0 && a.use_it(x))
{
...
}
(if (and (not (null a))
(/= 0 x)
(use-it a x))
...) a && b is just a * b
a || b is just a + b
Now you remember the precedence between them (except in broken languages, of which the only notable one is shell).a + b + c is nonzero if any of them are nonzero. (Remember each value is either 0 or 1.) So that's the intuition for OR.
a+b != 0 <=> a!=0 or b!=0
a*b != 0 <=> a!=0 and b!=0
Of course this intuition also reveals the pitfall behind this correspondence! You'd better make sure those are unsigned ints or #defined booleans, so you're not using general C expressions. 1 || -1 is true but 1 + (-1) is false.
Edit: forgot to mention: INT_MAX || 1 is true, but what about INT_MAX + 1 :)
My point was more around the conditionals being weakly typed around unsigned ints rather than a specific lack of built-ins. A lot of commenters were going into arithmetic mod 2 or philosophical issues, neither of which actually apply here.
The analogy isn't perfect, because || is also distributive over &&, but addition isn't distributive over multiplication. I think this is actually one of the essential properties that distinguishes a Boolean algebra from a ring. Someone with more knowledge of abstract algebra could probably provide more insight here, though.
The neutral element for + is 0 (x + 0 = x for any x).
The neutral element for * is 1 (x * 1 = x for any x).
Furthermore, you have arithmetic properties like x * 0 = 0 for any x (annulation) or (x + y) * z = (x * z) + (y * z) for any x, y, z (distributivity).
Similarly:
The neutral element for OR is false (x OR false = x for any x).
The neutral element for AND is true (x AND true = x for any x).
Furthermore, x AND false = false for any x, and (x OR y) AND z = (x AND z) OR (y AND z) for any x, y, z.
So OR works very much like + algebraically, and AND works very much like *.
When using 0 and 1 for false and true, AND is exactly the same as multiplication, and OR is like addition with saturation arithmetics (i.e. 1 + 1 = 1).
The common precedence rules stem from those parallels.
On the other hand, there is no strict need to have a dedicated boolean XOR operator, as it works the same as = (equals).
saturate(0 - 1) = 0
bool(0 - 1) = 1
Similar analogue for set theory, as another commenter pointed out.
It seems clear for me, because I remember learning De Morgan's Laws in electronics class and from one specific level of Turing Complete game.
My own mnemonic: SAXO. Shift, And, Xor, Or. (Like a real Saxo, it's fun to go a bit faster like this, but you do have to trust everybody involved, because any accident is probably going to end up badly for you.)
And now you know the bitwise precedence as well! And this ordering actually works out tidily for common operations: "x=a<<sa|b<<sb|c<<sb"; "x=p>>n&m"; "x=x&~ma|a<<sa"; and so on. You do need brackets sometimes, but fewer than you'd think, and it helps the unusual cases stand out.
(Main annoying thing: a lot of compilers, even popular ones such as clang and gcc, don't actually seem to know what the precedence rules actually are, and generate warnings asking you to clarify. Presumably the authors didn't realise that C has an ISO standard, that can be consulted to answer this question? Very surprising.)
The compilers do know what the precedence rules are, but they know that programmers don't routinely consult the ISO standard, so they emit those warnings to reduce the chance of error. Compilers are tools that are designed to help programmers avoid bugs. If they don't help programmers, they aren't doing their job.
It ain't no surprise if you see the crap that passes for software these days and the nosedive in quality, but that's a rant for some other time...
The analogy is that when you set 1 to true and 0 to false, 1 becomes the identity for AND, and 0 becomes the identity for OR. Just as 1 is the identity for multiplication and 0 is the identity for addition.
X * 0 = 0 | X ^ F = F
X * 1 = X | X ^ T = X
X + 0 = X | X V F = X
X + 1 = ? | X V T = T <-- This one breaks the analogy
With this, you can turn any logical expression into something that looks and feels like normal algebra, with the only weird exception being that both operators distribute over each other:
A * (B + C) = A*B + A*C
A + (B * C) = (A + B) * (A + C)
True + True = True, because "true" means "not zero" and "false" means "zero". In most all languages that permit int->bool casting, if(2) will evaluate to "true".
The warnings clang and FCC generate are a style warning because it's unclear on casual reading. Even readers who know the precedence rules will typically want to insert the parentheses manually. If the meaning were undefined, the compiler would give an error, not a warning.
More importantly, only if you don’t rely on short circuiting logic.
(Not a big hot hatch connoisseur though, I must admit - I just remember the Saxo in particular as having a reputation of hitting a bad spot on the tradeoff graph for flimsiness/power/good sense of average member of target demographic.)
[1]: https://en.wikipedia.org/wiki/Boolean_ring#Relation_to_Boole...
| is like + when operating over bools, since (bool)2 == 1
-1 || 1 != -1 + 1
0x8000 || 0x8000 != 0x8000 + 0x8000
(with a 16-bit integer, adjust accordingly for larger word sizes)
All binary messages in Smalltalk (messages with selectors consisting of punctuation, like !@+-, including punctuation sequences like "+-&|", if you want) have the same precedence, and the keyword variants (which short-circuit, using block arguments) and: and or: also have the same precedence (one level lower than the binary/punctuation messages), but because they take block arguments, are usually disambiguated:
a | (b & c) "parens needed to ensure b & c is evaluated first"
ifTrue: [self doSomething]
ifFalse: [
"because the and: is sent in a block arg to or:, it won't be evaluated unless or:'s receiver is false"
(self hasPendingTask or: [self updateTaskQueue and: [self hasPendingTask]])
ifTrue: [self processTask]]
Smalltalk is extremely elegant, powerful, and simple. 6 reserved words, 3 levels of operator precedence, and not much syntax to learn. It's what r5rs Scheme should have been.I'm a fan of both languages, but R5RS Scheme was to be an algorithmic language, and Smalltalk is a particular flavor of OO language (class-instance, single dispatch).
Would you say that doing conditionals and Boolean expressions with Smalltalk's object semantics and `ifTrue:ifFalse:` and mix of `and:` and `&` etc. is cleaner than Scheme's `if`, `and`, etc. syntax?
> 3 levels of operator precedence,
Scheme doesn't see what's wrong with fewer:
(if (or a (and b c))
(do-something)
(if (or (has-pending-task)
(and (update-task-queue)
(has-pending-task)))
(process-task)))SRFI-9 soon introduced a record type, and various Scheme implementations introduced much more.
Racket (nee MzScheme, or PLT Scheme) was one of them that introduced an object system, which was neat in some ways (e.g., mixins), but rougher in others, and thankfully it was limited to pedagogic and GUI use. There was also at least one CLOS-like. Later, Racket got a `struct` concept with some interesting hooks (e.g., inheritance/subtyping), and some simpler version of that might've been a good candidate for R5RS.
I'm don't know where RnRS is going recently, but I could imagine a fundamantal record/struct type, or interfaces more like the current Rust thinking.
> We were very pleased with this toy actor implementation and named it “Schemer” because we thought it might become another AI language in the tradition of Planner and Conniver. However, the ITS operating system had a 6-character limitation on file names and so the name was truncated to simply SCHEME and that name stuck. (Yes, the names “Planner” and “Conniver” also have more than six characters. Under ITS, their names were abbreviated to PLNR and CNVR. We can no longer remember why we chose SCHEME rather than SCHMR—maybe it just looked nicer.)
> then came a crucial discovery. Once we got the interpreter working correctly and had played with it for a while, writing small actors programs, we were astonished to discover that the program fragments in apply that implemented function application and actor invocation were identical! Further inspection of other parts of the interpreter, such as the code for creating functions and actors, confirmed this insight: the fact that functions were intended to return values and actors were not made no difference anywhere in their implementation. The difference lay purely in the primitives used in their bodies. If the underlying primitives all returned values, then the user could (and must) write functions that return values; if all primitives expected continuations, then the user could (and must) write actors. Our interpreter provided both kinds of primitives, so it was possible to mix the two styles, which was our original objective.
> But the lambda and alpha mechanisms were themselves absolutely identical. We concluded that actors and closures were effectively the same concept.
a * (x + y) = (a * x) + (a * y)
However, the reverse doesn’t work… a + (x * y) =? (a + x) * (a + y)
Why is this a problem? a ∧ (x ∨ y) = (a ∧ x) ∨ (a ∧ y)
a ∨ (x ∧ y) = (a ∨ x) ∧ (a ∨ y)
Both are true. So, who are we to say that one corresponds to multiplication, and the other corresponds to addition? The two operations are too similar to each other.It is equally true that 1*0=0 is the same as false|true=true, and 0+1=1 is the same as true&false=false.
But it is also not true that 1+1=1, so it is probably wrong to equate 'or' with '+'. The operation has the wrong properties.
As someone who sometimes dabbles in electronics, 0 = true makes a lot of intuitive sense to me. You have your pin with an open collector, your pull-up resistor, and “true” (as in, it is true that the transistor is conducting) pulls the voltage to ground, which is 0.
As someone who uses a Unix shell, 0 = true makes a lot of intuitive sense to me.
$ true; echo $?
0
$ false; echo $?
1> You can interpret "A or B" as a set union and "A and B" as a set intersection.
{True, False, Or, And} and {False, True, And, Or} are two different naming conventions for the exact same structure: the unique boolean algebra on two elements.
The array language people (APL, J, K) are going to come in and protest, but you aren't going to be able to understand them.
/s
> I remember how much more pleasant the predicate calculus became to work with after we had decided to give con- and disjunction the same binding power and thus to consider p ∧ q ∨ r an ill-formed formula.
https://www.cs.utexas.edu/users/EWD/ewd13xx/EWD1300.PDF (page 4-5)
I have (gently) jumped on peoples' cases for not using parentheses in expressions involving these operators, and will continue to do so, thanks.
...and I just realised your username adds some additional irony.
Why are we happy to avoid parentheses for these operations but not for && and ||? Probably because we are all really used to the precedences for + and *.
So at the end of the day, what's more clear depends on How familiar the engineers working on your code are with a given set of operators.
The second example is irrelevant because addition is commutative so the parentheses are meaningless. (This is why languages shouldn't override + to be a concatenation operator.)
That is, (a+b)+c == a+(b+c).
Interestingly, C integer addition is not actually associative, since (1+INT_MAX) + (-1) is UB, but 1+(INT_MAX+(-1)) should in principle be defined as INT_MAX.
((x**b) * a) + c
Is equivalent and is the clearer way to express it.The first expression looks messy and confusing, the second is completely clear in how it will work.
IMHO it's on a similar level as "== true" "== false" and variations thereof --- absolutely redundant and unnecessary, and shows a lack of knowledge. The same "explicit is better than implicit" mantra is often repeated to justify the latter, but if you think
if(x == true)
is somehow more "explicit", then surely if((x == true) == true)
is even better?So at the end of the day, what's more clear depends on How familiar the engineers working on your code are with a given set of operators
If someone is not familiar then they should be encouraged to learn and level up, rather than pulling down everyone else.
Crystallized syntax is clear, but also extremely fragile, if you take humans into account. We have limits. Everything pushes us to these limits. Any complexity spike that overlaps with secondary complexity or fatigue is above our limits. That’s where we make mistakes.
(isAdmin || (canReadDocument && canModifyMetadata)) && accountIsActive
Using parentheses makes the code a lot easier to understand here. All these variables carry state with lots of subtleties. Figuring out the implications of all the different cases becomes a lot easier when the code clearly tells them apart.
I otherwise agree with your comment: in most cases, unnecessary parentheses make the code slower to read, just like unnecessary "==true".
I wrote a piece of code that said ` if ( boolean_variable == true)`
this was meant to be "if neither false nor null", since the variable was nullable (kotlin), until someone else tried to "fix the beginner mistake" :P
luckily kotlin requires you handle the nullability and the other person immediately figured out what was going on.
The other person was me, a couple of months down the line.
In the Pony language, any expression where more than one infix operator is used must use parentheses to remove the ambiguity. [1]
The Zig language considered following Pony, but didn't. [2]
[0] http://archive.adaic.com/standards/83rat/html/ratl-03-06.htm...
C++ is at least as complicated as Ada, both in terms of using the language and in terms of writing a compiler for it.
C++ also sees plenty of use in military applications. The F-35's 141 page C++ coding standard is publicly available, for instance.
https://learn.adacore.com/courses/Ada_For_The_CPP_Java_Devel...
I find this a good compromise between being explicit and readability (Lisp syndrome).
Do not introduce priority rules that destroy symmetry. I remember how much more pleasant the predicate calculus became to work with after we had decided to give con- and disjunction the same binding power and thus to consider p ∧ q ∨ r an ill-formed formula.
[1] https://www.cs.utexas.edu/users/EWD/transcriptions/EWD13xx/E...I feel this justifies why I sometimes write code with lots of temporary variables on their own lines: It's a way to break down the problem, to name each thing (especially when it's not a simple method-call to a correctly-named method) and it also makes it easier to verify the behavior with the average line-by-line debugger or line-by-line debugging easier.
In many cases, such temporary local variables have no negative performance impact, being optimized away.
Then, since my first bug having been merged that was caused by a && vs || operator precedence mistake, I like my parens-always ESLint rule (or whatever exactly it was called).
Even redundant parens can start to look as pleasent as indents, after getting used to them.
In short, I think expressions like (a && b || c && d || e) are a footgun and linters should forbid them. Parens fit well with logical thinking, similar to relative clauses in language.
There are some languages (e.g. shell) where they have the same precedence.
Prettier (JavaScript) adds parens like you suggest automatically.
Plus the rules may be just different enough in different languages to trip up even those who try to remember them.
[ -e "${target}" ] && echo "Getting information about ${target}" && stat "${target}" || echo "Failed to stat file."
EDIT: What about the || die("...") pattern in Perl?
Or the fact that the glyphs in || are visually lower than the glyphs in &&?
Or that "and" comes before "or" in the expression "and/or"?
If I really have to touch a bash script, I just assume nothing works like I might expect from proper programming languages despite any surface similarity, and google every construct with sweat drops dripping down my face. ChatGPT has made the process somewhat more tolerable.
a && b && c is a common idiom for "stop if any step fails". In the shell language && and || are more like control flow concepts, rather than binary operators.
And a && b || c is a universal "ternary operator" (with one minor deviation) idiom across many languages, not just the shell
Early in my career I erased several megabytes of shared memory on a mini computer, ie DOZENS of users worth of memory just because I was being 'clever' and cut+pasted bits of an expression the wrong way.
Since then, as a rule, sod the priorities, parenthesis it is...
Lots of prior languages have tried either:
1. No operator precedence, expressions must use parentheses so that it's very clear
2. No operator precedence, everything just happens left to right regardless
But Carbon says what if we do have precedence, but only between operators which programmers expect to have precedence - whenever it's unclear what should happen the compiler instead rejects that, the same way many languages won't let you ask whether 5 == "Five" because you need to explain WTF you intended as most likely you've screwed up and didn't realise they're completely different types.
2 - 1 * 5
will return 5, while many would expect -3. You must apply parentheses if you want a precedence other than ltr.For example, `a + b << c` will fail to parse in a conforming implementation, as will `a << b << c` and `a && b || c`. Note however that `a && b && c` does parse. I find these rules to be well-thought through.
[1]: https://www.w3.org/TR/WGSL/#operator-precedence-associativit...
However, the precedence of & vs &&, or & vs ||, etc is a source of problems.
Do you have any examples of this? In my experience you almost always want the bitwise operators to have a higher precedence than the logical operators, as using the result of logical operators as a bitmask makes little sense. Consider e.g. `A & B && C & D`, which currently is equivalent to `(A & B) && (C & D)`, but with reversed precedence would be equivalent to the almost nonsensical `A & (B && C) & D`.
Now, the precedence of == with respect to & and | is actually problematic (as Ritchie admits as well). Having `A == B | C` interpreted as `(A == B) | C` is almost never desirable. For extra annoyance, the shift operators do have higher precedence than comparison, so `A == B << C` does what you usually want (`A == (B << C)`).
& vs == is the real problem: `(val & MASK) == CONSTANT` needs parentheses due to this mistake.
`a & (b == c)` essentially almost never makes sense (== gives you a boolean, and when dealing with booleans you can use && instead), yet that it what you get by default if omitting the parentheses.
if ((a==0) && (b==0))
instead of if (a==0 && b==0)Tangentially, I wonder if
if (a+b == 0)
generates more efficient code in presently popular languages with that syntax.Unless "equivalence" is supposed to be useful for comparing boolean expressions with unbound variables? But evaluating that would require a built-in SAT solver, to be remotely performant.
Also, just because two integers sum to 0 doesn't mean they're both equal to 0, so replacing (x == 0 && y == 0) with (x + y == 0) wouldn't be valid. Regardless, it wouldn't make for more performant code: compilers already translate (x == 0 && y == 0) into the assembly equivalent of ((x | y) == 0) automatically, without the programmer having to mess with their code.
Indeed, I forgot to specify unsigned.
Unless the integers were unsigned big-integers, in which case performing the long addition with carries would take Θ(n) time, as opposed to the simple Θ(1) operation of just checking both their bit-lengths.
arithmetic expressions?
As painful as breaking changes might be, they beat the alternative of dealing with a bad design indefinitely. At least in more modern languages, the type checker usually catches the mistakes caused by this design mistake.
Fortunately Go, Rust, and Swift all chose to defy C and fix the precedence of &, which I expect sets enough of a precedence (heh) for every language for the rest of human history to get it right.
Besides `not` (which often has different precedence depending on whether it's a keyword or a punctuator, and is a great reminder that multiple levels of pratt parsing is meaningful), `await` is the one with the most variation - in most languages, it binds tighter than binary operators, whereas in C++ it's just barely tighter than assignment.
That doesn't seem right to me. According to cppreference[1] it's the operator with the third-highest precedence, well above assignment, which is the second-lowest.
[1] https://en.cppreference.com/w/cpp/language/operator_preceden...
I've gotten surprisingly out of touch with the C++ world, even though it was my first serious language and I used to have the draft numbers memorized ...
Edit: on a quick glance, Ada uses the := as the assignment operator. However there seems to be only one version of the and, or, xor operators, used for both logical and bitwise.
1) algebraic
2) relational
3) logical
From this, bitwise & and | are algebraic, so they should have higher priority than relationals.
One problem is !, which as a logical operator should have much lower priority.
Another problem is ==, which could be interpreted as both relational (when used on integers) or logical when used on booleans).
On the other hand I don't know if there's a real upside either. It's not very difficult to use && and || and it serves as extra documentation, and everyone is used to it by now.
^ is a lot more valuable.
(a != NULL) && a->x
You don't want to right-hand side to be evaluated if the left-hand side is false.I don't know if the following addendum is apocryphal or not, but I still like it.
Follow-up question: several hundred kilobytes? Why didn't you just grep for all instances of "&"?
Dennis Ritchie: because that happened in 1972, and Ken would write grep only in 1973.
Different notations like revers polish notation also easier to parse. You can evaluate RPN only using stack.
Also, recently I learned about thread-last macro in emacs lisp. Using it you can evaluate forms from left to right, and using it you can write less parenthesis.