Partial Operator Precedence
ziglang.org
ziglang.org
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)
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):)
(* (+ a b 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.
assign(result, add(b, mul(c, sub(d, e)), add(add(f, g), array_get(arr, i)));
(I bet I messed up the parentheses somewhere).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.
E.g. 1 & 2 == 0:
Python => True
C => 0 (False)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.
> Except that there are almost no circumstances in which "((a & b) + c) >> d" is a sensible expression.
That doesn't seem like a convincing argument to me. The question is whether or not the rules set you up for scenarios where misunderstandings are likely. Strict left to right does not - for example, given a & b << c it is immediately clear that the desired interpretation is not the actual one while b << c & a will unmistakeably do what you want.
slct(var, 6, 2) = expr; // var = (var &~ (0b111111 << 2)) | ((expr & 0b111111) << 2);
var = slct(expr, 6, 2); // var = (expr >> 2) & 0b111111;
Freaking BCPL had those in 1976, and that language was more primitive than C (it didn't even have any types beyond "machine word, reinterpreted accordingly to the operation applied to 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.
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?
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
b * b - 4 * a * c
a * b + c * d > e * f and g < hHowever, 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 ^ 7Note that I view ? as a language construct rather than an operator. If for whatever reason someone insisted on including that in the strict left to right requirement the second example still works and is easy to read. I just don't like those semantics as much because the parenthesis in something like a ? ( b >> c ) : d are otherwise unnecessary. The implicit parenthesis of expr ? expr : expr seems preferable to me.
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.
-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.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:
warning: operator precedence might not be obvious
--> src/main.rs:2:5
|
2 | 1 << 2 + 3;
| ^^^^^^^^^^ help: consider parenthesizing your expression: `1 << (2 + 3)`
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#precedence
= note: `#[warn(clippy::precedence)]` on by default