This is unexpected behavior.
n = 77
puts n|1 - 1 # => 77
puts (n|1) - 1 # => 76
n = 80
puts n|1 - 1 # => 80
puts (n|1) - 1 # => 80
It seems n|1 - 1
is parsed as n|(1 - 1) = n|0 = n
This is unexpected behavior.
n = 77
puts n|1 - 1 # => 77
puts (n|1) - 1 # => 76
n = 80
puts n|1 - 1 # => 80
puts (n|1) - 1 # => 80
It seems n|1 - 1
is parsed as n|(1 - 1) = n|0 = n
This is correct according to the order of operations. The additive operators, which -
is part of, are a higher precedence than the binary OR operator.
EDIT: it also matches the order of operators in C.
Yes, but visually (and implicitly) it just doesn’t feel right.
Just one more gotcha to be aware of.
It doesn’t feel right due to the spacing you used. You didn’t put spaces around the |
but you did put them around the -
.
Adding to what @MistressRemilia points out, it’s also the same in Ruby, Python, SQL, Rust, Lua, and even Haskell. So if it didn’t use this same precedence, that might be unexpected, but this is a very common precedence.
In fact, the only language I was able to find where this is not the case (excluding languages like Lisp and Fortran where at least one of the operators is not infix) is Go. 77 | 1 - 1
evaluates to 76
in Go, which makes it the odd one out.
FWIW the crystal formatter formats this properly and
n = 77
puts n|1 - 1 # => 77
puts (n|1) - 1 # => 76
becomes
n = 77
puts n | 1 - 1 # => 77
puts (n | 1) - 1 # => 76
which makes order of operations much more obvious.
This is a case where the compiler can be made smarter to understand the intent (or at least an alternative meaning than the technical meaning) of the programmer.
It would be nice for the compiler to issue a warning in this case.
When seeing n|1 - 1
, it could infer the intent was not to create a pedantic way of writing n|1 - 1 = n
, and assume parsing it as n | (1 - 1)
, though technically correct was not the ergonomic goal.
Thus like with most modern editors|wordprocessors, it could either autocorrect it on the fly, or issue a warning, w/wo the autocorrect. At minimum it could ask the programmer Do you understand this may not give the result you want as written?
The Rust compiler is really good at giving warnings to code that will compile but probably needs to be changed (written better).
Anyway, as I said, this was “unexpected” (maybe better to say “undesired”) behavior.
I don’t write Crystal|code everyday, and I even forget to use //
instead of /
sometimes, as my native tongue is Ruby.
Something to think abou to make Crystal easier to use, and make programmers using it (more) happy.
If you have your code editor configured properly, crystal tool format
should run every time you save your file, so having n|1 - 1
in your code should be impossible.
Similarly, if you write 1+1 * 2
, it’s not the compiler’s job to tell you that what you wrote might not be what you wanted.
And if you’re ever in doubt, parentheses are a surefire way to always get what you intended :D
Well, I disagree with that, or why should a compiler ever issue warnings and error notices. And I’m glad the Rust designers disagree with that sentiment too.
Humans write the code to do what they want (think important) for it to do.
The idea that a compiler can guess a programmer’s intent from spaces is interesting. However, thinking about the priorities of the Crystal community, it is difficult to support this idea now. It may be a good idea for the distant future. But in that future, we may not write programs by hand as we do today.
FWIW Rust issues no warning for this either.
fn main() {
println!("{}", 80|1 - 1)
}
This compiles without any warnings or hints, and prints 80, just like crystal
Similarly, rustfmt
formats this to
fn main() {
println!("{}", 80 | 1 - 1)
}
exactly like Crystal.
I also find it surprising that bitwise operators aren’t higher priority than arithmetic. It really seems unintuitive to me, but I guess the world thinks otherwise
To be more technically precise, it would be the parser
(not compilation) that would have a rule to parse n|1 - 1
to (n|1) - 1
to compile, and probably issue a warning too.
The parser already knows how to deal with expression overloads like x << 1
, to differentiate between x
being an integer bitshift vs an array store.
So this is something that could be included in Crystal 2.0. The only real issue is do you want to do it? But that’s a political question not a technical one.
Please no. Making space MORE meaningful is not a feature.
Here’s a somewhat short explanation of why it makes sense:
Mathematical order of operations was drilled into virtually everyone in elementary school.
Every (sane) person knows this by heart. Anything beyond this can change between languages since there’s no one governing body to regulate it, so you’re stuck in a limbo where other common operators have to go somewhere, but they shouldn’t mess with the fundamental mathematical ones because that would throw people off. To keep that intact, you’d have to put other operators either before parentheses, or after addition and division. The latter is clearly the better choice.
Well, then it would make sense for &
to have the same priority as *
and /
, and |
the same as +
and -
. But they don’t, all the bitwise operator priorities are separated from the arithmetic ones.
Given that we have three separated groups: 1. arithmetic multiplicative and additive, 2. bitwise multiplicative and additive, and 3. logical multiplicative and additive, in my view it’s more intuitive for group 2 to have higher priority than group 1. (Group 3 having the lowest priority is all right, of course.)
but they shouldn’t mess with the fundamental mathematical ones because that would throw people off. To keep that intact, you’d have to put other operators either before parentheses, or after addition and division.
Sorry, but this doesn’t make sense to me. But even so, I think the part of having to put other operators before parenthesis doesn’t make sense in general.
That’s because the << method gets overloaded by arrays. The parser doesn’t care about this, all it cares about is that it parses the << operator in the correct order. Later stages figure out what method to call, whether the one on Array or Int32 or what have you. The <<, whether used with an Array or as a bitshift, still obeys the order of operations as described by the language specification.
When in doubt, use parentheses.
Again, people determine the rules that gets translated into the parser to adhere to. The rules are not immutable, and can change with time, preference, or desire.
This also may shed light on why the order is as it is.
Operator priority seems to be defined here.
Perhaps changing priorities will cause many tests to fail. Not only will the tests for operator precedence fail, but much of the code in the standard library that implicitly depends on this precedence will also fail.
The idea of letting users define operator precedence within local scopes or user-defined contexts is genuinely cool — and chaos.
It reminds me a little of Ruby’s “refinement”.
While I don’t think you’re technically wrong that it’s a political question, it is also a technical question. This seems to me to relate to the Principle of Least Surprise. There’s a conflict inherent to that design principle: different users (in this case, programmers) will be surprised by different things. But, as has been pointed out, there’s an existing consensus among widely-used programming languages regarding the order of operations in this case. Most people coming to Crystal are familiar with another language already, so it’s reasonable to decide that operator precedence should follow this existing structure. It may be surprising to you, but Crystal is designed to be used by more than one person, and, unless I’ve missed an explanation above, there’s no reason to change the precedence that’s more useful than minimizing the surprise for people coming from other languages.
Regardless of how this is handled in other languages, my personal opinion is that it’s careless to combine operators when you’re not absolutely sure of the precedence without either 1) using parentheses, or 2) checking documentation. You’re entitled to your own opinion, of course. It’s just that “I assumed that my intuition about operator precedence matched up with the language and it didn’t work” isn’t very compelling to me.