mirror of
https://github.com/Noratrieb/website.git
synced 2026-01-14 17:05:02 +01:00
65 lines
3.5 KiB
Markdown
65 lines
3.5 KiB
Markdown
+++
|
|
title = "Don't play the precedence game"
|
|
date = "2025-03-10"
|
|
author = "Noratrieb"
|
|
tags = ["language-design"]
|
|
keywords = ["language", "design"]
|
|
description = "Why programming languages should abolish most operator precedence"
|
|
showFullContent = false
|
|
readingTime = true
|
|
hideComments = false
|
|
draft = false
|
|
+++
|
|
|
|
If you've ever done any math, you are familiar with operator precedence, even if you don't know that word.
|
|
The result of the mathematical expression `2 + 1 * 3` is 5 and not 9, because the right multiplication expression is evaluated first, so we get `2 + (1 * 3)`.
|
|
The mathematical ordering for this is exponents, multiplication/division, and then addition/subtraction (sometimes known as "PEMDAS").
|
|
|
|
Programming languages take this much further. They add many new operators into the mix (for example, bit manipulation or comparison), and all of these operators fit into this hierarchy.
|
|
|
|
In C, it's as follows (from [the standard](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3220.pdf)):
|
|
- multiplicative (`*`, `/`, `%`)
|
|
- additive (`+`, `-`)
|
|
- bit shifts (`<<`, `>>`)
|
|
- comparison (but not equality) (`<`, `>`, `<=`, `>=`)
|
|
- equality (`==`, `!=`)
|
|
- bit and (`&`)
|
|
- bit xor (`^`)
|
|
- bit or (`|`)
|
|
- logical and (`&&`)
|
|
- logical or (`||`)
|
|
|
|
If I asked you to recite this order, you would probably get it wrong.
|
|
It's completely arbitrary and impossible to remember unless you really practice it.
|
|
|
|
But it gets worse.
|
|
|
|
Let's look at Rust (from [the reference](https://doc.rust-lang.org/stable/reference/expressions.html))
|
|
- multiplicative (`*`, `/`, `%`)
|
|
- additive (`+`, `-`)
|
|
- bit shifts (`<<`, `>>`)
|
|
- bit and (`&`)
|
|
- bit xor (`^`)
|
|
- bit or (`|`)
|
|
- comparison (`<`, `>`, `<=`, `>=`, `==`, `!=`)
|
|
- logical and (`&&`)
|
|
- logical or (`||`)
|
|
|
|
It's different! Rusts choice here arguably makes sense; you can now write `1 & 0 == 0` and it does what you want, but due to the differences between languages it's now gotten even more impossible to remember.
|
|
|
|
If you ever mix the precedence up, your code will be incorrect. And if a reader mixes them up, they will be very confused why the code is seemingly incorrect even when it is correct.
|
|
|
|
It's like a game. And the only way to win this game is to **not play**.
|
|
|
|
Enter: parentheses. We haven't talked about them before here, but they're the "P" in PEMDAS and way above multiplicative in either language.
|
|
You can use them to group operators to do exactly what you want, in an obvious way. `(1 & 0) == 0` is correct in C and Rust, and every reader knows what exactly is up.
|
|
|
|
While it can be a bit verbose, it makes the code much easier to understand without knowing the complex precedence hierarchies.
|
|
|
|
Which is where this turns into a language design post: programming languages should not have these hierarchies in the first place, and parentheses should just be required.
|
|
It seems acceptable to allow it for the basic math operations most people are familiar with, but there is no reason why `||` and `^` should have a precedence relationship.
|
|
There are also some other cases where you might want to have precedence; for example, writing `x > 0 && x < 5` is fairly clear and useful. But in general, not everything should have a relative precedence with everything else.
|
|
|
|
And until programming languages require you to do this[^lisp], we can at least do it ourselves. And maybe even enable a linter rule that requires it, if it exists for the language.
|
|
|
|
[^lisp]: LISP already does this in a way by having prefix operator syntax; you do `(add 2 (multiply 1 3))`, always adding parentheses.
|