From f0a2417a466e401f9a6305d9722734f5e285008b Mon Sep 17 00:00:00 2001 From: Noratrieb <48135649+Noratrieb@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:59:43 +0100 Subject: [PATCH] add operator precedence post --- .../posts/dont-play-the-precedence-game.md | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 content/posts/dont-play-the-precedence-game.md diff --git a/content/posts/dont-play-the-precedence-game.md b/content/posts/dont-play-the-precedence-game.md new file mode 100644 index 0000000..708841d --- /dev/null +++ b/content/posts/dont-play-the-precedence-game.md @@ -0,0 +1,65 @@ ++++ +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.