Rendered at 04:12:01 GMT+0000 (Coordinated Universal Time) with Cloudflare Workers.
raphlinus 20 hours ago [-]
Worth noting, WGSL does this too, and is mostly a bit stricter than Zig: `a + b & c` and `a << b & c` are errors in WGSL but accepted by Zig. (Quick: off the top of your head, how do those parse)?
Funny that they named it "orelse" which sounds like a conditional threat to me :)
zorobo 15 hours ago [-]
Yes, felt this way when I was learning Ada. Semantics differ.
Also, no zig `andthen` :)
hansvm 13 hours ago [-]
It is a threat! You're going to give me the data I asked for orelse I'm going to pull some BS out of thin air.
andrewla 14 hours ago [-]
I personally have been increasingly in favor of eliminating operator precedence, even including associativity. It so rarely adds value and is so easy to misunderstand. The SmallTalk and APL models are at least consistent and operator agnostic, but they are unintuitive to read to inexperienced coders.
Heavy nesting of complex expressions is also not easy to comprehend immediately, but it's better than trying to unwind the precedence.
That said, maybe this approach is best; just take the small set of operators where we have all internalized the precedence and require it everywhere else.
Tarq0n 7 hours ago [-]
Nesting can be ameliorated with a pipe operator.
unrented7977 8 hours ago [-]
After being gaslit by C's operator precedence for my entire career, I've learned to just ignore it and add parentheses around every pair of operands until my linter starts crying.
ozgrakkurt 19 hours ago [-]
Here is a crazy idea, don’t use operators, just use functions.
Operators cause issues because it is not possible to cleanly express error output from an operator, it is not possible to express the bit-width of the operation, have to understand precedence.
All of these issues cause other downstream issues.
Why not add_u64()?
anonymous908213 18 hours ago [-]
Because it's ridiculously verbose. Abbreviating extremely common operations makes code far easier to read and understand than not.
> have to understand precedence
Precedence is not in any way an argument against using operators; you could disallow precedence at all (eg. each expression must have at most one operator, or rather two counting the assignment operator) while still having operators. That said, precedence is also very beneficial to readability when not abused.
> Operators cause issues because it is not possible to cleanly express error output from an operator
Incorrect. An operator can simply desugar to functions, and therefore has the exact same error semantic possibilities. If you take issue with a language not returning errors from operators, that is a language design issue, not something inherent to the existence of operators.
epolanski 18 hours ago [-]
The amount of times I've been sad that multiply or sum are longer than + or * is limited to long formulas and even then it's a non issue.
Also, functions can be used as callbacks, and offer overloads to handle partial applications with ease, pipe(2, sum(3), multiply(2)), e.g.
OskarS 18 hours ago [-]
I don't think your experience is particularly universal, it vastly differs depending on what kind of programming you do. If you're doing games stuff or graphics or signal processing or any other number of subfields of programming that require a lot of math, operators are a god-send for readability.
This Zig change is the best of both worlds: avoid the ambiguity, but keep the readability.
bennettnate5 18 hours ago [-]
I don't think character length of an expression is the issue. Reading chained operators is explicitly easier than reading nested functions by virtue of operators having infix notation and not explicitly requiring parantheses where they are not necessary to disambiguate order of operations. The prefix notation of functions moves the operation farther from the eye from operands and introduces the issue of parantheses-miscounting bugs. I'd hardly say the difference in readability of these is a non issue:
(a + b + c + d) * e
mult(add(a, add(b, add(c, d))), e)
Mawr 18 hours ago [-]
> (a + b + c + d) * e
a
|> add(b)
|> add(c)
|> add(d)
|> mult(e)
> mult(add(a, add(b, add(c, d))), e)
c
|> add(d)
|> add(b, _)
|> add(a, _)
|> mult(e)
bennettnate5 15 hours ago [-]
What a lovely pipe operator you have there to chain the functions and contribute to code readability :)
Zambyte 18 hours ago [-]
mult(add(a, b, c, d), e)
:)
mtklein 16 hours ago [-]
At that point,
(* (+ a b c d) e)
epolanski 18 hours ago [-]
Hence I said that operators are more readable for longer formulas.
xg15 3 hours ago [-]
There is also some irony that the function style makes the order of the different addition operations explicit - which in this case is not helpful, because the addition operation is associative. So the parentheses here are just noise.
(Well, almost. You might have weird floating point issues or other bugs, where the associativity does not apply anymore and knowing the evaluation order would be useful. But I think this would be rare situations)
vidarh 18 hours ago [-]
There are plenty of languages, like e.g. Ruby, where operators are just sugar for functions or methods. Meaning they can still be partially applied, have their value taken, and/or passed as callbacks.
If your language doesn't allow you to do the same thing with operators as you can do with functions, that's a language design issue and has nothing to do with operators as a concept.
flohofwoe 18 hours ago [-]
> The amount of times I've been sad that multiply or sum are longer than + or * is limited to long formulas and even then it's a non issue.
It depends on what things you work on. When I'm writing 'math-y' code (GPU shaders for renderng etc) then I want the expressions to look close to the reference paper and not artificially split them up into multiple lines of code with function calls instead of operators.
anonymous908213 18 hours ago [-]
Operators can also support those things. Operators are just syntax sugar for functions, or at least they're capable of being such. If a language chooses not to support the full range of what functions can do for operators, that is a thing you can criticise the language for (and I do!), but it is not a reason to suggest operators should be disposed with in language design altogether.
solatic 18 hours ago [-]
Zig sometimes gives you this, for example there are @divCeil, @divTrunc, @divExact, and @divFloor built-ins so that you can be more precise in your intent compared to using "/". It's rather simple to then write a linting rule that fails if you ever have a literal "/" in your codebase.
shiandow 17 hours ago [-]
Personally I think the meaning of + and * should be context dependent.
Though the main reason for infix notation is to signal associativity. Which a lot of these don't satisfy.
A lot of these are just trying to mimic natural language, which can be useful but as lisp demonstrates is not that important if the grammar is clear enough.
layer8 16 hours ago [-]
People disagree about that characterization of Lisp.
layer8 16 hours ago [-]
For even cleaner expression, let’s just draw the whole program as a syntax tree.
18 hours ago [-]
flohofwoe 18 hours ago [-]
...and while at why not also dump the assignment and indexing operators!
At that point, even assembly code is more readable. Also there's a reason why Lisp and Forth haven't exactly taken the world by storm ;)
skrebbel 20 hours ago [-]
I never realized that languages have so many strict precedence levels because that’s easier to implement than not having them.
This here seems like a very good idea and I hope other languages in active development take note. And als JS’ Prettier, which actively removes parentheses like these.
pasc1878 19 hours ago [-]
No you can have no precedence at all see Lisp and APL for examples.
Lisp because everything is explicit and only the form just inside an opening parenthesis does anything.
APL as all is scanned right to left.
amenghra 17 hours ago [-]
Things become error prone once you realize that the precedence rules are different across languages!
E.g. 1 & 2 == 0:
Python => True
C => 0 (False)
nayuki 12 hours ago [-]
Similarly, I don't like people who write code like `if 0`, because 0 is false in C and JavaScript and Python, but 0 is considered true in Ruby!
qalmakka 19 hours ago [-]
If you use the shunting yard algorithm or similar for infix binary operators it's trivial to define a table with operator precedence values and use that. You can have a bunch of operators all at the same priority but it is the same complexity really, the algorithm is pretty trivial
tsimionescu 13 hours ago [-]
The point here is that not all operators should have a concept of precedence. Expressions like "a & b + c >> d" should be invalid, as no human actually understands them unless they spend a lot of careful focus. Instead, such an expression must be written as "((a & b) + c) >> d" explicitly. On the other hand, "a + b + c" and even "a + b * c" are fine, as addition and multiplication (and subtraction and division) are so drilled into everyone in early education.
childintime 11 hours ago [-]
> Expressions like "a & b + c >> d" should be invalid
When read procedurally (left to right) it is quite okay though, and no parens are needed (but could be added).
The catch is that `1 + 2 * 3` becomes 9 instead of 7 as math eyes expect. I guess there are things to unlearn and unlearning is always hard. But consider the rules other languages have, like C regarding signed and unsigned, and Javascript regarding comparisons, compared to them this is almost trivial.
Joker_vD 4 hours ago [-]
> When read procedurally (left to right) it is quite okay though,
Except that there are almost no circumstances in which "((a & b) + c) >> d" is a sensible expression. The only reasonable interpretation is "(a & b) + (c >> d)", and even then that plus is suspicious.
In practice, the worst offenders are "a & b >> c" and "a & b << c". The first expression is usually intended to mean "(a & b) >> c" — it's a bitfield extraction; the second one is usually intended to mean "a & (b << c)" — it's clearing of all but one bitfield. As you can see, those semantics are impossible to achieve with << and >> having the same precedence.
foldr 12 hours ago [-]
Extra precedence levels don't really make parsing any easier. You have to handle precedence level ties anyway (for expressions involving multiple instances of the same operator, like 1 + 2 + 3 + 4 ...). All you have to do is decide whether any given binary operator is right or left associative.
Someone 19 hours ago [-]
I don’t find the article clear on how it fixes things, but I guess that, now
a op1 b op2 c
is an error if op1 is unequal to op2 and they have the same precedence. Do I see that right?
Also, it seems each operator will have to have a associativity to handle a future operator for exponentiation or (also potential future) custom operators.
Finally, the “What you need to know” section doesn’t mention orelse. Where does that fit in?
dan-robertson 17 hours ago [-]
The point is that there is no longer a global ‘precedence number’ where the operator with the higher precedence binds tighter. Instead, it is only valid to write your example for some combinations of op1 and op2; other combinations will require explicit parentheses.
Presumably orelse is not in the table because it requires parentheses for any other operator.
Someone 15 hours ago [-]
I would hope they implemented it with precedence numbers, as any other way is likely to be harder to write and lead to inconsistencies.
It’s harder to write because users will want to use expressions with more than one operator such as
b * b - 4 * a * c
a * b + c * d > e * f and g < h
tsimionescu 13 hours ago [-]
The first expression is fine, as it uses extremely commonly known precedence rules. The second is more arguable.
However, the following expressions should be invalid in any language, and no one should write them even in languages where they are unfortunately allowed:
a + b >> c & d && 0 % 9
a >> 4 ? b : c & d ^ 7
kmill 13 hours ago [-]
> is an error if op1 is unequal to op2 and they have the same precedence. Do I see that right?
It doesn't look like it works that way; only some operator classes are "unchainable".
A bit over 10 years ago, I remember implementing partial operator precedence in Bison, for an internship. I think it was merged, but I'm not sure anyone ever used it for anything :) The age of Bison for new parsers was already mostly over
mncharity 4 hours ago [-]
At the moment, there's a "anti-operators" theme in comments. So, in defense of operators...
comments> Hard to understand; just use functions
So turn our back on mathematics as a language?
Instead of a `_ [ _ ]` post-circumfix operator, simple concise understandable, we'll use... `(vector-ref)`? `(aref)`? A hardwired expression grammar rule?
comments> Just use scheme/lisp
Non-toy scheme parsing is not simple. In implementations where the reader isn't a misbegotten handcrafted hairball, it can be a big set of regexes. Regexes notably not expressed in a verbose functional sexp syntax.
So yes, precedence should be a partial order, not a total order. You want comparability of two operators' precedences to be a design choice, not a we-used-ints implementation artifact. Various composition constructs should require disambiguation. But operator parsing is a nice way of obtaining that.
And yes, in many language designs, you don't optimize for expression concision, or extensibility, or mathematics as an expressive language. But when you do... yay operator parsing.
Scaevolus 20 hours ago [-]
C/C++ obviously have far too many precedence levels, but you can easily have a more readable tower without forcing parentheses, like Go's 5 levels: https://go.dev/ref/spec#Operator_precedence
layer8 15 hours ago [-]
`a + b | c + d` is still confusing and prone to misinterpretation in that scheme.
sheept 20 hours ago [-]
JavaScript does something similar for the exponentiation operator. This is a syntax error:
-a ** b
Someone who learned PEMDAS in school may argue it should be equivalent to -(a * b), while a programmer may be used to unary operators binding tightest, so (-a) * b.
marcosdumay 12 hours ago [-]
The unary minus isn't in PEMDAS. Besides, I have never seen anybody that interprets -2^2 as -4 instead of 4.
elegios 18 hours ago [-]
This is one of those ideas that is very weird if you’re approaching it from more or less traditional parsing techniques, but makes a lot of sense when you think about it.
I ended up doing a bunch of work on it in a slightly broader and more formal setting during my phd (https://dl.acm.org/doi/abs/10.1145/3571251) with the expectation that it probably wouldn’t be relevant for more mainstream languages any time soon, very happy to be wrong on that.
boxed 20 hours ago [-]
I like the compromise, although I still think operator precedence in itself is evil :P Lisp kinda made the right call by mistake.
jraph 20 hours ago [-]
Made the right call, yes, but at the right time and with the right parameters above all!
tgv 19 hours ago [-]
So did Forth, but that's no guarantee for readability. APL got the worst of both worlds, IMO.
19 hours ago [-]
Luke2642 17 hours ago [-]
Saturating vs wrapping operators are so nice! Apparently compiles to 3 or 4 branchless ops on most architectures.
kibwen 14 hours ago [-]
I would like to see languages go even further. Make like Smalltalk did in 1972 and evaluate operators left-to-right with parentheses for grouping.
adrian_b 14 hours ago [-]
In reality it is much better to evaluate all operators from right to left, as APL does since 1962 (with parentheses when you want another order).
Only in this direction the rule is always consistent, inclusively with assignment and with McCarthy logical operators (i.e. what the C language writes as && and ||).
Moreover, only with this evaluation direction the non-commutative operators lead to useful operations when applied to arrays.
In order to make evaluation from left to right always consistent, far more changes would have to be done to the syntax of a programming language, than with the APL evaluation rule.
childintime 13 hours ago [-]
A procedural language is read top-down and left-to-right, so left-to-right is naturally favored. Without an example I don't get how right-to-left avoids the gotchas with non-commutative operators you hint at.
adrian_b 9 hours ago [-]
In most programming languages, assignments are evaluated right-to-left, not left-to-right, which is in contradiction with the left-to-right evaluation used for most operators. The same is true for a few other syntactic forms.
The most frequently used non-commutative operators are subtraction and division.
With left-to-right evaluation, an expression like x1 - x2 - x3 - x4 means x1 - (x2 + x3 + x4), which is seldom a useful expression, but with right-to-left evaluation it becomes an alternating sum, i.e. what in left-to-right evaluation would be written as x1 - x2 + x3 - x4.
Given an array of numbers, it is very frequent to want their sum or their alternating sum, which can be obtained by applying "+" or "-" to it.
The same happens with division where dividing the first number by the product of the following is very seldom useful, but an alternating product, i.e. the product of the ratios of consecutive numbers, is frequently needed.
The same happens for other less common operators, the alternating operations are frequently useful, unlike the operations that result from left-to-right evaluation.
kibwen 13 hours ago [-]
There's no need to switch evaluation direction, as RPN and concatenative languages like Forth demonstrate. And if one really wants a C-style language with bindings on the LHS, leaving the assignment operator as a special case would still be better than the precedence mess we currently have.
adrian_b 9 hours ago [-]
RPN and concatenative languages is what I meant when I said that for a consistent left-to-right evaluation you must make much more significant changes to the syntax of most programming languages (which are ALGOL-like) than with the APL evaluation rule.
You are right that RPN/concatenative languages is the alternative to the APL-like syntax, but they are even more distant from the traditional mathematical notation.
IshKebab 19 hours ago [-]
This is very clearly the best design for operator precedence IMO and it's nice that Zig has converged on it. It would be good to get it as a linting/formatting rule in Rust too (maybe that already exists?).
kibwen 14 hours ago [-]
> It would be good to get it as a linting/formatting rule in Rust too (maybe that already exists?).
I'm thinking of doing this for symbolasm also.
Also, no zig `andthen` :)
Heavy nesting of complex expressions is also not easy to comprehend immediately, but it's better than trying to unwind the precedence.
That said, maybe this approach is best; just take the small set of operators where we have all internalized the precedence and require it everywhere else.
Operators cause issues because it is not possible to cleanly express error output from an operator, it is not possible to express the bit-width of the operation, have to understand precedence.
All of these issues cause other downstream issues.
Why not add_u64()?
> have to understand precedence
Precedence is not in any way an argument against using operators; you could disallow precedence at all (eg. each expression must have at most one operator, or rather two counting the assignment operator) while still having operators. That said, precedence is also very beneficial to readability when not abused.
> Operators cause issues because it is not possible to cleanly express error output from an operator
Incorrect. An operator can simply desugar to functions, and therefore has the exact same error semantic possibilities. If you take issue with a language not returning errors from operators, that is a language design issue, not something inherent to the existence of operators.
Also, functions can be used as callbacks, and offer overloads to handle partial applications with ease, pipe(2, sum(3), multiply(2)), e.g.
This Zig change is the best of both worlds: avoid the ambiguity, but keep the readability.
(a + b + c + d) * e
mult(add(a, add(b, add(c, d))), e)
:)
(Well, almost. You might have weird floating point issues or other bugs, where the associativity does not apply anymore and knowing the evaluation order would be useful. But I think this would be rare situations)
If your language doesn't allow you to do the same thing with operators as you can do with functions, that's a language design issue and has nothing to do with operators as a concept.
It depends on what things you work on. When I'm writing 'math-y' code (GPU shaders for renderng etc) then I want the expressions to look close to the reference paper and not artificially split them up into multiple lines of code with function calls instead of operators.
Though the main reason for infix notation is to signal associativity. Which a lot of these don't satisfy.
A lot of these are just trying to mimic natural language, which can be useful but as lisp demonstrates is not that important if the grammar is clear enough.
At that point, even assembly code is more readable. Also there's a reason why Lisp and Forth haven't exactly taken the world by storm ;)
This here seems like a very good idea and I hope other languages in active development take note. And als JS’ Prettier, which actively removes parentheses like these.
Lisp because everything is explicit and only the form just inside an opening parenthesis does anything. APL as all is scanned right to left.
When read procedurally (left to right) it is quite okay though, and no parens are needed (but could be added).
The catch is that `1 + 2 * 3` becomes 9 instead of 7 as math eyes expect. I guess there are things to unlearn and unlearning is always hard. But consider the rules other languages have, like C regarding signed and unsigned, and Javascript regarding comparisons, compared to them this is almost trivial.
Except that there are almost no circumstances in which "((a & b) + c) >> d" is a sensible expression. The only reasonable interpretation is "(a & b) + (c >> d)", and even then that plus is suspicious.
In practice, the worst offenders are "a & b >> c" and "a & b << c". The first expression is usually intended to mean "(a & b) >> c" — it's a bitfield extraction; the second one is usually intended to mean "a & (b << c)" — it's clearing of all but one bitfield. As you can see, those semantics are impossible to achieve with << and >> having the same precedence.
Also, it seems each operator will have to have a associativity to handle a future operator for exponentiation or (also potential future) custom operators.
Finally, the “What you need to know” section doesn’t mention orelse. Where does that fit in?
Presumably orelse is not in the table because it requires parentheses for any other operator.
It’s harder to write because users will want to use expressions with more than one operator such as
However, the following expressions should be invalid in any language, and no one should write them even in languages where they are unfortunately allowed:
It doesn't look like it works that way; only some operator classes are "unchainable".
An older PR description gives more information: https://github.com/ziglang/zig/pull/25275 (There's also a small mention of keyword operators, which I presume `orelse` is. There's a nice diagram showing the precedence classes here: https://codeberg.org/ziglang/zig/pulls/30160#issuecomment-20...)
comments> Hard to understand; just use functions
So turn our back on mathematics as a language?
Instead of a `_ [ _ ]` post-circumfix operator, simple concise understandable, we'll use... `(vector-ref)`? `(aref)`? A hardwired expression grammar rule?
comments> Just use scheme/lisp
Non-toy scheme parsing is not simple. In implementations where the reader isn't a misbegotten handcrafted hairball, it can be a big set of regexes. Regexes notably not expressed in a verbose functional sexp syntax.
So yes, precedence should be a partial order, not a total order. You want comparability of two operators' precedences to be a design choice, not a we-used-ints implementation artifact. Various composition constructs should require disambiguation. But operator parsing is a nice way of obtaining that.
And yes, in many language designs, you don't optimize for expression concision, or extensibility, or mathematics as an expressive language. But when you do... yay operator parsing.
I ended up doing a bunch of work on it in a slightly broader and more formal setting during my phd (https://dl.acm.org/doi/abs/10.1145/3571251) with the expectation that it probably wouldn’t be relevant for more mainstream languages any time soon, very happy to be wrong on that.
Only in this direction the rule is always consistent, inclusively with assignment and with McCarthy logical operators (i.e. what the C language writes as && and ||).
Moreover, only with this evaluation direction the non-commutative operators lead to useful operations when applied to arrays.
In order to make evaluation from left to right always consistent, far more changes would have to be done to the syntax of a programming language, than with the APL evaluation rule.
The most frequently used non-commutative operators are subtraction and division.
With left-to-right evaluation, an expression like x1 - x2 - x3 - x4 means x1 - (x2 + x3 + x4), which is seldom a useful expression, but with right-to-left evaluation it becomes an alternating sum, i.e. what in left-to-right evaluation would be written as x1 - x2 + x3 - x4.
Given an array of numbers, it is very frequent to want their sum or their alternating sum, which can be obtained by applying "+" or "-" to it.
The same happens with division where dividing the first number by the product of the following is very seldom useful, but an alternating product, i.e. the product of the ratios of consecutive numbers, is frequently needed.
The same happens for other less common operators, the alternating operations are frequently useful, unlike the operations that result from left-to-right evaluation.
You are right that RPN/concatenative languages is the alternative to the APL-like syntax, but they are even more distant from the traditional mathematical notation.
Yes, Clippy has a "precedence" lint that's enabled by default: https://clippy.rust-lang.org/master/index.html?search=preced...
Here's what it looks like: